Initial commit: Federated Dashboard
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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
|
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
])
|
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!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
package-lock.json
generated
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Tailwind v4 PostCSS wiring (array form avoids loader quirks)
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("@tailwindcss/postcss"),
|
||||
],
|
||||
};
|
BIN
public/assets/Billingblack.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
public/assets/Billingwhite.png
Normal file
After Width: | Height: | Size: 16 KiB |
58
public/assets/Vaultwarden.svg
Normal file
@@ -0,0 +1,58 @@
|
||||
<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>
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/assets/Wordpress.png
Normal file
After Width: | Height: | Size: 34 KiB |
1
public/assets/baserow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 984 B |
1
public/assets/bookstack.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 794 B |
9
public/assets/calcomblack.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.7 KiB |
9
public/assets/calcomwhite.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/assets/element.png
Normal file
After Width: | Height: | Size: 37 KiB |
14
public/assets/espocrm.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.7 KiB |
BIN
public/assets/federated.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
public/assets/freescout.png
Normal file
After Width: | Height: | Size: 20 KiB |
1
public/assets/gitea.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 2.1 KiB |
1
public/assets/jitsi.svg
Normal file
After Width: | Height: | Size: 17 KiB |
1
public/assets/nextcloud.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 739 B |
BIN
public/assets/panelblack.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
public/assets/panelwhite.png
Normal file
After Width: | Height: | Size: 14 KiB |
1
public/assets/plane.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 330 B |
1
public/assets/powerdns.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 830 B |
1
public/assets/roundcube.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 785 B |
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 1.5 KiB |
42
src/App.css
Normal file
@@ -0,0 +1,42 @@
|
||||
#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;
|
||||
}
|
6
src/App.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from "react";
|
||||
import FederatedDashboard from "./FederatedDashboard";
|
||||
|
||||
export default function App() {
|
||||
return <FederatedDashboard />;
|
||||
}
|
493
src/FederatedDashboard.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
// 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
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
After Width: | Height: | Size: 4.0 KiB |
5
src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
/* Tailwind v4: single import */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* your app baseline */
|
||||
html, body, #root { height: 100%; }
|
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
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
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
25
tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|