Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
2a8304b452 |
27
federated-dashboard/.gitignore
vendored
@@ -1,27 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
@@ -1,69 +0,0 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
@@ -1,23 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/panelblack.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Federated Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4242
federated-dashboard/package-lock.json
generated
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "federated-dashboard",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||
"@fortawesome/react-fontawesome": "^3.0.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
// Tailwind v4 PostCSS wiring (array form avoids loader quirks)
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("@tailwindcss/postcss"),
|
||||
],
|
||||
};
|
Before Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 16 KiB |
@@ -1,58 +0,0 @@
|
||||
<svg width="1365.826" height="280.489" viewBox="0 0 1365.825 280.489" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<mask id="b">
|
||||
<path fill="#fff" d="M-60-60H60V60H-60z"/>
|
||||
<circle id="a" cy="-40" r="3"/>
|
||||
<use transform="rotate(72)" xlink:href="#a"/>
|
||||
<use transform="rotate(144)" xlink:href="#a"/>
|
||||
<use transform="rotate(216)" xlink:href="#a"/>
|
||||
<use transform="rotate(-72)" xlink:href="#a"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<text transform="translate(-10.708 -9.297)" x="286.592" y="223.436" font-family="'Open Sans'" font-size="200" style="line-height:1.25" xml:space="preserve"><tspan x="286.592" y="223.436"><tspan font-weight="bold">ault</tspan>warden</tspan></text>
|
||||
<g mask="url(#b)" transform="matrix(2.67128 0 0 2.67128 140.242 140.242)">
|
||||
<path d="m-31.172-33.813 26.496 74.189h9.352l26.496-74.19h-9.767l-16.73 47.59Q3.014 18.348 1.87 22.4.727 26.348 0 29.985q-.727-3.637-1.87-7.689-1.143-4.052-2.806-8.728L-21.3-33.813z" stroke="#000" stroke-width="4.512"/>
|
||||
<circle transform="scale(-1 1)" r="43" fill="none" stroke="#000" stroke-width="9"/>
|
||||
<g transform="scale(-1 1)">
|
||||
<path id="c" stroke="#000" stroke-linejoin="round" stroke-width="3" d="M46-3v6l5-3z"/>
|
||||
<use transform="rotate(11.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(22.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(33.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(45)" xlink:href="#c"/>
|
||||
<use transform="rotate(56.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(67.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(78.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(90)" xlink:href="#c"/>
|
||||
<use transform="rotate(101.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(112.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(123.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(135)" xlink:href="#c"/>
|
||||
<use transform="rotate(146.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(157.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(168.75)" xlink:href="#c"/>
|
||||
<use transform="scale(-1)" xlink:href="#c"/>
|
||||
<use transform="rotate(191.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(202.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(213.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(225)" xlink:href="#c"/>
|
||||
<use transform="rotate(236.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(247.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(258.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(-90)" xlink:href="#c"/>
|
||||
<use transform="rotate(-78.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(-67.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(-56.25)" xlink:href="#c"/>
|
||||
<use transform="rotate(-45)" xlink:href="#c"/>
|
||||
<use transform="rotate(-33.75)" xlink:href="#c"/>
|
||||
<use transform="rotate(-22.5)" xlink:href="#c"/>
|
||||
<use transform="rotate(-11.25)" xlink:href="#c"/>
|
||||
</g>
|
||||
<g transform="scale(-1 1)">
|
||||
<path id="d" stroke="#000" stroke-linejoin="round" stroke-width="6" d="M7-42H-7l7 7z"/>
|
||||
<use transform="rotate(72)" xlink:href="#d"/>
|
||||
<use transform="rotate(144)" xlink:href="#d"/>
|
||||
<use transform="rotate(216)" xlink:href="#d"/>
|
||||
<use transform="rotate(-72)" xlink:href="#d"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 34 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Warstwa_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.st0{fill:#4d68c4}.st2{fill:#2bc3f1}</style><path d="M291.8 512H27.5C12.3 512 0 499.7 0 484.5V413c0-15.2 12.3-27.5 27.5-27.5h264.2c15.2 0 27.5 12.3 27.5 27.5v71.5c.1 15.2-12.2 27.5-27.4 27.5" class="st0"/><path d="M484.5 319.3h-457C12.3 319.3 0 307 0 291.8v-71.5c0-15.2 12.3-27.5 27.5-27.5h457c15.2 0 27.5 12.3 27.5 27.5v71.5c0 15.2-12.3 27.5-27.5 27.5" style="fill:#5190ef"/><path d="M484.5 126.6H220.2c-15.2 0-27.5-12.3-27.5-27.5V27.5C192.7 12.3 205 0 220.2 0h264.2C499.7 0 512 12.3 512 27.5V99c0 15.3-12.3 27.6-27.5 27.6" class="st2"/><path d="M484.5 512H413c-15.2 0-27.5-12.3-27.5-27.5V413c0-15.2 12.3-27.5 27.5-27.5h71.5c15.2 0 27.5 12.3 27.5 27.5v71.5c0 15.2-12.3 27.5-27.5 27.5" class="st0"/><path d="M27.5 0H99c15.2 0 27.5 12.3 27.5 27.5V99c0 15.2-12.3 27.5-27.5 27.5H27.5C12.3 126.6 0 114.3 0 99.1V27.5C0 12.3 12.3 0 27.5 0" class="st2"/></svg>
|
Before Width: | Height: | Size: 984 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="245.756" height="233.193" viewBox="0 0 230.397 218.62"><g fill="#fff" fill-rule="evenodd" stroke="#0288d1" stroke-linejoin="round" stroke-width="6"><g stroke-linecap="round"><path d="m98.52 180.166 128.88-74.409-92.058-53.15-128.88 74.409z"/><path d="M6.46 127.016v21.26l92.058 53.15 128.88-74.409v-21.26"/><path d="m98.52 215.596-92.058-53.15s-7.5-16.918 0-28.346l92.058 53.15 128.88-74.409v28.346z"/><path d="m98.52 130.556 128.88-74.41L135.34 3 6.46 77.406z"/><path d="M98.52 130.556 227.4 56.147v28.346L98.52 158.902l-92.058-53.15s-6.071-17.632 0-28.346z"/><path d="m98.52 187.256-92.058-53.15s-7.5-16.918 0-28.346l92.058 53.15L227.4 84.501v28.346z"/></g><path d="m156.82 15.402-55.234 31.89 21.48 1.772 3.069 12.402 55.235-31.89z"/></g></svg>
|
Before Width: | Height: | Size: 794 B |
@@ -1,9 +0,0 @@
|
||||
<svg width="101" height="22" viewBox="0 0 101 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0582 20.817C4.32115 20.817 0 16.2763 0 10.6704C0 5.04589 4.1005 0.467773 10.0582 0.467773C13.2209 0.467773 15.409 1.43945 17.1191 3.66311L14.3609 5.96151C13.2025 4.72822 11.805 4.11158 10.0582 4.11158C6.17833 4.11158 4.04533 7.08268 4.04533 10.6704C4.04533 14.2582 6.38059 17.1732 10.0582 17.1732C11.7866 17.1732 13.2577 16.5566 14.4161 15.3233L17.1375 17.7151C15.501 19.8453 13.2577 20.817 10.0582 20.817Z" fill="#292929"/>
|
||||
<path d="M29.0161 5.88601H32.7304V20.4612H29.0161V18.331C28.2438 19.8446 26.9566 20.8536 24.4927 20.8536C20.5577 20.8536 17.4133 17.4341 17.4133 13.2297C17.4133 9.02528 20.5577 5.60571 24.4927 5.60571C26.9383 5.60571 28.2438 6.61477 29.0161 8.12835V5.88601ZM29.1264 13.2297C29.1264 10.95 27.5634 9.06266 25.0995 9.06266C22.7274 9.06266 21.1828 10.9686 21.1828 13.2297C21.1828 15.4346 22.7274 17.3967 25.0995 17.3967C27.5451 17.3967 29.1264 15.4907 29.1264 13.2297Z" fill="#292929"/>
|
||||
<path d="M35.3599 0H39.0742V20.4427H35.3599V0Z" fill="#292929"/>
|
||||
<path d="M40.7291 18.5182C40.7291 17.3223 41.6853 16.3132 42.9908 16.3132C44.2964 16.3132 45.2158 17.3223 45.2158 18.5182C45.2158 19.7515 44.278 20.7605 42.9908 20.7605C41.7037 20.7605 40.7291 19.7515 40.7291 18.5182Z" fill="#292929"/>
|
||||
<path d="M59.4296 18.1068C58.0505 19.7885 55.9543 20.8536 53.4719 20.8536C49.0404 20.8536 45.7858 17.4341 45.7858 13.2297C45.7858 9.02528 49.0404 5.60571 53.4719 5.60571C55.8623 5.60571 57.9402 6.61477 59.3193 8.20309L56.4508 10.6136C55.7336 9.71667 54.7958 9.04397 53.4719 9.04397C51.0999 9.04397 49.5553 10.95 49.5553 13.211C49.5553 15.472 51.0999 17.378 53.4719 17.378C54.9062 17.378 55.8991 16.6306 56.6346 15.6215L59.4296 18.1068Z" fill="#292929"/>
|
||||
<path d="M59.7422 13.2297C59.7422 9.02528 62.9968 5.60571 67.4283 5.60571C71.8598 5.60571 75.1144 9.02528 75.1144 13.2297C75.1144 17.4341 71.8598 20.8536 67.4283 20.8536C62.9968 20.8349 59.7422 17.4341 59.7422 13.2297ZM71.3449 13.2297C71.3449 10.95 69.8003 9.06266 67.4283 9.06266C65.0563 9.04397 63.5117 10.95 63.5117 13.2297C63.5117 15.4907 65.0563 17.3967 67.4283 17.3967C69.8003 17.3967 71.3449 15.4907 71.3449 13.2297Z" fill="#292929"/>
|
||||
<path d="M100.232 11.5482V20.4428H96.518V12.4638C96.518 9.94119 95.3412 8.85739 93.576 8.85739C91.921 8.85739 90.7442 9.67958 90.7442 12.4638V20.4428H87.0299V12.4638C87.0299 9.94119 85.8346 8.85739 84.0878 8.85739C82.4329 8.85739 80.9802 9.67958 80.9802 12.4638V20.4428H77.2659V5.8676H80.9802V7.88571C81.7525 6.31607 83.15 5.53125 85.3014 5.53125C87.3425 5.53125 89.0525 6.5403 89.9903 8.24074C90.9281 6.50293 92.3072 5.53125 94.8079 5.53125C97.8603 5.54994 100.232 7.86702 100.232 11.5482Z" fill="#292929"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,9 +0,0 @@
|
||||
<svg width="101" height="22" viewBox="0 0 101 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0582 20.817C4.32115 20.817 0 16.2763 0 10.6704C0 5.04589 4.1005 0.467773 10.0582 0.467773C13.2209 0.467773 15.409 1.43945 17.1191 3.66311L14.3609 5.96151C13.2025 4.72822 11.805 4.11158 10.0582 4.11158C6.17833 4.11158 4.04533 7.08268 4.04533 10.6704C4.04533 14.2582 6.38059 17.1732 10.0582 17.1732C11.7866 17.1732 13.2577 16.5566 14.4161 15.3233L17.1375 17.7151C15.501 19.8453 13.2577 20.817 10.0582 20.817Z" fill="#fafafa"/>
|
||||
<path d="M29.0161 5.88601H32.7304V20.4612H29.0161V18.331C28.2438 19.8446 26.9566 20.8536 24.4927 20.8536C20.5577 20.8536 17.4133 17.4341 17.4133 13.2297C17.4133 9.02528 20.5577 5.60571 24.4927 5.60571C26.9383 5.60571 28.2438 6.61477 29.0161 8.12835V5.88601ZM29.1264 13.2297C29.1264 10.95 27.5634 9.06266 25.0995 9.06266C22.7274 9.06266 21.1828 10.9686 21.1828 13.2297C21.1828 15.4346 22.7274 17.3967 25.0995 17.3967C27.5451 17.3967 29.1264 15.4907 29.1264 13.2297Z" fill="#fafafa"/>
|
||||
<path d="M35.3599 0H39.0742V20.4427H35.3599V0Z" fill="#fafafa"/>
|
||||
<path d="M40.7291 18.5182C40.7291 17.3223 41.6853 16.3132 42.9908 16.3132C44.2964 16.3132 45.2158 17.3223 45.2158 18.5182C45.2158 19.7515 44.278 20.7605 42.9908 20.7605C41.7037 20.7605 40.7291 19.7515 40.7291 18.5182Z" fill="#fafafa"/>
|
||||
<path d="M59.4296 18.1068C58.0505 19.7885 55.9543 20.8536 53.4719 20.8536C49.0404 20.8536 45.7858 17.4341 45.7858 13.2297C45.7858 9.02528 49.0404 5.60571 53.4719 5.60571C55.8623 5.60571 57.9402 6.61477 59.3193 8.20309L56.4508 10.6136C55.7336 9.71667 54.7958 9.04397 53.4719 9.04397C51.0999 9.04397 49.5553 10.95 49.5553 13.211C49.5553 15.472 51.0999 17.378 53.4719 17.378C54.9062 17.378 55.8991 16.6306 56.6346 15.6215L59.4296 18.1068Z" fill="#fafafa"/>
|
||||
<path d="M59.7422 13.2297C59.7422 9.02528 62.9968 5.60571 67.4283 5.60571C71.8598 5.60571 75.1144 9.02528 75.1144 13.2297C75.1144 17.4341 71.8598 20.8536 67.4283 20.8536C62.9968 20.8349 59.7422 17.4341 59.7422 13.2297ZM71.3449 13.2297C71.3449 10.95 69.8003 9.06266 67.4283 9.06266C65.0563 9.04397 63.5117 10.95 63.5117 13.2297C63.5117 15.4907 65.0563 17.3967 67.4283 17.3967C69.8003 17.3967 71.3449 15.4907 71.3449 13.2297Z" fill="#fafafa"/>
|
||||
<path d="M100.232 11.5482V20.4428H96.518V12.4638C96.518 9.94119 95.3412 8.85739 93.576 8.85739C91.921 8.85739 90.7442 9.67958 90.7442 12.4638V20.4428H87.0299V12.4638C87.0299 9.94119 85.8346 8.85739 84.0878 8.85739C82.4329 8.85739 80.9802 9.67958 80.9802 12.4638V20.4428H77.2659V5.8676H80.9802V7.88571C81.7525 6.31607 83.15 5.53125 85.3014 5.53125C87.3425 5.53125 89.0525 6.5403 89.9903 8.24074C90.9281 6.50293 92.3072 5.53125 94.8079 5.53125C97.8603 5.54994 100.232 7.86702 100.232 11.5482Z" fill="#fafafa"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 37 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg5" xml:space="preserve" inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" sodipodi:docname="favicon-32.svg" inkscape:export-filename="favicon-32.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" viewBox="0.02 0.02 8.44 8.44"><sodipodi:namedview id="namedview7" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" showgrid="false" inkscape:zoom="16" inkscape:cx="22.15625" inkscape:cy="21.125" inkscape:window-width="1280" inkscape:window-height="971" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="layer1"/><defs id="defs2"/><g inkscape:label="icon" inkscape:groupmode="layer" id="layer1"><circle style="fill:#94a1b6;fill-opacity:1;stroke:none;stroke-width:0.264644;stroke-dasharray:none;stroke-opacity:0.196891" id="path3857" cx="4.2391791" cy="4.2440238" inkscape:label="circle" r="4.2198539"/><g style="overflow:visible;fill:#ffffff;fill-opacity:1" id="g165" transform="matrix(0.23872556,0,0,0.25044154,-18.034354,5.1243882)" inkscape:label="letter"><switch transform="matrix(1.089,0,0,1.089,-0.89733,-0.52658)" id="switch148" style="fill:#ffffff;fill-opacity:1">
|
||||
<foreignObject width="1" height="1" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||
</foreignObject>
|
||||
<g transform="matrix(0.96767,0,0,0.96767,3.9659,-1.2011)" id="g146" style="fill:#ffffff;fill-opacity:1">
|
||||
|
||||
|
||||
<g fill="#ffffff" id="g144" transform="matrix(0.53332351,0,0,0.52516097,68.999419,-22.209109)" style="fill:#ffffff;fill-opacity:1">
|
||||
<path d="M 45.672,58.148 H 27.146 c -2.861,0 -5.614,-0.651 -8.257,-1.953 -2.861,-1.409 -5.043,-3.651 -6.547,-6.725 -1.503,-3.074 -2.254,-6.455 -2.254,-10.145 0,-3.652 0.724,-6.961 2.173,-9.926 1.594,-3.219 3.803,-5.569 6.628,-7.052 1.557,-0.795 3.052,-1.355 4.482,-1.682 1.43,-0.325 3.07,-0.488 4.917,-0.488 h 17.168 v 6.789 H 29.57 c -1.415,0 -2.602,0.187 -3.563,0.558 -0.961,0.372 -1.912,1.037 -2.855,1.994 -0.943,0.957 -1.597,1.887 -1.959,2.791 -0.363,0.902 -0.543,2.027 -0.543,3.375 h 25.023 v 6.789 H 20.648 c 0,1.24 0.164,2.325 0.491,3.256 0.327,0.93 0.919,1.887 1.776,2.871 0.856,0.985 1.749,1.732 2.677,2.242 0.929,0.512 2.03,0.767 3.306,0.767 h 16.774 z" id="path136" style="fill:#ffffff;fill-opacity:1"/>
|
||||
|
||||
|
||||
|
||||
</g>
|
||||
</g>
|
||||
</switch></g></g></svg>
|
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 20 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="main_outline" x="0" y="0" version="1.1" viewBox="-0.05 98.32 512.15 315.48"><style>.st1{fill:#609926}</style><path id="teabag" d="m317.8 376-103.4-49.7c-10.2-4.9-14.6-17.3-9.6-27.5l49.7-103.4c4.9-10.2 17.3-14.6 27.5-9.6 14 6.8 22.1 10.6 22.1 10.6l-.1-88.9 13.6-.1.1 95.4s46.8 19.7 67.7 32.7c3 1.9 8.3 5.5 10.5 11.7 1.7 5 1.6 10.7-.8 15.7l-49.7 103.4c-5 10.3-17.4 14.7-27.6 9.7" style="fill:#fff"/><path d="M502.6 103.7c-3.3-3.3-7.8-3.3-7.8-3.3s-95.5 5.4-144.9 6.5c-10.8.2-21.6.5-32.3.6V203c-4.5-2.1-9-4.3-13.5-6.4 0-29.6-.1-88.9-.1-88.9-23.6.3-72.7-1.8-72.7-1.8s-115.2-5.8-127.7-6.9c-8-.5-18.3-1.7-31.8 1.2-7.1 1.5-27.3 6-43.8 21.9C-8.7 154.8.7 206.7 1.9 214.5c1.4 9.5 5.6 36 25.8 59 37.3 45.7 117.6 44.6 117.6 44.6s9.9 23.5 24.9 45.2c20.4 27 41.3 48 61.7 50.5 51.3 0 153.9-.1 153.9-.1s9.8.1 23-8.4c11.4-6.9 21.6-19.1 21.6-19.1s10.5-11.2 25.2-36.9c4.5-7.9 8.2-15.6 11.5-22.8 0 0 45-95.4 45-188.2-1-28-7.9-33-9.5-34.6M97.7 269.9c-21.1-6.9-30.1-15.2-30.1-15.2S52 243.8 44.2 222.3c-13.4-36-1.1-58-1.1-58s6.8-18.3 31.4-24.4c11.2-3 25.2-2.5 25.2-2.5s5.8 48.4 12.8 76.7c5.9 23.8 20.2 63.3 20.2 63.3s-21.3-2.6-35-7.5m244.6 87.6s-5 11.8-16 12.5c-4.7.3-8.4-1-8.4-1s-.2-.1-4.3-1.7l-92-44.8s-8.9-4.6-10.4-12.7c-1.8-6.6 2.2-14.7 2.2-14.7l44.2-91.1s3.9-7.9 9.9-10.6c.5-.2 1.9-.8 3.7-1.2 6.6-1.7 14.7 2.3 14.7 2.3l90.2 43.7s10.3 4.6 12.5 13.2c1.5 6-.4 11.4-1.5 14-5.2 12.6-44.8 92.1-44.8 92.1" class="st1"/><path d="M261.6 291.2c-6.7.1-12.5 4.7-14.1 11.2-1.5 6.5 1.6 13.3 7.4 16.3 6.3 3.3 14.3 1.5 18.5-4.4 4.2-5.8 3.5-13.8-1.5-18.8l19.5-40c1.2.1 3 .2 5-.4 3.3-.7 5.8-2.9 5.8-2.9 3.4 1.5 7 3.1 10.8 5 3.9 2 7.6 4 10.9 5.9.7.4 1.5.9 2.3 1.5 1.3 1.1 2.8 2.5 3.8 4.5 1.5 4.5-1.5 12.1-1.5 12.1-1.9 6.2-15 33.1-15 33.1-6.6-.2-12.5 4.1-14.4 10.2-2.1 6.6.9 14.1 7.2 17.3 6.4 3.3 14.2 1.4 18.3-4.3 4.1-5.5 3.7-13.3-.9-18.4l4.6-9.2c4.1-8.5 11-24.8 11-24.8.7-1.4 4.6-8.4 2.2-17.3-2-9.3-10.3-13.6-10.3-13.6-9.9-6.4-23.8-12.4-23.8-12.4s0-3.3-.9-5.8-2.3-4.2-3.2-5.1c3.8-7.9 7.7-15.7 11.5-23.6-3.3-1.6-6.6-3.3-9.9-5-3.9 8-7.9 16-11.8 24-5.5-.1-10.5 2.9-13.1 7.7-2.8 5.1-2.2 11.5 1.5 16.1-6.6 13.8-13.3 27.5-19.9 41.1" class="st1"/></svg>
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 17 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 139.3 512.2 233.5"><path d="M256 139.3c-53 0-97.9 35.8-112.1 84.4-12.2-25.5-38.2-43.4-68.2-43.4C34.2 180.2 0 214.4 0 256s34.2 75.8 75.8 75.8c30 0 55.9-17.9 68.2-43.4 14.1 48.6 59.1 84.4 112.1 84.4S354 337 368.2 288.4c12.2 25.5 38.2 43.4 68.2 43.4 41.6 0 75.8-34.2 75.8-75.8s-34.2-75.8-75.8-75.8c-30 0-55.9 17.9-68.2 43.4-14.3-48.5-59.2-84.3-112.2-84.3m0 45c39.9 0 71.7 31.8 71.7 71.7s-31.8 71.7-71.7 71.7-71.7-31.8-71.7-71.7 31.8-71.7 71.7-71.7m-180.2 41c17.2 0 30.7 13.5 30.7 30.7S93 286.7 75.8 286.7 45.1 273.2 45.1 256s13.4-30.7 30.7-30.7m360.4 0c17.2 0 30.7 13.5 30.7 30.7s-13.5 30.7-30.7 30.7-30.7-13.5-30.7-30.7 13.5-30.7 30.7-30.7" style="fill:#3784c9"/></svg>
|
Before Width: | Height: | Size: 739 B |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 14 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.st0{fill:#5275fc}</style><path d="M510 0H171.3v170.7h169.4v170.7H510z" class="st0"/><path d="M171.3 170.7H2v170.7h169.4V170.7z" class="st0"/><path d="M340.7 341.3H171.3V512h169.4z" class="st0"/></svg>
|
Before Width: | Height: | Size: 330 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" viewBox="0 11.55 64 40.89"><path fill="#e38000" d="M18.97 21.14c0 5.293-4.248 9.585-9.487 9.585S0 26.432 0 21.14s4.245-9.585 9.485-9.585 9.485 4.293 9.485 9.585"/><path fill="#e17f03" d="M18.97 42.865c0 5.29-4.248 9.58-9.487 9.58S0 48.156 0 42.86s4.245-9.585 9.485-9.585 9.485 4.293 9.485 9.585zM41.488 21.14c0 5.293-4.25 9.585-9.49 9.585s-9.485-4.29-9.485-9.585 4.248-9.585 9.485-9.585 9.487 4.293 9.487 9.585zm0 21.726c0 5.29-4.25 9.58-9.49 9.58s-9.485-4.29-9.485-9.585 4.248-9.585 9.485-9.585 9.487 4.293 9.487 9.585zM64 21.14c0 5.293-4.245 9.585-9.485 9.585s-9.485-4.29-9.485-9.585 4.245-9.585 9.485-9.585S64 15.848 64 21.14"/><path fill="#e38000" d="M64 42.865c0 5.29-4.245 9.58-9.485 9.58s-9.485-4.29-9.485-9.585 4.245-9.585 9.485-9.585S64 37.57 64 42.86z"/></svg>
|
Before Width: | Height: | Size: 830 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.st0,.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#37beff}.st1{fill:#404f54}</style><path d="M512 364.1 256 216.3V58.4l256 147.9z" class="st0"/><path d="m0 364.1 256-147.9V58.4L0 206.3z" class="st1"/><path d="M256 0c97.8 0 177.1 79.3 177.1 177.1S353.8 354.2 256 354.2 78.9 274.9 78.9 177.1 158.2 0 256 0" style="fill-rule:evenodd;clip-rule:evenodd;fill:#ccc"/><path d="M256 0c97.8 0 177.1 79.3 177.1 177.1S353.8 354.2 256 354.2c-56.1-37.8-79.4-113.9-79.4-177S199.8 37.9 256 0" style="fill-rule:evenodd;clip-rule:evenodd;fill:#e5e5e5"/><path d="M512 206.3 256 354.2V512l256-147.9z" class="st0"/><path d="m0 206.3 256 147.9V512L0 364.1z" class="st1"/></svg>
|
Before Width: | Height: | Size: 785 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,42 +0,0 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
import React from "react";
|
||||
import FederatedDashboard from "./FederatedDashboard";
|
||||
|
||||
export default function App() {
|
||||
return <FederatedDashboard />;
|
||||
}
|
@@ -1,493 +0,0 @@
|
||||
// Federated Dashboard — React + Tailwind + Font Awesome 6 Free (Canvas Preview JS)
|
||||
// Adds custom drag preview that follows the cursor with a blue (#0EA5E9 @ 50%) shadow.
|
||||
// Features: theme toggle, favorites, search (headings hidden while typing), drag & drop reordering,
|
||||
// custom floating preview, status pill, 4-col grid, centered Federated logo.
|
||||
|
||||
import React, { useMemo, useState, useCallback, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faSun,
|
||||
faMoon,
|
||||
faArrowUpRightFromSquare,
|
||||
faMagnifyingGlass,
|
||||
faStar as faStarSolid,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// ------------------------------------
|
||||
// Brand Tokens
|
||||
// ------------------------------------
|
||||
const BRAND_TOKENS = {
|
||||
primary: "#0F172A", // dark theme background
|
||||
accent: "#D67147", // favorites + focus ring + section label color
|
||||
surface: "#1E293B",
|
||||
};
|
||||
const LIGHT_TOKENS = {
|
||||
primary: "#4D6F83", // requested light background
|
||||
surface: "#FFFFFF", // white logo tile in light
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// Data
|
||||
// ------------------------------------
|
||||
// Local asset files for app logos (place in public/assets/)
|
||||
const ASSETS = {
|
||||
panel_black: "/assets/panelblack.png",
|
||||
panel_white: "/assets/panelwhite.png",
|
||||
billing_black: "/assets/Billingblack.png",
|
||||
billing_white: "/assets/Billingwhite.png",
|
||||
jitsi: "/assets/jitsi.svg",
|
||||
element: "/assets/element.png",
|
||||
nextcloud: "/assets/nextcloud.svg",
|
||||
espocrm: "/assets/espocrm.svg",
|
||||
plane: "/assets/plane.svg",
|
||||
freescout: "/assets/freescout.png",
|
||||
roundcube: "/assets/roundcube.svg",
|
||||
bookstack: "/assets/bookstack.svg",
|
||||
baserow: "/assets/baserow.svg",
|
||||
gitea: "/assets/gitea.svg",
|
||||
powerdns: "/assets/powerdns.svg",
|
||||
wordpress: "/assets/Wordpress.png",
|
||||
vaultwarden: "/assets/Vaultwarden.svg",
|
||||
calcom_black: "/assets/calcomblack.svg",
|
||||
calcom_white: "/assets/calcomwhite.svg",
|
||||
federated_logo_light: "/assets/federated.png",
|
||||
federated_logo_dark: "/assets/federated.png",
|
||||
};
|
||||
|
||||
const APP_CATALOG = [
|
||||
{ id: "nextcloud", name: "Nextcloud", category: "Email, Files, Documents", panelManaged: true, logoUrl: ASSETS.nextcloud },
|
||||
{ id: "jitsi", name: "Jitsi (Meet)", category: "Video Chat", panelManaged: true, logoUrl: ASSETS.jitsi },
|
||||
{ id: "element", name: "Element", category: "Team Chat", panelManaged: true, logoUrl: ASSETS.element },
|
||||
{ id: "espocrm", name: "EspoCRM", category: "Customer Relationship Manager", panelManaged: true, logoUrl: ASSETS.espocrm },
|
||||
{ id: "plane", name: "Plane", category: "Project Management", panelManaged: false, logoUrl: ASSETS.plane },
|
||||
{ id: "freescout", name: "FreeScout", category: "Customer Help Desk", panelManaged: false, logoUrl: ASSETS.freescout },
|
||||
{ id: "vaultwarden", name: "Vaultwarden", category: "Passwords", panelManaged: false, logoUrl: ASSETS.vaultwarden },
|
||||
{ id: "roundcube", name: "Roundcube", category: "Web Mail", panelManaged: true, logoUrl: ASSETS.roundcube },
|
||||
{ id: "bookstack", name: "BookStack", category: "Wiki Knowledgebase", panelManaged: true, logoUrl: ASSETS.bookstack },
|
||||
{ id: "baserow", name: "Baserow", category: "Visual Databases", panelManaged: true, logoUrl: ASSETS.baserow },
|
||||
{ id: "gitea", name: "Gitea", category: "GIT Source Control", panelManaged: true, logoUrl: ASSETS.gitea },
|
||||
{ id: "wordpress", name: "WordPress", category: "Your Website", panelManaged: true, logoUrl: ASSETS.wordpress },
|
||||
{ id: "powerdns", name: "PowerDNS", category: "DNS Management", panelManaged: true, logoUrl: ASSETS.powerdns },
|
||||
{ id: "calcom", name: "Cal.com", category: "Scheduling", panelManaged: true, logoUrl: ASSETS.calcom_black },
|
||||
{ id: "panel", name: "Federated Panel", category: "Create & Manage Users", panelManaged: true, logoUrl: ASSETS.panel_black },
|
||||
{ id: "billing", name: "Billing", category: "Manage your subscription", panelManaged: false, logoUrl: ASSETS.billing_black },
|
||||
];
|
||||
|
||||
// ------------------------------------
|
||||
// Hooks
|
||||
// ------------------------------------
|
||||
function useDebouncedValue(value, delay = 150) {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(t);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// UI bits
|
||||
// ------------------------------------
|
||||
const Pill = React.memo(function Pill({ managed }) {
|
||||
const pillBg = managed
|
||||
? "bg-emerald-600 text-white ring-1 ring-emerald-300/30"
|
||||
: "bg-red-600 text-white ring-1 ring-red-300/30";
|
||||
const label = managed ? "In Panel" : "In App";
|
||||
return (
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold tracking-wide ${pillBg}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
const AppLogo = React.memo(function AppLogo({ app, theme }) {
|
||||
const bg = theme === "light" ? "bg-white" : "bg-black/20";
|
||||
const themedSrc = app.id === "panel"
|
||||
? (theme === "light" ? ASSETS.panel_black : ASSETS.panel_white)
|
||||
: app.id === "billing"
|
||||
? (theme === "light" ? ASSETS.billing_black : ASSETS.billing_white)
|
||||
: app.id === "calcom"
|
||||
? (theme === "light" ? ASSETS.calcom_black : ASSETS.calcom_white)
|
||||
: app.logoUrl;
|
||||
return (
|
||||
<div className={`flex h-20 items-center justify-center rounded-xl ${bg}`} data-testid={`logo-${app.id}`}>
|
||||
{themedSrc ? (
|
||||
<img
|
||||
src={themedSrc}
|
||||
className={`max-h-10 max-w-[60%] object-contain ${app.id === "vaultwarden" && theme === "dark" ? "invert brightness-200" : ""}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
referrerPolicy="no-referrer"
|
||||
fetchPriority="low"
|
||||
width={120}
|
||||
height={40}
|
||||
crossOrigin="anonymous"
|
||||
alt={`${app.name} logo`}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// Drag-and-drop capable card
|
||||
const AppCard = React.memo(function AppCard({
|
||||
app,
|
||||
favorite = false,
|
||||
theme = "light",
|
||||
onToggleFavorite,
|
||||
dnd,
|
||||
}) {
|
||||
const draggable = !!(dnd && dnd.enabled);
|
||||
const isDragging = draggable && dnd && dnd.draggingId === app.id;
|
||||
return (
|
||||
<div
|
||||
className={`group relative rounded-2xl border border-white/10 bg-white/5 p-4 transform-gpu transition duration-200 ease-out cursor-default ${isDragging ? "opacity-0" : "hover:scale-[1.02] hover:shadow-[0_12px_32px_rgba(214,113,71,0.5)]"}`}
|
||||
data-brand={app.brandUrl || undefined}
|
||||
draggable={draggable}
|
||||
onDragStart={draggable ? dnd.onDragStart(app.id, dnd.list) : undefined}
|
||||
onDragOver={draggable ? dnd.onDragOver(app.id, dnd.list) : undefined}
|
||||
onDrop={draggable ? dnd.onDrop(app.id, dnd.list) : undefined}
|
||||
onDrag={draggable ? dnd.onDrag : undefined}
|
||||
onDragEnd={draggable ? dnd.onDragEnd : undefined}
|
||||
aria-grabbed={draggable ? undefined : false}
|
||||
>
|
||||
{/* Brand link (top-left) */}
|
||||
<div className="absolute left-2 top-2">
|
||||
{app.brandUrl && (
|
||||
<a
|
||||
href={app.brandUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`inline-flex h-5 w-5 items-center justify-center rounded-full text-[10px] text-white/80 ring-1 ring-white/20 transition ${theme === "light" ? "bg-[#0EA5E9] hover:bg-[#0EA5E9]/90" : "bg-white/10 hover:bg-white/20"}`}
|
||||
title="Brand assets / logo policy"
|
||||
>
|
||||
i
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status pill (bottom-right) */}
|
||||
<div className="absolute right-2 bottom-2">
|
||||
<Pill managed={app.panelManaged} />
|
||||
</div>
|
||||
|
||||
{/* Favorite toggle */}
|
||||
<div className="absolute right-2 top-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggleFavorite && onToggleFavorite(app.id)}
|
||||
aria-pressed={favorite}
|
||||
aria-label={favorite ? "Remove from Favorites" : "Add to Favorites"}
|
||||
className="rounded-full p-1 transition hover:scale-110 focus:outline-none focus:ring-2 focus:ring-[var(--brand-accent)] cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon icon={favorite ? faStarSolid : faStarRegular} className={favorite ? "text-[var(--brand-accent)]" : "text-white/70"} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title and purpose ABOVE the logo */}
|
||||
<div className="mt-6 mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{app.category}</div>
|
||||
<h3 className="text-xs text-white/60 leading-tight">{app.name}</h3>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="mt-1 text-xs opacity-0 group-hover:opacity-100" />
|
||||
</div>
|
||||
|
||||
{/* Logo area */}
|
||||
<AppLogo app={app} theme={theme} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------
|
||||
// Page
|
||||
// ------------------------------------
|
||||
export default function FederatedDashboard() {
|
||||
const [theme, setTheme] = useState("light");
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// Default favorite: Panel only
|
||||
const [favorites, setFavorites] = useState(new Set(["panel"]));
|
||||
|
||||
// Orders for within-section reordering
|
||||
const initialFav = new Set(["panel"]);
|
||||
const [favoritesOrder, setFavoritesOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => initialFav.has(id)));
|
||||
const [othersOrder, setOthersOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => !initialFav.has(id)));
|
||||
|
||||
// Drag state
|
||||
const [dragging, setDragging] = useState(null); // { id, from }
|
||||
const [dragPos, setDragPos] = useState(null); // { x, y }
|
||||
const [dragMeta, setDragMeta] = useState(null); // { offsetX, offsetY, width, height, app }
|
||||
|
||||
// CSS vars
|
||||
const rootStyle = useMemo(() => ({
|
||||
"--brand-primary": theme === "light" ? LIGHT_TOKENS.primary : BRAND_TOKENS.primary,
|
||||
"--brand-accent": BRAND_TOKENS.accent,
|
||||
"--brand-surface": theme === "light" ? LIGHT_TOKENS.surface : BRAND_TOKENS.surface,
|
||||
}), [theme]);
|
||||
|
||||
// Index for search
|
||||
const APP_INDEX = useMemo(
|
||||
() => APP_CATALOG.map((a) => ({ ...a, _hay: `${a.name} ${a.category} ${a.id}`.toLowerCase() })),
|
||||
[]
|
||||
);
|
||||
const APP_MAP = useMemo(() => {
|
||||
const m = new Map();
|
||||
APP_INDEX.forEach(a => m.set(a.id, a));
|
||||
return m;
|
||||
}, [APP_INDEX]);
|
||||
|
||||
const debouncedQuery = useDebouncedValue(query, 150);
|
||||
const tokens = useMemo(
|
||||
() => debouncedQuery.toLowerCase().trim().split(/\s+/).filter(Boolean),
|
||||
[debouncedQuery]
|
||||
);
|
||||
const isSearching = tokens.length > 0;
|
||||
|
||||
const matches = useCallback((a) => {
|
||||
if (tokens.length === 0) return true;
|
||||
const h = a._hay;
|
||||
for (const t of tokens) if (!h.includes(t)) return false;
|
||||
return true;
|
||||
}, [tokens]);
|
||||
|
||||
// Lists in display order, then filtered
|
||||
const favAll = useMemo(() => favoritesOrder.map(id => APP_MAP.get(id)).filter(Boolean), [favoritesOrder, APP_MAP]);
|
||||
const otherAll = useMemo(() => othersOrder.map(id => APP_MAP.get(id)).filter(Boolean), [othersOrder, APP_MAP]);
|
||||
const favList = useMemo(() => favAll.filter(matches), [favAll, matches]);
|
||||
const otherList = useMemo(() => otherAll.filter(matches), [otherAll, matches]);
|
||||
|
||||
// Favorite toggle + sync orders
|
||||
const toggleFavorite = useCallback((id) => {
|
||||
setFavorites(prev => {
|
||||
const next = new Set(prev);
|
||||
const wasFav = next.has(id);
|
||||
if (wasFav) next.delete(id); else next.add(id);
|
||||
setFavoritesOrder(o => (wasFav ? o.filter(x => x !== id) : o.includes(id) ? o : [...o, id]));
|
||||
setOthersOrder(o => (wasFav ? (o.includes(id) ? o : [...o, id]) : o.filter(x => x !== id)));
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// DnD handlers (within-section only) + floating preview tracking
|
||||
const onDragStart = useCallback((id, list) => (e) => {
|
||||
setDragging({ id, from: list });
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left;
|
||||
const offsetY = e.clientY - rect.top;
|
||||
setDragMeta({ offsetX, offsetY, width: rect.width, height: rect.height, app: APP_MAP.get(id) });
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
// hide the native ghost image so only our custom preview shows
|
||||
const img = new Image();
|
||||
img.src = "data:image/svg+xml;base64," + btoa('<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>');
|
||||
e.dataTransfer.setDragImage(img, 0, 0);
|
||||
}, [APP_MAP]);
|
||||
|
||||
const onDragOver = useCallback((overId, list) => (e) => {
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
}, [dragging]);
|
||||
|
||||
// keep preview following cursor even when not over a specific card
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const handle = (e) => {
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
window.addEventListener("dragover", handle);
|
||||
return () => window.removeEventListener("dragover", handle);
|
||||
}, [dragging]);
|
||||
|
||||
const onDrag = useCallback((e) => {
|
||||
if (!dragging) return;
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
}, [dragging]);
|
||||
|
||||
const onDrop = useCallback((overId, list) => (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
if (dragging.id === overId) { setDragging(null); setDragMeta(null); setDragPos(null); return; }
|
||||
const arr = list === "fav" ? favoritesOrder.slice() : othersOrder.slice();
|
||||
const fromIdx = arr.indexOf(dragging.id);
|
||||
const toIdx = arr.indexOf(overId);
|
||||
if (fromIdx < 0 || toIdx < 0) { setDragging(null); setDragMeta(null); setDragPos(null); return; }
|
||||
arr.splice(fromIdx, 1);
|
||||
arr.splice(toIdx, 0, dragging.id);
|
||||
if (list === "fav") setFavoritesOrder(arr); else setOthersOrder(arr);
|
||||
setDragging(null); setDragMeta(null); setDragPos(null);
|
||||
}, [dragging, favoritesOrder, othersOrder]);
|
||||
|
||||
const onDropToEnd = useCallback((list) => (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
const arr = list === "fav" ? favoritesOrder.slice() : othersOrder.slice();
|
||||
const fromIdx = arr.indexOf(dragging.id);
|
||||
if (fromIdx < 0) { setDragging(null); setDragMeta(null); setDragPos(null); return; }
|
||||
arr.splice(fromIdx, 1);
|
||||
arr.push(dragging.id);
|
||||
if (list === "fav") setFavoritesOrder(arr); else setOthersOrder(arr);
|
||||
setDragging(null); setDragMeta(null); setDragPos(null);
|
||||
}, [dragging, favoritesOrder, othersOrder]);
|
||||
|
||||
const onDragEnd = useCallback(() => { setDragging(null); setDragMeta(null); setDragPos(null); }, []);
|
||||
|
||||
const clearSearch = useCallback(() => setQuery(""), []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--brand-primary)] text-white" style={rootStyle}>
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 border-b border-white/10 bg-[var(--brand-primary)]/95 backdrop-blur">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-3 items-center px-4 py-3">
|
||||
<div className="justify-self-start"></div>
|
||||
<div className="flex flex-col items-center gap-1 leading-none text-center justify-self-center">
|
||||
<img
|
||||
src={theme === "light" ? ASSETS.federated_logo_light : ASSETS.federated_logo_dark}
|
||||
alt="Federated Computer"
|
||||
className="h-6 sm:h-7 w-auto object-contain"
|
||||
/>
|
||||
<h1 className="mt-1 text-base sm:text-xl font-semibold leading-none tracking-tight">Dashboard</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 justify-self-end">
|
||||
<button
|
||||
onClick={() => setTheme("light")}
|
||||
aria-pressed={theme === "light"}
|
||||
className={`rounded-lg border border-white/10 bg-white/5 px-3 py-2 ${theme === "light" ? "ring-2 ring-[var(--brand-accent)]" : ""}`}
|
||||
aria-label="Light mode"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSun} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme("dark")}
|
||||
aria-pressed={theme === "dark"}
|
||||
className={`rounded-lg border border-white/10 bg-white/5 px-3 py-2 ${theme === "dark" ? "ring-2 ring-[var(--brand-accent)]" : ""}`}
|
||||
aria-label="Dark mode"
|
||||
>
|
||||
<FontAwesomeIcon icon={faMoon} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Search */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-6">
|
||||
<div className="relative">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-white/50" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Escape") clearSearch(); }}
|
||||
placeholder="Search apps…"
|
||||
className="w-full rounded-xl border border-white/10 bg-white/5 py-3 pl-10 pr-10 outline-none placeholder:text-white/50 focus:ring-2 focus:ring-[var(--brand-accent)]"
|
||||
aria-label="Search apps"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSearch}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-md p-1 text-white/70 hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-[var(--brand-accent)]"
|
||||
aria-label="Clear search"
|
||||
title="Clear"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{isSearching ? (
|
||||
<section className="mx-auto max-w-7xl px-4 py-6">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{[...favList, ...otherList].map((app) => (
|
||||
<AppCard key={`search-${app.id}`} app={app} favorite={favorites.has(app.id)} theme={theme} onToggleFavorite={toggleFavorite} />
|
||||
))}
|
||||
{[...favList, ...otherList].length === 0 && (
|
||||
<div className="col-span-full rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/70">
|
||||
<span>No apps found for “{debouncedQuery}”.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
{/* Favorites */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-6" onDragOver={(e) => dragging && dragging.from === "fav" && e.preventDefault()} onDrop={onDropToEnd("fav")}>
|
||||
<h2 className={`mb-3 text-sm font-semibold uppercase tracking-wider ${theme === "light" ? "text-white" : "text-[var(--brand-accent)]"}`}>Favorites</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{favList.map((app) => (
|
||||
<AppCard
|
||||
key={`fav-${app.id}`}
|
||||
app={app}
|
||||
favorite={true}
|
||||
theme={theme}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
dnd={{ enabled: true, list: "fav", onDragStart, onDragOver, onDrop, onDrag, draggingId: dragging && dragging.id, onDragEnd }}
|
||||
/>
|
||||
))}
|
||||
{favList.length === 0 && (
|
||||
<div className="col-span-full rounded-xl border border-white/10 bg-white/5 p-4 text-sm text-white/70">
|
||||
<span>Click the star on any app to favorite it.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Your Apps (non-favorites) */}
|
||||
<section className="mx-auto max-w-7xl px-4 py-6" onDragOver={(e) => dragging && dragging.from === "other" && e.preventDefault()} onDrop={onDropToEnd("other") }>
|
||||
<h2 className={`mb-3 text-sm font-semibold uppercase tracking-wider ${theme === "light" ? "text-white" : "text-[var(--brand-accent)]"}`}>Your Apps</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{otherList.map((app) => (
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
favorite={false}
|
||||
theme={theme}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
dnd={{ enabled: true, list: "other", onDragStart, onDragOver, onDrop, onDrag, draggingId: dragging && dragging.id, onDragEnd }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Floating drag preview */}
|
||||
{dragging && dragMeta && dragPos && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-[1000]"
|
||||
style={{ left: dragPos.x - dragMeta.offsetX, top: dragPos.y - dragMeta.offsetY, width: dragMeta.width }}
|
||||
>
|
||||
<div className="rounded-2xl border border-white/10 bg-white/5 p-4 transform-gpu shadow-[0_12px_32px_rgba(14,165,233,0.5)]">
|
||||
<div className="mt-2 mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{dragMeta.app.category}</div>
|
||||
<h3 className="text-xs text-white/60 leading-tight">{dragMeta.app.name}</h3>
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="mt-1 text-xs opacity-60" />
|
||||
</div>
|
||||
<AppLogo app={dragMeta.app} theme={theme} />
|
||||
<div className="absolute right-2 bottom-2">
|
||||
<Pill managed={dragMeta.app.panelManaged} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mx-auto max-w-7xl px-4 pb-12 pt-4 text-xs text-white/60">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-white/10 pt-4">
|
||||
<div>© {new Date().getFullYear()} Federated Computer · Dashboard</div>
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<span className="h-2 w-2 rounded-full bg-[var(--brand-accent)]" aria-hidden></span>
|
||||
<span>Light/Dark · Favorites toggle · Search (debounced) · Drag to reorder</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Dev-only sanity checks were removed to maximize Canvas compatibility.
|
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,5 +0,0 @@
|
||||
/* Tailwind v4: single import */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* your app baseline */
|
||||
html, body, #root { height: 100%; }
|
@@ -1,10 +0,0 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
1
federated-dashboard/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import FederatedDashboard from "./FederatedDashboard";
|
||||
|
||||
export default function App() {
|
||||
|
@@ -1,11 +1,9 @@
|
||||
// Final Federated Dashboard — React + Tailwind + Font Awesome 6 Free (Type-safe)
|
||||
// Features: theme toggle, favorites, debounced search, drag & drop reordering within sections,
|
||||
// custom floating drag preview with blue (#0EA5E9 @ 50%) shadow, status pill, 4-col grid,
|
||||
// centered Federated logo, and a top-left "Documentation" link.
|
||||
|
||||
import { useMemo, useState, useCallback, useEffect, memo } from "react";
|
||||
import type React from "react";
|
||||
// Federated Dashboard — React + Tailwind + Font Awesome 6 Free (Canvas Preview JS)
|
||||
// Adds custom drag preview that follows the cursor with a blue (#0EA5E9 @ 50%) shadow.
|
||||
// Features: theme toggle, favorites, search (headings hidden while typing), drag & drop reordering,
|
||||
// custom floating preview, status pill, 4-col grid, centered Federated logo.
|
||||
|
||||
import React, { useMemo, useState, useCallback, useEffect } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
@@ -15,7 +13,6 @@ import {
|
||||
faMagnifyingGlass,
|
||||
faStar as faStarSolid,
|
||||
faXmark,
|
||||
faComments, // icon for Documentation button
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// ------------------------------------
|
||||
@@ -23,7 +20,7 @@ import {
|
||||
// ------------------------------------
|
||||
const BRAND_TOKENS = {
|
||||
primary: "#0F172A", // dark theme background
|
||||
accent: "#D67147", // favorites + focus ring + section label color
|
||||
accent: "#D67147", // favorites + focus ring + section label color
|
||||
surface: "#1E293B",
|
||||
};
|
||||
const LIGHT_TOKENS = {
|
||||
@@ -32,38 +29,9 @@ const LIGHT_TOKENS = {
|
||||
};
|
||||
|
||||
// ------------------------------------
|
||||
// Types
|
||||
// ------------------------------------
|
||||
export type Theme = "light" | "dark";
|
||||
export type DragList = "fav" | "other";
|
||||
|
||||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string; // e.g. "Email, Files, Documents"
|
||||
panelManaged: boolean;
|
||||
logoUrl?: string; // local or remote logo
|
||||
brandUrl?: string; // optional brand page
|
||||
}
|
||||
|
||||
interface DragState { id: string; from: DragList }
|
||||
interface DragMeta { offsetX: number; offsetY: number; width: number; height: number; app: App }
|
||||
interface DragPos { x: number; y: number }
|
||||
|
||||
interface DndProps {
|
||||
enabled: boolean;
|
||||
list: DragList;
|
||||
onDragStart: (id: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDragOver: (_overId: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDrop: (overId: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDrag: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
onDragEnd: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
draggingId?: string | null;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Data (local assets in public/assets)
|
||||
// Data
|
||||
// ------------------------------------
|
||||
// Local asset files for app logos (place in public/assets/)
|
||||
const ASSETS = {
|
||||
panel_black: "/assets/panelblack.png",
|
||||
panel_white: "/assets/panelwhite.png",
|
||||
@@ -88,7 +56,7 @@ const ASSETS = {
|
||||
federated_logo_dark: "/assets/federated.png",
|
||||
};
|
||||
|
||||
const APP_CATALOG: App[] = [
|
||||
const APP_CATALOG = [
|
||||
{ id: "nextcloud", name: "Nextcloud", category: "Email, Files, Documents", panelManaged: true, logoUrl: ASSETS.nextcloud },
|
||||
{ id: "jitsi", name: "Jitsi (Meet)", category: "Video Chat", panelManaged: true, logoUrl: ASSETS.jitsi },
|
||||
{ id: "element", name: "Element", category: "Team Chat", panelManaged: true, logoUrl: ASSETS.element },
|
||||
@@ -108,10 +76,10 @@ const APP_CATALOG: App[] = [
|
||||
];
|
||||
|
||||
// ------------------------------------
|
||||
// Utilities / Hooks
|
||||
// Hooks
|
||||
// ------------------------------------
|
||||
function useDebouncedValue<T>(value: T, delay = 150): T {
|
||||
const [debounced, setDebounced] = useState<T>(value);
|
||||
function useDebouncedValue(value, delay = 150) {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setDebounced(value), delay);
|
||||
return () => clearTimeout(t);
|
||||
@@ -122,7 +90,7 @@ function useDebouncedValue<T>(value: T, delay = 150): T {
|
||||
// ------------------------------------
|
||||
// UI bits
|
||||
// ------------------------------------
|
||||
const Pill = memo(function Pill({ managed }: { managed: boolean }) {
|
||||
const Pill = React.memo(function Pill({ managed }) {
|
||||
const pillBg = managed
|
||||
? "bg-emerald-600 text-white ring-1 ring-emerald-300/30"
|
||||
: "bg-red-600 text-white ring-1 ring-red-300/30";
|
||||
@@ -134,7 +102,7 @@ const Pill = memo(function Pill({ managed }: { managed: boolean }) {
|
||||
);
|
||||
});
|
||||
|
||||
const AppLogo = memo(function AppLogo({ app, theme }: { app: App; theme: Theme }) {
|
||||
const AppLogo = React.memo(function AppLogo({ app, theme }) {
|
||||
const bg = theme === "light" ? "bg-white" : "bg-black/20";
|
||||
const themedSrc = app.id === "panel"
|
||||
? (theme === "light" ? ASSETS.panel_black : ASSETS.panel_white)
|
||||
@@ -163,18 +131,13 @@ const AppLogo = memo(function AppLogo({ app, theme }: { app: App; theme: Theme }
|
||||
);
|
||||
});
|
||||
|
||||
const AppCard = memo(function AppCard({
|
||||
// Drag-and-drop capable card
|
||||
const AppCard = React.memo(function AppCard({
|
||||
app,
|
||||
favorite = false,
|
||||
theme = "light",
|
||||
onToggleFavorite,
|
||||
dnd,
|
||||
}: {
|
||||
app: App;
|
||||
favorite?: boolean;
|
||||
theme?: Theme;
|
||||
onToggleFavorite?: (id: string) => void;
|
||||
dnd?: DndProps;
|
||||
}) {
|
||||
const draggable = !!(dnd && dnd.enabled);
|
||||
const isDragging = draggable && dnd && dnd.draggingId === app.id;
|
||||
@@ -183,11 +146,11 @@ const AppCard = memo(function AppCard({
|
||||
className={`group relative rounded-2xl border border-white/10 bg-white/5 p-4 transform-gpu transition duration-200 ease-out cursor-default ${isDragging ? "opacity-0" : "hover:scale-[1.02] hover:shadow-[0_12px_32px_rgba(214,113,71,0.5)]"}`}
|
||||
data-brand={app.brandUrl || undefined}
|
||||
draggable={draggable}
|
||||
onDragStart={draggable ? dnd!.onDragStart(app.id, dnd!.list) : undefined}
|
||||
onDragOver={draggable ? dnd!.onDragOver(app.id, dnd!.list) : undefined}
|
||||
onDrop={draggable ? dnd!.onDrop(app.id, dnd!.list) : undefined}
|
||||
onDrag={draggable ? dnd!.onDrag : undefined}
|
||||
onDragEnd={draggable ? dnd!.onDragEnd : undefined}
|
||||
onDragStart={draggable ? dnd.onDragStart(app.id, dnd.list) : undefined}
|
||||
onDragOver={draggable ? dnd.onDragOver(app.id, dnd.list) : undefined}
|
||||
onDrop={draggable ? dnd.onDrop(app.id, dnd.list) : undefined}
|
||||
onDrag={draggable ? dnd.onDrag : undefined}
|
||||
onDragEnd={draggable ? dnd.onDragEnd : undefined}
|
||||
aria-grabbed={draggable ? undefined : false}
|
||||
>
|
||||
{/* Brand link (top-left) */}
|
||||
@@ -242,32 +205,28 @@ const AppCard = memo(function AppCard({
|
||||
// Page
|
||||
// ------------------------------------
|
||||
export default function FederatedDashboard() {
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [theme, setTheme] = useState("light");
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
// Default favorite: Panel only
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set(["panel"]));
|
||||
const [favorites, setFavorites] = useState(new Set(["panel"]));
|
||||
|
||||
// Orders for within-section reordering
|
||||
const initialFav = new Set(["panel"]);
|
||||
const [favoritesOrder, setFavoritesOrder] = useState<string[]>(
|
||||
APP_CATALOG.map(a => a.id).filter(id => initialFav.has(id))
|
||||
);
|
||||
const [othersOrder, setOthersOrder] = useState<string[]>(
|
||||
APP_CATALOG.map(a => a.id).filter(id => !initialFav.has(id))
|
||||
);
|
||||
const [favoritesOrder, setFavoritesOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => initialFav.has(id)));
|
||||
const [othersOrder, setOthersOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => !initialFav.has(id)));
|
||||
|
||||
// Drag state
|
||||
const [dragging, setDragging] = useState<DragState | null>(null);
|
||||
const [dragPos, setDragPos] = useState<DragPos | null>(null);
|
||||
const [dragMeta, setDragMeta] = useState<DragMeta | null>(null);
|
||||
const [dragging, setDragging] = useState(null); // { id, from }
|
||||
const [dragPos, setDragPos] = useState(null); // { x, y }
|
||||
const [dragMeta, setDragMeta] = useState(null); // { offsetX, offsetY, width, height, app }
|
||||
|
||||
// CSS vars
|
||||
const rootStyle = useMemo(() => ({
|
||||
"--brand-primary": theme === "light" ? LIGHT_TOKENS.primary : BRAND_TOKENS.primary,
|
||||
"--brand-accent": BRAND_TOKENS.accent,
|
||||
"--brand-surface": theme === "light" ? LIGHT_TOKENS.surface : BRAND_TOKENS.surface,
|
||||
}) as React.CSSProperties, [theme]);
|
||||
}), [theme]);
|
||||
|
||||
// Index for search
|
||||
const APP_INDEX = useMemo(
|
||||
@@ -275,7 +234,7 @@ export default function FederatedDashboard() {
|
||||
[]
|
||||
);
|
||||
const APP_MAP = useMemo(() => {
|
||||
const m = new Map<string, App & { _hay: string }>();
|
||||
const m = new Map();
|
||||
APP_INDEX.forEach(a => m.set(a.id, a));
|
||||
return m;
|
||||
}, [APP_INDEX]);
|
||||
@@ -287,7 +246,7 @@ export default function FederatedDashboard() {
|
||||
);
|
||||
const isSearching = tokens.length > 0;
|
||||
|
||||
const matches = useCallback((a: App & { _hay: string }) => {
|
||||
const matches = useCallback((a) => {
|
||||
if (tokens.length === 0) return true;
|
||||
const h = a._hay;
|
||||
for (const t of tokens) if (!h.includes(t)) return false;
|
||||
@@ -295,13 +254,13 @@ export default function FederatedDashboard() {
|
||||
}, [tokens]);
|
||||
|
||||
// Lists in display order, then filtered
|
||||
const favAll = useMemo(() => favoritesOrder.map(id => APP_MAP.get(id)!).filter(Boolean), [favoritesOrder, APP_MAP]);
|
||||
const otherAll = useMemo(() => othersOrder.map(id => APP_MAP.get(id)!).filter(Boolean), [othersOrder, APP_MAP]);
|
||||
const favAll = useMemo(() => favoritesOrder.map(id => APP_MAP.get(id)).filter(Boolean), [favoritesOrder, APP_MAP]);
|
||||
const otherAll = useMemo(() => othersOrder.map(id => APP_MAP.get(id)).filter(Boolean), [othersOrder, APP_MAP]);
|
||||
const favList = useMemo(() => favAll.filter(matches), [favAll, matches]);
|
||||
const otherList = useMemo(() => otherAll.filter(matches), [otherAll, matches]);
|
||||
|
||||
// Favorite toggle + sync orders
|
||||
const toggleFavorite = useCallback((id: string) => {
|
||||
const toggleFavorite = useCallback((id) => {
|
||||
setFavorites(prev => {
|
||||
const next = new Set(prev);
|
||||
const wasFav = next.has(id);
|
||||
@@ -313,12 +272,12 @@ export default function FederatedDashboard() {
|
||||
}, []);
|
||||
|
||||
// DnD handlers (within-section only) + floating preview tracking
|
||||
const onDragStart = useCallback((id: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const onDragStart = useCallback((id, list) => (e) => {
|
||||
setDragging({ id, from: list });
|
||||
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const offsetX = e.clientX - rect.left;
|
||||
const offsetY = e.clientY - rect.top;
|
||||
setDragMeta({ offsetX, offsetY, width: rect.width, height: rect.height, app: APP_MAP.get(id)! });
|
||||
setDragMeta({ offsetX, offsetY, width: rect.width, height: rect.height, app: APP_MAP.get(id) });
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
// hide the native ghost image so only our custom preview shows
|
||||
@@ -327,7 +286,7 @@ export default function FederatedDashboard() {
|
||||
e.dataTransfer.setDragImage(img, 0, 0);
|
||||
}, [APP_MAP]);
|
||||
|
||||
const onDragOver = useCallback((_overId: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const onDragOver = useCallback((overId, list) => (e) => {
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
@@ -336,17 +295,19 @@ export default function FederatedDashboard() {
|
||||
// keep preview following cursor even when not over a specific card
|
||||
useEffect(() => {
|
||||
if (!dragging) return;
|
||||
const handle = (e: DragEvent) => { setDragPos({ x: e.clientX, y: e.clientY }); };
|
||||
window.addEventListener("dragover", handle as unknown as EventListener);
|
||||
return () => window.removeEventListener("dragover", handle as unknown as EventListener);
|
||||
const handle = (e) => {
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
};
|
||||
window.addEventListener("dragover", handle);
|
||||
return () => window.removeEventListener("dragover", handle);
|
||||
}, [dragging]);
|
||||
|
||||
const onDrag = useCallback((e: React.DragEvent<HTMLDivElement>) => {
|
||||
const onDrag = useCallback((e) => {
|
||||
if (!dragging) return;
|
||||
setDragPos({ x: e.clientX, y: e.clientY });
|
||||
}, [dragging]);
|
||||
|
||||
const onDrop = useCallback((overId: string, list: DragList) => (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const onDrop = useCallback((overId, list) => (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
if (dragging.id === overId) { setDragging(null); setDragMeta(null); setDragPos(null); return; }
|
||||
@@ -360,7 +321,7 @@ export default function FederatedDashboard() {
|
||||
setDragging(null); setDragMeta(null); setDragPos(null);
|
||||
}, [dragging, favoritesOrder, othersOrder]);
|
||||
|
||||
const onDropToEnd = useCallback((list: DragList) => (e: React.DragEvent<HTMLDivElement>) => {
|
||||
const onDropToEnd = useCallback((list) => (e) => {
|
||||
e.preventDefault();
|
||||
if (!dragging || dragging.from !== list) return;
|
||||
const arr = list === "fav" ? favoritesOrder.slice() : othersOrder.slice();
|
||||
@@ -381,20 +342,7 @@ export default function FederatedDashboard() {
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 border-b border-white/10 bg-[var(--brand-primary)]/95 backdrop-blur">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-3 items-center px-4 py-3">
|
||||
<div className="justify-self-start">
|
||||
<a
|
||||
href="https://discourse.federated.computer"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-[var(--brand-accent)]"
|
||||
aria-label="Open Federated Documentation (new tab)"
|
||||
title="Documentation"
|
||||
>
|
||||
<FontAwesomeIcon icon={faComments} />
|
||||
<span>Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="justify-self-start"></div>
|
||||
<div className="flex flex-col items-center gap-1 leading-none text-center justify-self-center">
|
||||
<img
|
||||
src={theme === "light" ? ASSETS.federated_logo_light : ASSETS.federated_logo_dark}
|
||||
@@ -403,7 +351,6 @@ export default function FederatedDashboard() {
|
||||
/>
|
||||
<h1 className="mt-1 text-base sm:text-xl font-semibold leading-none tracking-tight">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 justify-self-end">
|
||||
<button
|
||||
onClick={() => setTheme("light")}
|
||||
@@ -467,11 +414,7 @@ export default function FederatedDashboard() {
|
||||
) : (
|
||||
<>
|
||||
{/* Favorites */}
|
||||
<section
|
||||
className="mx-auto max-w-7xl px-4 py-6"
|
||||
onDragOver={(e) => dragging && dragging.from === "fav" && e.preventDefault()}
|
||||
onDrop={onDropToEnd("fav")}
|
||||
>
|
||||
<section className="mx-auto max-w-7xl px-4 py-6" onDragOver={(e) => dragging && dragging.from === "fav" && e.preventDefault()} onDrop={onDropToEnd("fav")}>
|
||||
<h2 className={`mb-3 text-sm font-semibold uppercase tracking-wider ${theme === "light" ? "text-white" : "text-[var(--brand-accent)]"}`}>Favorites</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{favList.map((app) => (
|
||||
@@ -493,11 +436,7 @@ export default function FederatedDashboard() {
|
||||
</section>
|
||||
|
||||
{/* Your Apps (non-favorites) */}
|
||||
<section
|
||||
className="mx-auto max-w-7xl px-4 py-6"
|
||||
onDragOver={(e) => dragging && dragging.from === "other" && e.preventDefault()}
|
||||
onDrop={onDropToEnd("other")}
|
||||
>
|
||||
<section className="mx-auto max-w-7xl px-4 py-6" onDragOver={(e) => dragging && dragging.from === "other" && e.preventDefault()} onDrop={onDropToEnd("other") }>
|
||||
<h2 className={`mb-3 text-sm font-semibold uppercase tracking-wider ${theme === "light" ? "text-white" : "text-[var(--brand-accent)]"}`}>Your Apps</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{otherList.map((app) => (
|
||||
@@ -551,18 +490,4 @@ export default function FederatedDashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
// Dev-only sanity checks (Node-agnostic)
|
||||
const __DEV__ = (typeof globalThis !== "undefined" && (globalThis as any)?.process?.env?.NODE_ENV !== "production");
|
||||
if (__DEV__) {
|
||||
try {
|
||||
const ids = new Set<string>();
|
||||
for (const a of APP_CATALOG) {
|
||||
if (!a.id || !a.name || !a.category) throw new Error("App missing id/name/category");
|
||||
if (ids.has(a.id)) throw new Error("Duplicate app id: " + a.id);
|
||||
ids.add(a.id);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[Dashboard] sanity check:", e);
|
||||
}
|
||||
}
|
||||
// NOTE: Dev-only sanity checks were removed to maximize Canvas compatibility.
|
||||
|
47
ubmodule status
Normal file
@@ -0,0 +1,47 @@
|
||||
[33mcommit 55fee32589b3202da09235af49206969a1529ff0[m[33m ([m[1;36mHEAD[m[33m -> [m[1;32mmain[m[33m, [m[1;31morigin/main[m[33m, [m[1;31morigin/HEAD[m[33m)[m
|
||||
Author: Lance <lance@federated.computer>
|
||||
Date: Wed Sep 3 15:55:42 2025 -0600
|
||||
|
||||
Fix: replace accidental submodule with regular files
|
||||
|
||||
federated-dashboard | 1 [31m-[m
|
||||
federated-dashboard/.gitignore | 27 [32m+[m
|
||||
federated-dashboard/README.md | 69 [32m+[m
|
||||
federated-dashboard/eslint.config.js | 23 [32m+[m
|
||||
federated-dashboard/index.html | 13 [32m+[m
|
||||
federated-dashboard/package-lock.json | 4242 [32m++++++++++++++++++++[m
|
||||
federated-dashboard/package.json | 38 [32m+[m
|
||||
federated-dashboard/postcss.config.cjs | 6 [32m+[m
|
||||
federated-dashboard/public/assets/Billingblack.png | Bin [31m0[m -> [32m9973[m bytes
|
||||
federated-dashboard/public/assets/Billingwhite.png | Bin [31m0[m -> [32m16375[m bytes
|
||||
federated-dashboard/public/assets/Vaultwarden.svg | 58 [32m+[m
|
||||
federated-dashboard/public/assets/Wordpress.png | Bin [31m0[m -> [32m34705[m bytes
|
||||
federated-dashboard/public/assets/baserow.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/bookstack.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/calcomblack.svg | 9 [32m+[m
|
||||
federated-dashboard/public/assets/calcomwhite.svg | 9 [32m+[m
|
||||
federated-dashboard/public/assets/element.png | Bin [31m0[m -> [32m37455[m bytes
|
||||
federated-dashboard/public/assets/espocrm.svg | 14 [32m+[m
|
||||
federated-dashboard/public/assets/federated.png | Bin [31m0[m -> [32m7800[m bytes
|
||||
federated-dashboard/public/assets/freescout.png | Bin [31m0[m -> [32m20709[m bytes
|
||||
federated-dashboard/public/assets/gitea.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/jitsi.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/nextcloud.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/panelblack.png | Bin [31m0[m -> [32m6298[m bytes
|
||||
federated-dashboard/public/assets/panelwhite.png | Bin [31m0[m -> [32m13988[m bytes
|
||||
federated-dashboard/public/assets/plane.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/powerdns.svg | 1 [32m+[m
|
||||
federated-dashboard/public/assets/roundcube.svg | 1 [32m+[m
|
||||
federated-dashboard/public/vite.svg | 1 [32m+[m
|
||||
federated-dashboard/src/App.css | 42 [32m+[m
|
||||
federated-dashboard/src/App.tsx | 6 [32m+[m
|
||||
federated-dashboard/src/FederatedDashboard.tsx | 493 [32m+++[m
|
||||
federated-dashboard/src/assets/react.svg | 1 [32m+[m
|
||||
federated-dashboard/src/index.css | 5 [32m+[m
|
||||
federated-dashboard/src/main.tsx | 10 [32m+[m
|
||||
federated-dashboard/src/vite-env.d.ts | 1 [32m+[m
|
||||
federated-dashboard/tsconfig.app.json | 27 [32m+[m
|
||||
federated-dashboard/tsconfig.json | 7 [32m+[m
|
||||
federated-dashboard/tsconfig.node.json | 25 [32m+[m
|
||||
federated-dashboard/vite.config.ts | 7 [32m+[m
|
||||
40 files changed, 5141 insertions(+), 1 deletion(-)
|