diff --git a/experimental/app/.gitignore b/experimental/app/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/experimental/app/.gitignore
@@ -0,0 +1,24 @@
+# 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?
diff --git a/experimental/app/.vite/deps/_metadata.json b/experimental/app/.vite/deps/_metadata.json
new file mode 100644
index 0000000..8cac017
--- /dev/null
+++ b/experimental/app/.vite/deps/_metadata.json
@@ -0,0 +1,8 @@
+{
+ "hash": "0e2daab1",
+ "configHash": "9fbff803",
+ "lockfileHash": "e3b0c442",
+ "browserHash": "04872398",
+ "optimized": {},
+ "chunks": {}
+}
\ No newline at end of file
diff --git a/experimental/app/.vite/deps/package.json b/experimental/app/.vite/deps/package.json
new file mode 100644
index 0000000..3dbc1ca
--- /dev/null
+++ b/experimental/app/.vite/deps/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/experimental/app/README.md b/experimental/app/README.md
new file mode 100644
index 0000000..da98444
--- /dev/null
+++ b/experimental/app/README.md
@@ -0,0 +1,54 @@
+# 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({
+ extends: [
+ // 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,
+ ],
+ languageOptions: {
+ // other options...
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+})
+```
+
+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({
+ plugins: {
+ // Add the react-x and react-dom plugins
+ 'react-x': reactX,
+ 'react-dom': reactDom,
+ },
+ rules: {
+ // other rules...
+ // Enable its recommended typescript rules
+ ...reactX.configs['recommended-typescript'].rules,
+ ...reactDom.configs.recommended.rules,
+ },
+})
+```
diff --git a/experimental/app/components.json b/experimental/app/components.json
new file mode 100644
index 0000000..73afbdb
--- /dev/null
+++ b/experimental/app/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/experimental/app/eslint.config.js b/experimental/app/eslint.config.js
new file mode 100644
index 0000000..092408a
--- /dev/null
+++ b/experimental/app/eslint.config.js
@@ -0,0 +1,28 @@
+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'
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ },
+)
diff --git a/experimental/app/index.html b/experimental/app/index.html
new file mode 100644
index 0000000..58db59d
--- /dev/null
+++ b/experimental/app/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+ Wild Cloud Central
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
\ No newline at end of file
diff --git a/experimental/app/package.json b/experimental/app/package.json
new file mode 100644
index 0000000..68e5d02
--- /dev/null
+++ b/experimental/app/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "wild-cloud-central",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "type-check": "tsc --noEmit",
+ "test": "vitest",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest --coverage",
+ "build:css": "tailwindcss -i src/index.css -o ./output.css --config ./tailwind.config.js"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^5.1.1",
+ "@radix-ui/react-collapsible": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.14",
+ "@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-separator": "^1.1.7",
+ "@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-tooltip": "^1.2.7",
+ "@tailwindcss/vite": "^4.1.10",
+ "@tanstack/react-query": "^5.62.10",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.516.0",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-hook-form": "^7.58.1",
+ "tailwind-merge": "^3.3.1",
+ "tailwindcss": "^4.1.10",
+ "zod": "^3.25.67"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.25.0",
+ "@types/node": "^24.0.3",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.2",
+ "@vitejs/plugin-react": "^4.4.1",
+ "eslint": "^9.25.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.19",
+ "globals": "^16.0.0",
+ "tw-animate-css": "^1.3.4",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.30.1",
+ "vite": "^6.3.5"
+ },
+ "packageManager": "pnpm@10.9.0+sha512.0486e394640d3c1fb3c9d43d49cf92879ff74f8516959c235308f5a8f62e2e19528a65cdc2a3058f587cde71eba3d5b56327c8c33a97e4c4051ca48a10ca2d5f"
+}
diff --git a/experimental/app/pnpm-lock.yaml b/experimental/app/pnpm-lock.yaml
new file mode 100644
index 0000000..be90570
--- /dev/null
+++ b/experimental/app/pnpm-lock.yaml
@@ -0,0 +1,3284 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@hookform/resolvers':
+ specifier: ^5.1.1
+ version: 5.1.1(react-hook-form@7.58.1(react@19.1.0))
+ '@radix-ui/react-collapsible':
+ specifier: ^1.1.11
+ version: 1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-dialog':
+ specifier: ^1.1.14
+ version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-label':
+ specifier: ^2.1.7
+ version: 2.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-separator':
+ specifier: ^1.1.7
+ version: 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot':
+ specifier: ^1.2.3
+ version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.7
+ version: 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@tailwindcss/vite':
+ specifier: ^4.1.10
+ version: 4.1.10(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))
+ '@tanstack/react-query':
+ specifier: ^5.62.10
+ version: 5.80.7(react@19.1.0)
+ class-variance-authority:
+ specifier: ^0.7.1
+ version: 0.7.1
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
+ lucide-react:
+ specifier: ^0.516.0
+ version: 0.516.0(react@19.1.0)
+ react:
+ specifier: ^19.1.0
+ version: 19.1.0
+ react-dom:
+ specifier: ^19.1.0
+ version: 19.1.0(react@19.1.0)
+ react-hook-form:
+ specifier: ^7.58.1
+ version: 7.58.1(react@19.1.0)
+ tailwind-merge:
+ specifier: ^3.3.1
+ version: 3.3.1
+ tailwindcss:
+ specifier: ^4.1.10
+ version: 4.1.10
+ zod:
+ specifier: ^3.25.67
+ version: 3.25.67
+ devDependencies:
+ '@eslint/js':
+ specifier: ^9.25.0
+ version: 9.29.0
+ '@types/node':
+ specifier: ^24.0.3
+ version: 24.0.3
+ '@types/react':
+ specifier: ^19.1.2
+ version: 19.1.8
+ '@types/react-dom':
+ specifier: ^19.1.2
+ version: 19.1.6(@types/react@19.1.8)
+ '@vitejs/plugin-react':
+ specifier: ^4.4.1
+ version: 4.5.2(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))
+ eslint:
+ specifier: ^9.25.0
+ version: 9.29.0(jiti@2.4.2)
+ eslint-plugin-react-hooks:
+ specifier: ^5.2.0
+ version: 5.2.0(eslint@9.29.0(jiti@2.4.2))
+ eslint-plugin-react-refresh:
+ specifier: ^0.4.19
+ version: 0.4.20(eslint@9.29.0(jiti@2.4.2))
+ globals:
+ specifier: ^16.0.0
+ version: 16.2.0
+ tw-animate-css:
+ specifier: ^1.3.4
+ version: 1.3.4
+ typescript:
+ specifier: ~5.8.3
+ version: 5.8.3
+ typescript-eslint:
+ specifier: ^8.30.1
+ version: 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ vite:
+ specifier: ^6.3.5
+ version: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@babel/code-frame@7.27.1':
+ resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.27.5':
+ resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.27.4':
+ resolution: {integrity: sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.27.5':
+ resolution: {integrity: sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.27.2':
+ resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.27.1':
+ resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.27.3':
+ resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.27.1':
+ resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.27.1':
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.27.1':
+ resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.27.6':
+ resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.27.5':
+ resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1':
+ resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1':
+ resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.27.2':
+ resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.27.4':
+ resolution: {integrity: sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.27.6':
+ resolution: {integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.25.5':
+ resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.5':
+ resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.5':
+ resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.5':
+ resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.5':
+ resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.5':
+ resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.5':
+ resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.5':
+ resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.5':
+ resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.5':
+ resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.5':
+ resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.5':
+ resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.5':
+ resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.5':
+ resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.5':
+ resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.5':
+ resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.5':
+ resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.5':
+ resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.5':
+ resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.5':
+ resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.5':
+ resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.25.5':
+ resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.5':
+ resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.5':
+ resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.5':
+ resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/config-array@0.20.1':
+ resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.2.3':
+ resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.14.0':
+ resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.15.0':
+ resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.29.0':
+ resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.3.2':
+ resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@floating-ui/core@1.7.1':
+ resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==}
+
+ '@floating-ui/dom@1.7.1':
+ resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==}
+
+ '@floating-ui/react-dom@2.1.3':
+ resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.9':
+ resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+
+ '@hookform/resolvers@5.1.1':
+ resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==}
+ peerDependencies:
+ react-hook-form: ^7.55.0
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
+ '@isaacs/fs-minipass@4.0.1':
+ resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+ engines: {node: '>=18.0.0'}
+
+ '@jridgewell/gen-mapping@0.3.8':
+ resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/set-array@1.2.1':
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.0':
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@radix-ui/primitive@1.1.2':
+ resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==}
+
+ '@radix-ui/react-arrow@1.1.7':
+ resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collapsible@1.1.11':
+ resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.2':
+ resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context@1.1.2':
+ resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dialog@1.1.14':
+ resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.10':
+ resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.2':
+ resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.7':
+ resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.1':
+ resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-label@2.1.7':
+ resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.2.7':
+ resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.9':
+ resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.4':
+ resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.3':
+ resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-separator@1.1.7':
+ resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.2.3':
+ resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.7':
+ resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.1':
+ resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.2':
+ resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.2':
+ resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-escape-keydown@1.1.1':
+ resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.1':
+ resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.1':
+ resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.1':
+ resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.3':
+ resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.1':
+ resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+ '@rolldown/pluginutils@1.0.0-beta.11':
+ resolution: {integrity: sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag==}
+
+ '@rollup/rollup-android-arm-eabi@4.43.0':
+ resolution: {integrity: sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.43.0':
+ resolution: {integrity: sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.43.0':
+ resolution: {integrity: sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.43.0':
+ resolution: {integrity: sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.43.0':
+ resolution: {integrity: sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.43.0':
+ resolution: {integrity: sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.43.0':
+ resolution: {integrity: sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.43.0':
+ resolution: {integrity: sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.43.0':
+ resolution: {integrity: sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.43.0':
+ resolution: {integrity: sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.43.0':
+ resolution: {integrity: sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.43.0':
+ resolution: {integrity: sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.43.0':
+ resolution: {integrity: sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.43.0':
+ resolution: {integrity: sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.43.0':
+ resolution: {integrity: sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.43.0':
+ resolution: {integrity: sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.43.0':
+ resolution: {integrity: sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.43.0':
+ resolution: {integrity: sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.43.0':
+ resolution: {integrity: sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.43.0':
+ resolution: {integrity: sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/utils@0.3.0':
+ resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
+
+ '@tailwindcss/node@4.1.10':
+ resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.10':
+ resolution: {integrity: sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.10':
+ resolution: {integrity: sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.10':
+ resolution: {integrity: sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.10':
+ resolution: {integrity: sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
+ resolution: {integrity: sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
+ resolution: {integrity: sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.10':
+ resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.10':
+ resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.10':
+ resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.10':
+ resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
+ resolution: {integrity: sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.10':
+ resolution: {integrity: sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.10':
+ resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.10':
+ resolution: {integrity: sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6
+
+ '@tanstack/query-core@5.80.7':
+ resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==}
+
+ '@tanstack/react-query@5.80.7':
+ resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==}
+ peerDependencies:
+ react: ^18 || ^19
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.20.7':
+ resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
+
+ '@types/estree@1.0.7':
+ resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/node@24.0.3':
+ resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==}
+
+ '@types/react-dom@19.1.6':
+ resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==}
+ peerDependencies:
+ '@types/react': ^19.0.0
+
+ '@types/react@19.1.8':
+ resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==}
+
+ '@typescript-eslint/eslint-plugin@8.34.1':
+ resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^8.34.1
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/parser@8.34.1':
+ resolution: {integrity: sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/project-service@8.34.1':
+ resolution: {integrity: sha512-nuHlOmFZfuRwLJKDGQOVc0xnQrAmuq1Mj/ISou5044y1ajGNp2BNliIqp7F2LPQ5sForz8lempMFCovfeS1XoA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/scope-manager@8.34.1':
+ resolution: {integrity: sha512-beu6o6QY4hJAgL1E8RaXNC071G4Kso2MGmJskCFQhRhg8VOH/FDbC8soP8NHN7e/Hdphwp8G8cE6OBzC8o41ZA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/tsconfig-utils@8.34.1':
+ resolution: {integrity: sha512-K4Sjdo4/xF9NEeA2khOb7Y5nY6NSXBnod87uniVYW9kHP+hNlDV8trUSFeynA2uxWam4gIWgWoygPrv9VMWrYg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/type-utils@8.34.1':
+ resolution: {integrity: sha512-Tv7tCCr6e5m8hP4+xFugcrwTOucB8lshffJ6zf1mF1TbU67R+ntCc6DzLNKM+s/uzDyv8gLq7tufaAhIBYeV8g==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/types@8.34.1':
+ resolution: {integrity: sha512-rjLVbmE7HR18kDsjNIZQHxmv9RZwlgzavryL5Lnj2ujIRTeXlKtILHgRNmQ3j4daw7zd+mQgy+uyt6Zo6I0IGA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@8.34.1':
+ resolution: {integrity: sha512-rjCNqqYPuMUF5ODD+hWBNmOitjBWghkGKJg6hiCHzUvXRy6rK22Jd3rwbP2Xi+R7oYVvIKhokHVhH41BxPV5mA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@8.34.1':
+ resolution: {integrity: sha512-mqOwUdZ3KjtGk7xJJnLbHxTuWVn3GO2WZZuM+Slhkun4+qthLdXx32C8xIXbO1kfCECb3jIs3eoxK3eryk7aoQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/visitor-keys@8.34.1':
+ resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@vitejs/plugin-react@4.5.2':
+ resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ brace-expansion@2.0.2:
+ resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.25.0:
+ resolution: {integrity: sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ caniuse-lite@1.0.30001723:
+ resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ chownr@3.0.0:
+ resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+ engines: {node: '>=18'}
+
+ class-variance-authority@0.7.1:
+ resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ detect-libc@2.0.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+ engines: {node: '>=8'}
+
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+ electron-to-chromium@1.5.169:
+ resolution: {integrity: sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==}
+
+ enhanced-resolve@5.18.1:
+ resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
+ engines: {node: '>=10.13.0'}
+
+ esbuild@0.25.5:
+ resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react-hooks@5.2.0:
+ resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+ eslint-plugin-react-refresh@0.4.20:
+ resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==}
+ peerDependencies:
+ eslint: '>=8.40'
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.1:
+ resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@9.29.0:
+ resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ fdir@6.4.6:
+ resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
+ flatted@3.3.3:
+ resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@16.2.0:
+ resolution: {integrity: sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==}
+ engines: {node: '>=18'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@7.0.5:
+ resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lightningcss-darwin-arm64@1.30.1:
+ resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.30.1:
+ resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.30.1:
+ resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.30.1:
+ resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.30.1:
+ resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.30.1:
+ resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.30.1:
+ resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.30.1:
+ resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.30.1:
+ resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.30.1:
+ resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.30.1:
+ resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ lucide-react@0.516.0:
+ resolution: {integrity: sha512-aybBJzLHcw1CIn3rUcRkztB37dsJATtpffLNX+0/w+ws2p21nYIlOwX/B5fqxq8F/BjqVemnJX8chKwRidvROg==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ magic-string@0.30.17:
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minizlib@3.0.2:
+ resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
+ engines: {node: '>= 18'}
+
+ mkdirp@3.0.1:
+ resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.2:
+ resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ react-dom@19.1.0:
+ resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
+ peerDependencies:
+ react: ^19.1.0
+
+ react-hook-form@7.58.1:
+ resolution: {integrity: sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
+ react-refresh@0.17.0:
+ resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
+ engines: {node: '>=0.10.0'}
+
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.1:
+ resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react@19.1.0:
+ resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rollup@4.43.0:
+ resolution: {integrity: sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ scheduler@0.26.0:
+ resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ tailwind-merge@3.3.1:
+ resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+
+ tailwindcss@4.1.10:
+ resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
+
+ tapable@2.2.2:
+ resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
+ engines: {node: '>=6'}
+
+ tar@7.4.3:
+ resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.14:
+ resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ ts-api-utils@2.1.0:
+ resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tw-animate-css@1.3.4:
+ resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typescript-eslint@8.34.1:
+ resolution: {integrity: sha512-XjS+b6Vg9oT1BaIUfkW3M3LvqZE++rbzAMEHuccCfO/YkP43ha6w3jTEMilQxMF92nVOYCcdjv1ZUhAa1D/0ow==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ typescript@5.8.3:
+ resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@7.8.0:
+ resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
+
+ update-browserslist-db@1.1.3:
+ resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ vite@6.3.5:
+ resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yallist@5.0.0:
+ resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+ engines: {node: '>=18'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ zod@3.25.67:
+ resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@babel/code-frame@7.27.1':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.27.1
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.27.5': {}
+
+ '@babel/core@7.27.4':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.27.5
+ '@babel/helper-compilation-targets': 7.27.2
+ '@babel/helper-module-transforms': 7.27.3(@babel/core@7.27.4)
+ '@babel/helpers': 7.27.6
+ '@babel/parser': 7.27.5
+ '@babel/template': 7.27.2
+ '@babel/traverse': 7.27.4
+ '@babel/types': 7.27.6
+ convert-source-map: 2.0.0
+ debug: 4.4.1
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.27.5':
+ dependencies:
+ '@babel/parser': 7.27.5
+ '@babel/types': 7.27.6
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.27.2':
+ dependencies:
+ '@babel/compat-data': 7.27.5
+ '@babel/helper-validator-option': 7.27.1
+ browserslist: 4.25.0
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-module-imports@7.27.1':
+ dependencies:
+ '@babel/traverse': 7.27.4
+ '@babel/types': 7.27.6
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.27.3(@babel/core@7.27.4)':
+ dependencies:
+ '@babel/core': 7.27.4
+ '@babel/helper-module-imports': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+ '@babel/traverse': 7.27.4
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.27.1': {}
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-validator-identifier@7.27.1': {}
+
+ '@babel/helper-validator-option@7.27.1': {}
+
+ '@babel/helpers@7.27.6':
+ dependencies:
+ '@babel/template': 7.27.2
+ '@babel/types': 7.27.6
+
+ '@babel/parser@7.27.5':
+ dependencies:
+ '@babel/types': 7.27.6
+
+ '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.27.4)':
+ dependencies:
+ '@babel/core': 7.27.4
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.27.4)':
+ dependencies:
+ '@babel/core': 7.27.4
+ '@babel/helper-plugin-utils': 7.27.1
+
+ '@babel/template@7.27.2':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/parser': 7.27.5
+ '@babel/types': 7.27.6
+
+ '@babel/traverse@7.27.4':
+ dependencies:
+ '@babel/code-frame': 7.27.1
+ '@babel/generator': 7.27.5
+ '@babel/parser': 7.27.5
+ '@babel/template': 7.27.2
+ '@babel/types': 7.27.6
+ debug: 4.4.1
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.27.6':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+
+ '@esbuild/aix-ppc64@0.25.5':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/android-arm@0.25.5':
+ optional: true
+
+ '@esbuild/android-x64@0.25.5':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.5':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.5':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.5':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.5':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.5':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.5':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.5':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.5':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.5':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.5':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.5':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.5':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.5':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.5':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.5':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.5':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0(jiti@2.4.2))':
+ dependencies:
+ eslint: 9.29.0(jiti@2.4.2)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.20.1':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.1
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.2.3': {}
+
+ '@eslint/core@0.14.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/core@0.15.0':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.1
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.29.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.3.2':
+ dependencies:
+ '@eslint/core': 0.15.0
+ levn: 0.4.1
+
+ '@floating-ui/core@1.7.1':
+ dependencies:
+ '@floating-ui/utils': 0.2.9
+
+ '@floating-ui/dom@1.7.1':
+ dependencies:
+ '@floating-ui/core': 1.7.1
+ '@floating-ui/utils': 0.2.9
+
+ '@floating-ui/react-dom@2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@floating-ui/dom': 1.7.1
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
+ '@floating-ui/utils@0.2.9': {}
+
+ '@hookform/resolvers@5.1.1(react-hook-form@7.58.1(react@19.1.0))':
+ dependencies:
+ '@standard-schema/utils': 0.3.0
+ react-hook-form: 7.58.1(react@19.1.0)
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
+ '@isaacs/fs-minipass@4.0.1':
+ dependencies:
+ minipass: 7.1.2
+
+ '@jridgewell/gen-mapping@0.3.8':
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/set-array@1.2.1': {}
+
+ '@jridgewell/sourcemap-codec@1.5.0': {}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@radix-ui/primitive@1.1.2': {}
+
+ '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-context@1.1.2(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-dialog@1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
+ aria-hidden: 1.2.6
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ react-remove-scroll: 2.7.1(@types/react@19.1.8)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-id@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-label@2.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/rect': 1.1.1
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-slot@1.2.3(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.2
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.8)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/rect': 1.1.1
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-use-size@1.1.1(@types/react@19.1.8)(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0)
+ react: 19.1.0
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+ '@types/react-dom': 19.1.6(@types/react@19.1.8)
+
+ '@radix-ui/rect@1.1.1': {}
+
+ '@rolldown/pluginutils@1.0.0-beta.11': {}
+
+ '@rollup/rollup-android-arm-eabi@4.43.0':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.43.0':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.43.0':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.43.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.43.0':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.43.0':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.43.0':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.43.0':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.43.0':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.43.0':
+ optional: true
+
+ '@standard-schema/utils@0.3.0': {}
+
+ '@tailwindcss/node@4.1.10':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ enhanced-resolve: 5.18.1
+ jiti: 2.4.2
+ lightningcss: 1.30.1
+ magic-string: 0.30.17
+ source-map-js: 1.2.1
+ tailwindcss: 4.1.10
+
+ '@tailwindcss/oxide-android-arm64@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.10':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.10':
+ dependencies:
+ detect-libc: 2.0.4
+ tar: 7.4.3
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.10
+ '@tailwindcss/oxide-darwin-arm64': 4.1.10
+ '@tailwindcss/oxide-darwin-x64': 4.1.10
+ '@tailwindcss/oxide-freebsd-x64': 4.1.10
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.10
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.10
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.10
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.10
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.10
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.10
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.10
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.10
+
+ '@tailwindcss/vite@4.1.10(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))':
+ dependencies:
+ '@tailwindcss/node': 4.1.10
+ '@tailwindcss/oxide': 4.1.10
+ tailwindcss: 4.1.10
+ vite: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)
+
+ '@tanstack/query-core@5.80.7': {}
+
+ '@tanstack/react-query@5.80.7(react@19.1.0)':
+ dependencies:
+ '@tanstack/query-core': 5.80.7
+ react: 19.1.0
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.27.5
+ '@babel/types': 7.27.6
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.20.7
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.27.6
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.27.5
+ '@babel/types': 7.27.6
+
+ '@types/babel__traverse@7.20.7':
+ dependencies:
+ '@babel/types': 7.27.6
+
+ '@types/estree@1.0.7': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/node@24.0.3':
+ dependencies:
+ undici-types: 7.8.0
+
+ '@types/react-dom@19.1.6(@types/react@19.1.8)':
+ dependencies:
+ '@types/react': 19.1.8
+
+ '@types/react@19.1.8':
+ dependencies:
+ csstype: 3.1.3
+
+ '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/type-utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.34.1
+ eslint: 9.29.0(jiti@2.4.2)
+ graphemer: 1.4.0
+ ignore: 7.0.5
+ natural-compare: 1.4.0
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/visitor-keys': 8.34.1
+ debug: 4.4.1
+ eslint: 9.29.0(jiti@2.4.2)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/project-service@8.34.1(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/types': 8.34.1
+ debug: 4.4.1
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@8.34.1':
+ dependencies:
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/visitor-keys': 8.34.1
+
+ '@typescript-eslint/tsconfig-utils@8.34.1(typescript@5.8.3)':
+ dependencies:
+ typescript: 5.8.3
+
+ '@typescript-eslint/type-utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ debug: 4.4.1
+ eslint: 9.29.0(jiti@2.4.2)
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@8.34.1': {}
+
+ '@typescript-eslint/typescript-estree@8.34.1(typescript@5.8.3)':
+ dependencies:
+ '@typescript-eslint/project-service': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/tsconfig-utils': 8.34.1(typescript@5.8.3)
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/visitor-keys': 8.34.1
+ debug: 4.4.1
+ fast-glob: 3.3.3
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.7.2
+ ts-api-utils: 2.1.0(typescript@5.8.3)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2))
+ '@typescript-eslint/scope-manager': 8.34.1
+ '@typescript-eslint/types': 8.34.1
+ '@typescript-eslint/typescript-estree': 8.34.1(typescript@5.8.3)
+ eslint: 9.29.0(jiti@2.4.2)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@8.34.1':
+ dependencies:
+ '@typescript-eslint/types': 8.34.1
+ eslint-visitor-keys: 4.2.1
+
+ '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))':
+ dependencies:
+ '@babel/core': 7.27.4
+ '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4)
+ '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.27.4)
+ '@rolldown/pluginutils': 1.0.0-beta.11
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.17.0
+ vite: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ argparse@2.0.1: {}
+
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.2:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist@4.25.0:
+ dependencies:
+ caniuse-lite: 1.0.30001723
+ electron-to-chromium: 1.5.169
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.3(browserslist@4.25.0)
+
+ callsites@3.1.0: {}
+
+ caniuse-lite@1.0.30001723: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chownr@3.0.0: {}
+
+ class-variance-authority@0.7.1:
+ dependencies:
+ clsx: 2.1.1
+
+ clsx@2.1.1: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ concat-map@0.0.1: {}
+
+ convert-source-map@2.0.0: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ csstype@3.1.3: {}
+
+ debug@4.4.1:
+ dependencies:
+ ms: 2.1.3
+
+ deep-is@0.1.4: {}
+
+ detect-libc@2.0.4: {}
+
+ detect-node-es@1.1.0: {}
+
+ electron-to-chromium@1.5.169: {}
+
+ enhanced-resolve@5.18.1:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.2.2
+
+ esbuild@0.25.5:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.5
+ '@esbuild/android-arm': 0.25.5
+ '@esbuild/android-arm64': 0.25.5
+ '@esbuild/android-x64': 0.25.5
+ '@esbuild/darwin-arm64': 0.25.5
+ '@esbuild/darwin-x64': 0.25.5
+ '@esbuild/freebsd-arm64': 0.25.5
+ '@esbuild/freebsd-x64': 0.25.5
+ '@esbuild/linux-arm': 0.25.5
+ '@esbuild/linux-arm64': 0.25.5
+ '@esbuild/linux-ia32': 0.25.5
+ '@esbuild/linux-loong64': 0.25.5
+ '@esbuild/linux-mips64el': 0.25.5
+ '@esbuild/linux-ppc64': 0.25.5
+ '@esbuild/linux-riscv64': 0.25.5
+ '@esbuild/linux-s390x': 0.25.5
+ '@esbuild/linux-x64': 0.25.5
+ '@esbuild/netbsd-arm64': 0.25.5
+ '@esbuild/netbsd-x64': 0.25.5
+ '@esbuild/openbsd-arm64': 0.25.5
+ '@esbuild/openbsd-x64': 0.25.5
+ '@esbuild/sunos-x64': 0.25.5
+ '@esbuild/win32-arm64': 0.25.5
+ '@esbuild/win32-ia32': 0.25.5
+ '@esbuild/win32-x64': 0.25.5
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react-hooks@5.2.0(eslint@9.29.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.29.0(jiti@2.4.2)
+
+ eslint-plugin-react-refresh@0.4.20(eslint@9.29.0(jiti@2.4.2)):
+ dependencies:
+ eslint: 9.29.0(jiti@2.4.2)
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.29.0(jiti@2.4.2):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0(jiti@2.4.2))
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.20.1
+ '@eslint/config-helpers': 0.2.3
+ '@eslint/core': 0.14.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.29.0
+ '@eslint/plugin-kit': 0.3.2
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.1
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 2.4.2
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ esutils@2.0.3: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.4.6(picomatch@4.0.2):
+ optionalDependencies:
+ picomatch: 4.0.2
+
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
+ flatted@3.3.3: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ get-nonce@1.0.1: {}
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@11.12.0: {}
+
+ globals@14.0.0: {}
+
+ globals@16.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ graphemer@1.4.0: {}
+
+ has-flag@4.0.0: {}
+
+ ignore@5.3.2: {}
+
+ ignore@7.0.5: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
+ is-extglob@2.1.1: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ isexe@2.0.0: {}
+
+ jiti@2.4.2: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lightningcss-darwin-arm64@1.30.1:
+ optional: true
+
+ lightningcss-darwin-x64@1.30.1:
+ optional: true
+
+ lightningcss-freebsd-x64@1.30.1:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.30.1:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.30.1:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.30.1:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.30.1:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.30.1:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.30.1:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.30.1:
+ optional: true
+
+ lightningcss@1.30.1:
+ dependencies:
+ detect-libc: 2.0.4
+ optionalDependencies:
+ lightningcss-darwin-arm64: 1.30.1
+ lightningcss-darwin-x64: 1.30.1
+ lightningcss-freebsd-x64: 1.30.1
+ lightningcss-linux-arm-gnueabihf: 1.30.1
+ lightningcss-linux-arm64-gnu: 1.30.1
+ lightningcss-linux-arm64-musl: 1.30.1
+ lightningcss-linux-x64-gnu: 1.30.1
+ lightningcss-linux-x64-musl: 1.30.1
+ lightningcss-win32-arm64-msvc: 1.30.1
+ lightningcss-win32-x64-msvc: 1.30.1
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ lucide-react@0.516.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ magic-string@0.30.17:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.2
+
+ minipass@7.1.2: {}
+
+ minizlib@3.0.2:
+ dependencies:
+ minipass: 7.1.2
+
+ mkdirp@3.0.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ natural-compare@1.4.0: {}
+
+ node-releases@2.0.19: {}
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.2: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prelude-ls@1.2.1: {}
+
+ punycode@2.3.1: {}
+
+ queue-microtask@1.2.3: {}
+
+ react-dom@19.1.0(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ scheduler: 0.26.0
+
+ react-hook-form@7.58.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
+ react-refresh@0.17.0: {}
+
+ react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ react-remove-scroll@2.7.1(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ react-remove-scroll-bar: 2.3.8(@types/react@19.1.8)(react@19.1.0)
+ react-style-singleton: 2.2.3(@types/react@19.1.8)(react@19.1.0)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.1.8)(react@19.1.0)
+ use-sidecar: 1.1.3(@types/react@19.1.8)(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ react-style-singleton@2.2.3(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ react@19.1.0: {}
+
+ resolve-from@4.0.0: {}
+
+ reusify@1.1.0: {}
+
+ rollup@4.43.0:
+ dependencies:
+ '@types/estree': 1.0.7
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.43.0
+ '@rollup/rollup-android-arm64': 4.43.0
+ '@rollup/rollup-darwin-arm64': 4.43.0
+ '@rollup/rollup-darwin-x64': 4.43.0
+ '@rollup/rollup-freebsd-arm64': 4.43.0
+ '@rollup/rollup-freebsd-x64': 4.43.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.43.0
+ '@rollup/rollup-linux-arm-musleabihf': 4.43.0
+ '@rollup/rollup-linux-arm64-gnu': 4.43.0
+ '@rollup/rollup-linux-arm64-musl': 4.43.0
+ '@rollup/rollup-linux-loongarch64-gnu': 4.43.0
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.43.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.43.0
+ '@rollup/rollup-linux-riscv64-musl': 4.43.0
+ '@rollup/rollup-linux-s390x-gnu': 4.43.0
+ '@rollup/rollup-linux-x64-gnu': 4.43.0
+ '@rollup/rollup-linux-x64-musl': 4.43.0
+ '@rollup/rollup-win32-arm64-msvc': 4.43.0
+ '@rollup/rollup-win32-ia32-msvc': 4.43.0
+ '@rollup/rollup-win32-x64-msvc': 4.43.0
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ scheduler@0.26.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.7.2: {}
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ source-map-js@1.2.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ tailwind-merge@3.3.1: {}
+
+ tailwindcss@4.1.10: {}
+
+ tapable@2.2.2: {}
+
+ tar@7.4.3:
+ dependencies:
+ '@isaacs/fs-minipass': 4.0.1
+ chownr: 3.0.0
+ minipass: 7.1.2
+ minizlib: 3.0.2
+ mkdirp: 3.0.1
+ yallist: 5.0.0
+
+ tinyglobby@0.2.14:
+ dependencies:
+ fdir: 6.4.6(picomatch@4.0.2)
+ picomatch: 4.0.2
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ ts-api-utils@2.1.0(typescript@5.8.3):
+ dependencies:
+ typescript: 5.8.3
+
+ tslib@2.8.1: {}
+
+ tw-animate-css@1.3.4: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typescript-eslint@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/parser': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ '@typescript-eslint/utils': 8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)
+ eslint: 9.29.0(jiti@2.4.2)
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@5.8.3: {}
+
+ undici-types@7.8.0: {}
+
+ update-browserslist-db@1.1.3(browserslist@4.25.0):
+ dependencies:
+ browserslist: 4.25.0
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ use-callback-ref@1.3.3(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ use-sidecar@1.1.3(@types/react@19.1.8)(react@19.1.0):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.1.0
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.1.8
+
+ vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1):
+ dependencies:
+ esbuild: 0.25.5
+ fdir: 6.4.6(picomatch@4.0.2)
+ picomatch: 4.0.2
+ postcss: 8.5.6
+ rollup: 4.43.0
+ tinyglobby: 0.2.14
+ optionalDependencies:
+ '@types/node': 24.0.3
+ fsevents: 2.3.3
+ jiti: 2.4.2
+ lightningcss: 1.30.1
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ word-wrap@1.2.5: {}
+
+ yallist@3.1.1: {}
+
+ yallist@5.0.0: {}
+
+ yocto-queue@0.1.0: {}
+
+ zod@3.25.67: {}
diff --git a/experimental/app/public/vite.svg b/experimental/app/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/experimental/app/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/app/src/App.css b/experimental/app/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/experimental/app/src/App.css
@@ -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;
+}
diff --git a/experimental/app/src/App.tsx b/experimental/app/src/App.tsx
new file mode 100644
index 0000000..4879a4b
--- /dev/null
+++ b/experimental/app/src/App.tsx
@@ -0,0 +1,140 @@
+import { useEffect, useState } from 'react';
+import { useConfig } from './hooks';
+import {
+ Advanced,
+ ErrorBoundary
+} from './components';
+import { CloudComponent } from './components/CloudComponent';
+import { CentralComponent } from './components/CentralComponent';
+import { DnsComponent } from './components/DnsComponent';
+import { DhcpComponent } from './components/DhcpComponent';
+import { PxeComponent } from './components/PxeComponent';
+import { ClusterNodesComponent } from './components/ClusterNodesComponent';
+import { ClusterServicesComponent } from './components/ClusterServicesComponent';
+import { AppsComponent } from './components/AppsComponent';
+import { AppSidebar } from './components/AppSidebar';
+import { SidebarProvider, SidebarInset, SidebarTrigger } from './components/ui/sidebar';
+import type { Phase, Tab } from './components/AppSidebar';
+
+function App() {
+ const [currentTab, setCurrentTab] = useState('cloud');
+ const [completedPhases, setCompletedPhases] = useState([]);
+
+ const { config } = useConfig();
+
+ // Update phase state from config when it changes
+ useEffect(() => {
+ console.log('Config changed:', config);
+ console.log('config?.wildcloud:', config?.wildcloud);
+ if (config?.wildcloud?.currentPhase) {
+ console.log('Setting currentTab to:', config.wildcloud.currentPhase);
+ setCurrentTab(config.wildcloud.currentPhase as Phase);
+ }
+ if (config?.wildcloud?.completedPhases) {
+ console.log('Setting completedPhases to:', config.wildcloud.completedPhases);
+ setCompletedPhases(config.wildcloud.completedPhases as Phase[]);
+ }
+ }, [config]);
+
+ const handlePhaseComplete = (phase: Phase) => {
+ if (!completedPhases.includes(phase)) {
+ setCompletedPhases(prev => [...prev, phase]);
+ }
+
+ // Auto-advance to next phase (excluding advanced)
+ const phases: Phase[] = ['setup', 'infrastructure', 'cluster', 'apps'];
+ const currentIndex = phases.indexOf(phase);
+ if (currentIndex < phases.length - 1) {
+ setCurrentTab(phases[currentIndex + 1]);
+ }
+ };
+
+ const renderCurrentTab = () => {
+ switch (currentTab) {
+ case 'cloud':
+ return (
+
+
+
+ );
+ case 'central':
+ return (
+
+
+
+ );
+ case 'dns':
+ return (
+
+
+
+ );
+ case 'dhcp':
+ return (
+
+
+
+ );
+ case 'pxe':
+ return (
+
+
+
+ );
+ case 'setup':
+ case 'infrastructure':
+ return (
+
+ handlePhaseComplete('infrastructure')} />
+
+ );
+ case 'cluster':
+ return (
+
+ handlePhaseComplete('cluster')} />
+
+ );
+ case 'apps':
+ return (
+
+ handlePhaseComplete('apps')} />
+
+ );
+ case 'advanced':
+ return (
+
+
+
+ );
+ default:
+ return (
+
+
+
+ );
+ }
+ };
+
+ return (
+
+
+
+
+
+ {renderCurrentTab()}
+
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/experimental/app/src/assets/react.svg b/experimental/app/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/experimental/app/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/experimental/app/src/components/Advanced.tsx b/experimental/app/src/components/Advanced.tsx
new file mode 100644
index 0000000..583a85d
--- /dev/null
+++ b/experimental/app/src/components/Advanced.tsx
@@ -0,0 +1,114 @@
+import { useState } from "react";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "./ui/card";
+import { ConfigEditor } from "./ConfigEditor";
+import { Button, Input, Label } from "./ui";
+import { Check, Edit2, HelpCircle, X } from "lucide-react";
+
+export function Advanced() {
+ const [upstreamValue, setUpstreamValue] = useState("https://mywildcloud.org");
+ const [editingUpstream, setEditingUpstream] = useState(false);
+ const [tempUpstream, setTempUpstream] = useState(upstreamValue);
+ const handleUpstreamEdit = () => {
+ setTempUpstream(upstreamValue);
+ setEditingUpstream(true);
+ };
+
+ const handleUpstreamSave = () => {
+ setUpstreamValue(tempUpstream);
+ setEditingUpstream(false);
+ };
+
+ const handleUpstreamCancel = () => {
+ setTempUpstream(upstreamValue);
+ setEditingUpstream(false);
+ };
+
+ return (
+
+
+
+ Advanced Configuration
+
+ Advanced settings and system configuration options
+
+
+
+
+
+ Configuration Management
+
+
+ Edit the raw YAML configuration file directly. This provides full
+ access to all configuration options.
+
+
+
+
+
+ {/* Upstream Section */}
+
+
+
+
Upstream Configuration
+
+ External service endpoint
+
+
+
+
+
+
+ {!editingUpstream && (
+
+
+ Edit
+
+ )}
+
+
+
+ {editingUpstream ? (
+
+
+ Upstream URL
+ setTempUpstream(e.target.value)}
+ placeholder="https://example.com"
+ className="mt-1"
+ />
+
+
+
+
+ Save
+
+
+
+ Cancel
+
+
+
+ ) : (
+
+
Upstream URL
+
+ {upstreamValue}
+
+
+ )}
+
+
+ );
+}
diff --git a/experimental/app/src/components/AppSidebar.tsx b/experimental/app/src/components/AppSidebar.tsx
new file mode 100644
index 0000000..d2ab6e5
--- /dev/null
+++ b/experimental/app/src/components/AppSidebar.tsx
@@ -0,0 +1,416 @@
+import { CheckCircle, Lock, Server, Play, Container, AppWindow, Settings, CloudLightning, Sun, Moon, Monitor, ChevronDown, Globe, Wifi, HardDrive } from 'lucide-react';
+import { cn } from '../lib/utils';
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarRail,
+} from './ui/sidebar';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
+import { useTheme } from '../contexts/ThemeContext';
+
+export type Phase = 'setup' | 'infrastructure' | 'cluster' | 'apps';
+export type Tab = Phase | 'advanced' | 'cloud' | 'central' | 'dns' | 'dhcp' | 'pxe';
+
+interface AppSidebarProps {
+ currentTab: Tab;
+ onTabChange: (tab: Tab) => void;
+ completedPhases: Phase[];
+}
+
+
+export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSidebarProps) {
+ const { theme, setTheme } = useTheme();
+
+ const cycleTheme = () => {
+ if (theme === 'light') {
+ setTheme('dark');
+ } else if (theme === 'dark') {
+ setTheme('system');
+ } else {
+ setTheme('light');
+ }
+ };
+
+ const getThemeIcon = () => {
+ switch (theme) {
+ case 'light':
+ return ;
+ case 'dark':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getThemeLabel = () => {
+ switch (theme) {
+ case 'light':
+ return 'Light mode';
+ case 'dark':
+ return 'Dark mode';
+ default:
+ return 'System theme';
+ }
+ };
+
+ const getTabStatus = (tab: Tab) => {
+ // Non-phase tabs (like advanced and cloud) are always available
+ if (tab === 'advanced' || tab === 'cloud') {
+ return 'available';
+ }
+
+ // Central sub-tabs are available if setup phase is available or completed
+ if (tab === 'central' || tab === 'dns' || tab === 'dhcp' || tab === 'pxe') {
+ if (completedPhases.includes('setup')) {
+ return 'completed';
+ }
+ return 'available';
+ }
+
+ // For phase tabs, check completion status
+ if (completedPhases.includes(tab as Phase)) {
+ return 'completed';
+ }
+
+ // Allow access to the first phase always
+ if (tab === 'setup') {
+ return 'available';
+ }
+
+ // Allow access to the next phase if the previous phase is completed
+ if (tab === 'infrastructure' && completedPhases.includes('setup')) {
+ return 'available';
+ }
+
+ if (tab === 'cluster' && completedPhases.includes('infrastructure')) {
+ return 'available';
+ }
+
+ if (tab === 'apps' && completedPhases.includes('cluster')) {
+ return 'available';
+ }
+
+ return 'locked';
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {
+ const status = getTabStatus('cloud');
+ if (status !== 'locked') onTabChange('cloud');
+ }}
+ disabled={getTabStatus('cloud') === 'locked'}
+ tooltip="Configure cloud settings and domains"
+ className={cn(
+ "transition-colors",
+ getTabStatus('cloud') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ Cloud
+
+
+
+
+
+
+
+
+ Central
+
+
+
+
+
+
+ {
+ const status = getTabStatus('central');
+ if (status !== 'locked') onTabChange('central');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('central') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ Central
+
+
+
+
+ {
+ const status = getTabStatus('dns');
+ if (status !== 'locked') onTabChange('dns');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('dns') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ DNS
+
+
+
+
+ {
+ const status = getTabStatus('dhcp');
+ if (status !== 'locked') onTabChange('dhcp');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('dhcp') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ DHCP
+
+
+
+
+ {
+ const status = getTabStatus('pxe');
+ if (status !== 'locked') onTabChange('pxe');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('pxe') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ PXE
+
+
+
+
+
+
+
+
+
+
+
+
+ Cluster
+
+
+
+
+
+
+ {
+ const status = getTabStatus('infrastructure');
+ if (status !== 'locked') onTabChange('infrastructure');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('infrastructure') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+ Cluster Nodes
+
+
+
+
+ {
+ const status = getTabStatus('cluster');
+ if (status !== 'locked') onTabChange('cluster');
+ }}
+ className={cn(
+ "transition-colors",
+ getTabStatus('cluster') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ Cluster Services
+
+
+
+
+
+
+
+
+ {
+ const status = getTabStatus('apps');
+ if (status !== 'locked') onTabChange('apps');
+ }}
+ disabled={getTabStatus('apps') === 'locked'}
+ tooltip="Install and manage applications"
+ className={cn(
+ "transition-colors",
+ getTabStatus('apps') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+ Apps
+
+
+
+
+ {
+ const status = getTabStatus('advanced');
+ if (status !== 'locked') onTabChange('advanced');
+ }}
+ disabled={getTabStatus('advanced') === 'locked'}
+ tooltip="Advanced settings and system configuration"
+ className={cn(
+ "transition-colors",
+ getTabStatus('advanced') === 'locked' && "opacity-50 cursor-not-allowed"
+ )}
+ >
+
+
+
+ Advanced
+
+
+
+
+
+
+
+
+ {getThemeIcon()}
+ {getThemeLabel()}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/AppsComponent.tsx b/experimental/app/src/components/AppsComponent.tsx
new file mode 100644
index 0000000..3002f9a
--- /dev/null
+++ b/experimental/app/src/components/AppsComponent.tsx
@@ -0,0 +1,394 @@
+import { useState } from 'react';
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Badge } from './ui/badge';
+import {
+ AppWindow,
+ Database,
+ Globe,
+ Shield,
+ BarChart3,
+ MessageSquare,
+ Plus,
+ Search,
+ Settings,
+ ExternalLink,
+ CheckCircle,
+ AlertCircle,
+ Clock,
+ Download,
+ Trash2,
+ BookOpen
+} from 'lucide-react';
+
+interface AppsComponentProps {
+ onComplete?: () => void;
+}
+
+interface Application {
+ id: string;
+ name: string;
+ description: string;
+ category: 'database' | 'web' | 'security' | 'monitoring' | 'communication' | 'storage';
+ status: 'available' | 'installing' | 'running' | 'error' | 'stopped';
+ version?: string;
+ namespace?: string;
+ replicas?: number;
+ resources?: {
+ cpu: string;
+ memory: string;
+ };
+ urls?: string[];
+}
+
+export function AppsComponent({ onComplete }: AppsComponentProps) {
+ const [applications, setApplications] = useState([
+ {
+ id: 'postgres',
+ name: 'PostgreSQL',
+ description: 'Reliable, high-performance SQL database',
+ category: 'database',
+ status: 'running',
+ version: 'v15.4',
+ namespace: 'default',
+ replicas: 1,
+ resources: { cpu: '500m', memory: '1Gi' },
+ urls: ['postgres://postgres.wildcloud.local:5432'],
+ },
+ {
+ id: 'redis',
+ name: 'Redis',
+ description: 'In-memory data structure store',
+ category: 'database',
+ status: 'running',
+ version: 'v7.2',
+ namespace: 'default',
+ replicas: 1,
+ resources: { cpu: '250m', memory: '512Mi' },
+ },
+ {
+ id: 'traefik-dashboard',
+ name: 'Traefik Dashboard',
+ description: 'Load balancer and reverse proxy dashboard',
+ category: 'web',
+ status: 'running',
+ version: 'v3.0',
+ namespace: 'kube-system',
+ urls: ['https://traefik.wildcloud.local'],
+ },
+ {
+ id: 'grafana',
+ name: 'Grafana',
+ description: 'Monitoring and observability dashboards',
+ category: 'monitoring',
+ status: 'installing',
+ version: 'v10.2',
+ namespace: 'monitoring',
+ },
+ {
+ id: 'prometheus',
+ name: 'Prometheus',
+ description: 'Time-series monitoring and alerting',
+ category: 'monitoring',
+ status: 'running',
+ version: 'v2.45',
+ namespace: 'monitoring',
+ replicas: 1,
+ resources: { cpu: '1000m', memory: '2Gi' },
+ },
+ {
+ id: 'vault',
+ name: 'HashiCorp Vault',
+ description: 'Secrets management and encryption',
+ category: 'security',
+ status: 'available',
+ version: 'v1.15',
+ },
+ {
+ id: 'minio',
+ name: 'MinIO',
+ description: 'High-performance object storage',
+ category: 'storage',
+ status: 'available',
+ version: 'RELEASE.2023-12-07',
+ },
+ ]);
+
+ const [searchTerm, setSearchTerm] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState('all');
+
+ const getStatusIcon = (status: Application['status']) => {
+ switch (status) {
+ case 'running':
+ return ;
+ case 'error':
+ return ;
+ case 'installing':
+ return ;
+ case 'stopped':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusBadge = (status: Application['status']) => {
+ const variants = {
+ available: 'secondary',
+ installing: 'default',
+ running: 'success',
+ error: 'destructive',
+ stopped: 'warning',
+ } as const;
+
+ const labels = {
+ available: 'Available',
+ installing: 'Installing',
+ running: 'Running',
+ error: 'Error',
+ stopped: 'Stopped',
+ };
+
+ return (
+
+ {labels[status]}
+
+ );
+ };
+
+ const getCategoryIcon = (category: Application['category']) => {
+ switch (category) {
+ case 'database':
+ return ;
+ case 'web':
+ return ;
+ case 'security':
+ return ;
+ case 'monitoring':
+ return ;
+ case 'communication':
+ return ;
+ case 'storage':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleAppAction = (appId: string, action: 'install' | 'start' | 'stop' | 'delete' | 'configure') => {
+ console.log(`${action} app: ${appId}`);
+ };
+
+ const categories = ['all', 'database', 'web', 'security', 'monitoring', 'communication', 'storage'];
+
+ const filteredApps = applications.filter(app => {
+ const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ app.description.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesCategory = selectedCategory === 'all' || app.category === selectedCategory;
+ return matchesSearch && matchesCategory;
+ });
+
+ const runningApps = applications.filter(app => app.status === 'running').length;
+
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What are Apps in your Personal Cloud?
+
+
+ Apps are the useful programs that make your personal cloud valuable - like having a personal Netflix
+ (media server), Google Drive (file storage), or Gmail (email server) running on your own hardware.
+ Instead of relying on big tech companies, you control your data and services.
+
+
+ Your cluster can run databases, web servers, photo galleries, password managers, backup services, and much more.
+ Each app runs in its own secure container, so they don't interfere with each other and can be easily managed.
+
+
+
+ Learn more about self-hosted applications
+
+
+
+
+
+
+
+
+
+
App Management
+
+ Install and manage applications on your Kubernetes cluster
+
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 border rounded-lg bg-background"
+ />
+
+
+ {categories.map(category => (
+ setSelectedCategory(category)}
+ className="capitalize"
+ >
+ {category}
+
+ ))}
+
+
+
+
+
+ {runningApps} applications running โข {applications.length} total available
+
+
+
+ Add App
+
+
+
+
+
+ {filteredApps.map((app) => (
+
+
+
+ {getCategoryIcon(app.category)}
+
+
+
+
{app.name}
+ {app.version && (
+
+ {app.version}
+
+ )}
+ {getStatusIcon(app.status)}
+
+
{app.description}
+
+ {app.status === 'running' && (
+
+ {app.namespace && (
+
Namespace: {app.namespace}
+ )}
+ {app.replicas && (
+
Replicas: {app.replicas}
+ )}
+ {app.resources && (
+
Resources: {app.resources.cpu} CPU, {app.resources.memory} RAM
+ )}
+ {app.urls && app.urls.length > 0 && (
+
+ URLs:
+ {app.urls.map((url, index) => (
+ window.open(url, '_blank')}
+ >
+
+ Access
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ {getStatusBadge(app.status)}
+
+ {app.status === 'available' && (
+ handleAppAction(app.id, 'install')}
+ >
+ Install
+
+ )}
+ {app.status === 'running' && (
+ <>
+ handleAppAction(app.id, 'configure')}
+ >
+
+
+ handleAppAction(app.id, 'stop')}
+ >
+ Stop
+
+ >
+ )}
+ {app.status === 'stopped' && (
+ handleAppAction(app.id, 'start')}
+ >
+ Start
+
+ )}
+ {(app.status === 'running' || app.status === 'stopped') && (
+ handleAppAction(app.id, 'delete')}
+ >
+
+
+ )}
+
+
+
+
+ ))}
+
+
+ {filteredApps.length === 0 && (
+
+
+ No applications found
+
+ {searchTerm || selectedCategory !== 'all'
+ ? 'Try adjusting your search or category filter'
+ : 'Install your first application to get started'
+ }
+
+
+
+ Browse App Catalog
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/CentralComponent.tsx b/experimental/app/src/components/CentralComponent.tsx
new file mode 100644
index 0000000..85c78ee
--- /dev/null
+++ b/experimental/app/src/components/CentralComponent.tsx
@@ -0,0 +1,114 @@
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Server, Network, Settings, Clock, HelpCircle, CheckCircle, BookOpen, ExternalLink } from 'lucide-react';
+import { Input, Label } from './ui';
+
+export function CentralComponent() {
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What is the Central Service?
+
+
+ The Central Service is the "brain" of your personal cloud. It acts as the main coordinator that manages
+ all the different services running on your network. Think of it like the control tower at an airport -
+ it keeps track of what's happening, routes traffic between services, and ensures everything works together smoothly.
+
+
+ This service handles configuration management, service discovery, and provides the web interface you're using right now.
+
+
+
+ Learn more about service orchestration
+
+
+
+
+
+
+
+
+
+
+
+
Central Service
+
+ Monitor and manage the central server service
+
+
+
+
+
+
Service Status
+
+
+
+
+ IP Address: 192.168.8.50
+
+
+
+ Network: 192.168.8.0/24
+
+
+
+ Version: 1.0.0 (update available)
+
+
+
+ Age: 12s
+
+
+
+ Platform: ARM
+
+
+
+ File permissions: Good
+
+
+
+
+
+
+
Interface
+
+
+
+
+
+
+
+
+
+
+ console.log('Update service')}>
+ Update
+
+ console.log('Restart service')}>
+ Restart
+
+ console.log('View log')}>
+ View log
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/CloudComponent.tsx b/experimental/app/src/components/CloudComponent.tsx
new file mode 100644
index 0000000..bdd32e9
--- /dev/null
+++ b/experimental/app/src/components/CloudComponent.tsx
@@ -0,0 +1,137 @@
+import { useState } from "react";
+import { Card } from "./ui/card";
+import { Button } from "./ui/button";
+import { Cloud, HelpCircle, Edit2, Check, X } from "lucide-react";
+import { Input, Label } from "./ui";
+
+export function CloudComponent() {
+ const [domainValue, setDomainValue] = useState("cloud.payne.io");
+ const [internalDomainValue, setInternalDomainValue] = useState(
+ "internal.cloud.payne.io"
+ );
+
+ const [editingDomains, setEditingDomains] = useState(false);
+
+ const [tempDomain, setTempDomain] = useState(domainValue);
+ const [tempInternalDomain, setTempInternalDomain] =
+ useState(internalDomainValue);
+
+ const handleDomainsEdit = () => {
+ setTempDomain(domainValue);
+ setTempInternalDomain(internalDomainValue);
+ setEditingDomains(true);
+ };
+
+ const handleDomainsSave = () => {
+ setDomainValue(tempDomain);
+ setInternalDomainValue(tempInternalDomain);
+ setEditingDomains(false);
+ };
+
+ const handleDomainsCancel = () => {
+ setTempDomain(domainValue);
+ setTempInternalDomain(internalDomainValue);
+ setEditingDomains(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
Cloud Configuration
+
+ Configure top-level cloud settings and domains
+
+
+
+
+
+ {/* Domains Section */}
+
+
+
+
Domain Configuration
+
+ Public and internal domain settings
+
+
+
+
+
+
+ {!editingDomains && (
+
+
+ Edit
+
+ )}
+
+
+
+ {editingDomains ? (
+
+
+ Public Domain
+ setTempDomain(e.target.value)}
+ placeholder="example.com"
+ className="mt-1"
+ />
+
+
+ Internal Domain
+ setTempInternalDomain(e.target.value)}
+ placeholder="internal.example.com"
+ className="mt-1"
+ />
+
+
+
+
+ Save
+
+
+
+ Cancel
+
+
+
+ ) : (
+
+
+
Public Domain
+
+ {domainValue}
+
+
+
+
Internal Domain
+
+ {internalDomainValue}
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/experimental/app/src/components/ClusterNodesComponent.tsx b/experimental/app/src/components/ClusterNodesComponent.tsx
new file mode 100644
index 0000000..2d7165d
--- /dev/null
+++ b/experimental/app/src/components/ClusterNodesComponent.tsx
@@ -0,0 +1,378 @@
+import { useState } from 'react';
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Badge } from './ui/badge';
+import { Cpu, HardDrive, Network, Monitor, Plus, CheckCircle, AlertCircle, Clock, BookOpen, ExternalLink } from 'lucide-react';
+
+interface ClusterNodesComponentProps {
+ onComplete?: () => void;
+}
+
+interface Node {
+ id: string;
+ name: string;
+ type: 'controller' | 'worker' | 'unassigned';
+ status: 'pending' | 'connecting' | 'connected' | 'healthy' | 'error';
+ ipAddress?: string;
+ macAddress: string;
+ osVersion?: string;
+ specs: {
+ cpu: string;
+ memory: string;
+ storage: string;
+ };
+}
+
+export function ClusterNodesComponent({ onComplete }: ClusterNodesComponentProps) {
+ const [currentOsVersion, setCurrentOsVersion] = useState('v13.0.5');
+ const [nodes, setNodes] = useState([
+ {
+ id: 'controller-1',
+ name: 'Controller Node 1',
+ type: 'controller',
+ status: 'healthy',
+ macAddress: '00:1A:2B:3C:4D:5E',
+ osVersion: 'v13.0.4',
+ specs: {
+ cpu: '4 cores',
+ memory: '8GB RAM',
+ storage: '120GB SSD',
+ },
+ },
+ {
+ id: 'worker-1',
+ name: 'Worker Node 1',
+ type: 'worker',
+ status: 'healthy',
+ macAddress: '00:1A:2B:3C:4D:5F',
+ osVersion: 'v13.0.5',
+ specs: {
+ cpu: '8 cores',
+ memory: '16GB RAM',
+ storage: '500GB SSD',
+ },
+ },
+ {
+ id: 'worker-2',
+ name: 'Worker Node 2',
+ type: 'worker',
+ status: 'healthy',
+ macAddress: '00:1A:2B:3C:4D:60',
+ osVersion: 'v13.0.4',
+ specs: {
+ cpu: '8 cores',
+ memory: '16GB RAM',
+ storage: '500GB SSD',
+ },
+ },
+ {
+ id: 'node-1',
+ name: 'Node 1',
+ type: 'unassigned',
+ status: 'pending',
+ macAddress: '00:1A:2B:3C:4D:5E',
+ osVersion: 'v13.0.5',
+ specs: {
+ cpu: '4 cores',
+ memory: '8GB RAM',
+ storage: '120GB SSD',
+ },
+ },
+ {
+ id: 'node-2',
+ name: 'Node 2',
+ type: 'unassigned',
+ status: 'pending',
+ macAddress: '00:1A:2B:3C:4D:5F',
+ osVersion: 'v13.0.5',
+ specs: {
+ cpu: '8 cores',
+ memory: '16GB RAM',
+ storage: '500GB SSD',
+ },
+ },
+ ]);
+
+ const getStatusIcon = (status: Node['status']) => {
+ switch (status) {
+ case 'connected':
+ return ;
+ case 'error':
+ return ;
+ case 'connecting':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusBadge = (status: Node['status']) => {
+ const variants = {
+ pending: 'secondary',
+ connecting: 'default',
+ connected: 'success',
+ healthy: 'success',
+ error: 'destructive',
+ } as const;
+
+ const labels = {
+ pending: 'Pending',
+ connecting: 'Connecting',
+ connected: 'Connected',
+ healthy: 'Healthy',
+ error: 'Error',
+ };
+
+ return (
+
+ {labels[status]}
+
+ );
+ };
+
+ const getTypeIcon = (type: Node['type']) => {
+ return type === 'controller' ? (
+
+ ) : (
+
+ );
+ };
+
+ const handleNodeAction = (nodeId: string, action: 'connect' | 'retry' | 'upgrade_node') => {
+ console.log(`${action} node: ${nodeId}`);
+ };
+
+ const connectedNodes = nodes.filter(node => node.status === 'connected').length;
+ const assignedNodes = nodes.filter(node => node.type !== 'unassigned');
+ const unassignedNodes = nodes.filter(node => node.type === 'unassigned');
+ const totalNodes = nodes.length;
+ const isComplete = connectedNodes === totalNodes;
+
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What are Cluster Nodes?
+
+
+ Think of cluster nodes as the "workers" in your personal cloud factory. Each node is a separate computer
+ that contributes its processing power, memory, and storage to the overall cluster. Some nodes are "controllers"
+ (like managers) that coordinate the work, while others are "workers" that do the heavy lifting.
+
+
+ By connecting multiple computers together as nodes, you create a powerful, resilient system where if one
+ computer fails, the others can pick up the work. This is how you scale your personal cloud from one machine to many.
+
+
+
+ Learn more about distributed computing
+
+
+
+
+
+
+
+
+
+
+
+
Cluster Nodes
+
+ Connect machines to your wild-cloud
+
+
+
+
+
+
Assigned Nodes ({assignedNodes.length}/{totalNodes})
+ {assignedNodes.map((node) => (
+
+
+
+ {getTypeIcon(node.type)}
+
+
+
+
{node.name}
+
+ {node.type}
+
+ {getStatusIcon(node.status)}
+
+
+ MAC: {node.macAddress}
+ {node.ipAddress && ` โข IP: ${node.ipAddress}`}
+
+
+
+
+ {node.specs.cpu}
+
+
+
+ {node.specs.memory}
+
+
+
+ {node.specs.storage}
+
+ {node.osVersion && (
+
+
+ OS: {node.osVersion}
+
+
+ )}
+
+
+
+ {getStatusBadge(node.status)}
+ {node.osVersion !== currentOsVersion && (
+ handleNodeAction(node.id, 'upgrade_node')}
+ >
+ Upgrade OS
+
+ )}
+ {node.status === 'error' && (
+ handleNodeAction(node.id, 'retry')}
+ >
+ Retry
+
+ )}
+
+
+
+ ))}
+
+
+ Unassigned Nodes ({unassignedNodes.length}/{totalNodes})
+
+ {unassignedNodes.map((node) => (
+
+
+
+ {getTypeIcon(node.type)}
+
+
+
+
{node.name}
+
+ {node.type}
+
+ {getStatusIcon(node.status)}
+
+
+ MAC: {node.macAddress}
+ {node.ipAddress && ` โข IP: ${node.ipAddress}`}
+
+
+
+
+ {node.specs.cpu}
+
+
+
+ {node.specs.memory}
+
+
+
+ {node.specs.storage}
+
+
+
+
+ {getStatusBadge(node.status)}
+ {node.status === 'pending' && (
+ handleNodeAction(node.id, 'connect')}
+ >
+ Assign
+
+ )}
+ {node.status === 'error' && (
+ handleNodeAction(node.id, 'retry')}
+ >
+ Retry
+
+ )}
+
+
+
+ ))}
+
+
+ {isComplete && (
+
+
+
+
+ Infrastructure Ready!
+
+
+
+ All nodes are connected and ready for Kubernetes installation.
+
+
+ Continue to Kubernetes Installation
+
+
+ )}
+
+
+
+ PXE Boot Instructions
+
+
+
+ 1
+
+
+
Power on your nodes
+
+ Ensure network boot (PXE) is enabled in BIOS/UEFI settings
+
+
+
+
+
+ 2
+
+
+
Connect to the wild-cloud network
+
+ Nodes will automatically receive IP addresses via DHCP
+
+
+
+
+
+ 3
+
+
+
Boot Talos Linux
+
+ Nodes will automatically download and boot Talos Linux via PXE
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/ClusterServicesComponent.tsx b/experimental/app/src/components/ClusterServicesComponent.tsx
new file mode 100644
index 0000000..786cb8b
--- /dev/null
+++ b/experimental/app/src/components/ClusterServicesComponent.tsx
@@ -0,0 +1,299 @@
+import { useState } from 'react';
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Badge } from './ui/badge';
+import { Container, Shield, Network, Database, CheckCircle, AlertCircle, Clock, Terminal, FileText, BookOpen, ExternalLink } from 'lucide-react';
+
+interface ClusterServicesComponentProps {
+ onComplete?: () => void;
+}
+
+interface ClusterComponent {
+ id: string;
+ name: string;
+ description: string;
+ status: 'pending' | 'installing' | 'ready' | 'error';
+ version?: string;
+ logs?: string[];
+}
+
+export function ClusterServicesComponent({ onComplete }: ClusterServicesComponentProps) {
+ const [components, setComponents] = useState([
+ {
+ id: 'talos-config',
+ name: 'Talos Configuration',
+ description: 'Generate and apply Talos cluster configuration',
+ status: 'pending',
+ },
+ {
+ id: 'kubernetes-bootstrap',
+ name: 'Kubernetes Bootstrap',
+ description: 'Initialize Kubernetes control plane',
+ status: 'pending',
+ version: 'v1.29.0',
+ },
+ {
+ id: 'cni-plugin',
+ name: 'Container Network Interface',
+ description: 'Install and configure Cilium CNI',
+ status: 'pending',
+ version: 'v1.14.5',
+ },
+ {
+ id: 'storage-class',
+ name: 'Storage Classes',
+ description: 'Configure persistent volume storage',
+ status: 'pending',
+ },
+ {
+ id: 'ingress-controller',
+ name: 'Ingress Controller',
+ description: 'Install Traefik ingress controller',
+ status: 'pending',
+ version: 'v3.0.0',
+ },
+ {
+ id: 'monitoring',
+ name: 'Cluster Monitoring',
+ description: 'Deploy Prometheus and Grafana stack',
+ status: 'pending',
+ },
+ ]);
+
+ const [showLogs, setShowLogs] = useState(null);
+
+ const getStatusIcon = (status: ClusterComponent['status']) => {
+ switch (status) {
+ case 'ready':
+ return ;
+ case 'error':
+ return ;
+ case 'installing':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const getStatusBadge = (status: ClusterComponent['status']) => {
+ const variants = {
+ pending: 'secondary',
+ installing: 'default',
+ ready: 'success',
+ error: 'destructive',
+ } as const;
+
+ const labels = {
+ pending: 'Pending',
+ installing: 'Installing',
+ ready: 'Ready',
+ error: 'Error',
+ };
+
+ return (
+
+ {labels[status]}
+
+ );
+ };
+
+ const getComponentIcon = (id: string) => {
+ switch (id) {
+ case 'talos-config':
+ return ;
+ case 'kubernetes-bootstrap':
+ return ;
+ case 'cni-plugin':
+ return ;
+ case 'storage-class':
+ return ;
+ case 'ingress-controller':
+ return ;
+ case 'monitoring':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const handleComponentAction = (componentId: string, action: 'install' | 'retry') => {
+ console.log(`${action} component: ${componentId}`);
+ };
+
+ const readyComponents = components.filter(component => component.status === 'ready').length;
+ const totalComponents = components.length;
+ const isComplete = readyComponents === totalComponents;
+
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What are Cluster Services?
+
+
+ Cluster services are like the "essential utilities" that make your personal cloud actually work. Just like a city
+ needs electricity, water, and roads, your cluster needs networking, storage, monitoring, and security services.
+ These services run automatically in the background to keep everything functioning smoothly.
+
+
+ Services like Kubernetes orchestration, container networking, ingress routing, and monitoring work together to
+ create a robust platform where you can easily deploy and manage your applications.
+
+
+
+ Learn more about Kubernetes services
+
+
+
+
+
+
+
+
+
+
+
+
Cluster Services
+
+ Install and configure essential cluster services
+
+
+
+
+
+
+ endpoint: civil
+ endpointIp: 192.168.8.240
+ kubernetes:
+ config: /home/payne/.kube/config
+ context: default
+ loadBalancerRange: 192.168.8.240-192.168.8.250
+ dashboard:
+ adminUsername: admin
+ certManager:
+ namespace: cert-manager
+ cloudflare:
+ domain: payne.io
+ ownerId: cloud-payne-io-cluster
+
+
+
+
+
+ {components.map((component) => (
+
+
+
+ {getComponentIcon(component.id)}
+
+
+
+
{component.name}
+ {component.version && (
+
+ {component.version}
+
+ )}
+ {getStatusIcon(component.status)}
+
+
{component.description}
+
+
+ {getStatusBadge(component.status)}
+ {(component.status === 'installing' || component.status === 'error') && (
+ setShowLogs(showLogs === component.id ? null : component.id)}
+ >
+
+ Logs
+
+ )}
+ {component.status === 'pending' && (
+ handleComponentAction(component.id, 'install')}
+ >
+ Install
+
+ )}
+ {component.status === 'error' && (
+ handleComponentAction(component.id, 'retry')}
+ >
+ Retry
+
+ )}
+
+
+
+ {showLogs === component.id && (
+
+
+
Installing {component.name}...
+
โ Checking prerequisites
+
โ Downloading manifests
+ {component.status === 'installing' && (
+
โณ Applying configuration...
+ )}
+ {component.status === 'error' && (
+
โ Installation failed: timeout waiting for pods
+ )}
+
+
+ )}
+
+ ))}
+
+
+ {isComplete && (
+
+
+
+
+ Kubernetes Cluster Ready!
+
+
+
+ Your Kubernetes cluster is fully configured and ready for application deployment.
+
+
+ Continue to App Management
+
+
+ )}
+
+
+
+ Cluster Information
+
+
+
Control Plane
+
+
โข API Server: https://cluster.wildcloud.local:6443
+
โข Nodes: 1 controller, 2 workers
+
โข Version: Kubernetes v1.29.0
+
+
+
+
Network Configuration
+
+
โข Pod CIDR: 10.244.0.0/16
+
โข Service CIDR: 10.96.0.0/12
+
โข CNI: Cilium v1.14.5
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/ConfigEditor.tsx b/experimental/app/src/components/ConfigEditor.tsx
new file mode 100644
index 0000000..1f41877
--- /dev/null
+++ b/experimental/app/src/components/ConfigEditor.tsx
@@ -0,0 +1,125 @@
+import { useState, useEffect } from 'react';
+import { Settings, Save, X } from 'lucide-react';
+import { useConfigYaml } from '../hooks';
+import { Button, Textarea } from './ui';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger} from '@/components/ui/dialog';
+
+export function ConfigEditor() {
+ const { yamlContent, isLoading, error, isEndpointMissing, updateYaml, refetch } = useConfigYaml();
+
+ const [editedContent, setEditedContent] = useState('');
+ const [hasChanges, setHasChanges] = useState(false);
+
+ // Update edited content when YAML content changes
+ useEffect(() => {
+ if (yamlContent) {
+ setEditedContent(yamlContent);
+ setHasChanges(false);
+ }
+ }, [yamlContent]);
+
+ // Track changes
+ useEffect(() => {
+ setHasChanges(editedContent !== yamlContent);
+ }, [editedContent, yamlContent]);
+
+ const handleSave = () => {
+ if (!hasChanges) return;
+
+ updateYaml(editedContent, {
+ onSuccess: () => {
+ setHasChanges(false);
+ },
+ onError: (err) => {
+ console.error('Failed to update config:', err);
+ }
+ });
+ };
+
+ const handleOpenChange = (open: boolean) => {
+ if (!open && hasChanges) {
+ if (!window.confirm('You have unsaved changes. Close anyway?')) {
+ return;
+ }
+ }
+ if (open) {
+ refetch();
+ }
+ };
+
+ return (
+
+
+
+
+ Config
+
+
+
+
+
+
+ Configuration Editor
+
+
+ Edit the raw YAML configuration file. This provides direct access to all configuration options.
+
+
+
+
+ {error && error instanceof Error && error.message && (
+
+
+ Error: {error.message}
+
+
+ )}
+
+ {isEndpointMissing && (
+
+
+ Backend endpoints missing. Raw YAML editing not available.
+
+
+ )}
+
+
+
+
+
+
+ Cancel
+
+
+
+ Update Config
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/ConfigurationForm.tsx b/experimental/app/src/components/ConfigurationForm.tsx
new file mode 100644
index 0000000..6df8f53
--- /dev/null
+++ b/experimental/app/src/components/ConfigurationForm.tsx
@@ -0,0 +1,297 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FileText, Check, AlertCircle, Loader2 } from 'lucide-react';
+import { useConfig, useMessages } from '../hooks';
+import { configFormSchema, defaultConfigValues, type ConfigFormData } from '../schemas/config';
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardContent,
+ Button,
+ Form,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ Input,
+} from './ui';
+
+export const ConfigurationForm = () => {
+ const {
+ config,
+ isConfigured,
+ showConfigSetup,
+ isLoading,
+ isCreating,
+ error,
+ createConfig,
+ refetch
+ } = useConfig();
+
+ const form = useForm({
+ resolver: zodResolver(configFormSchema),
+ defaultValues: defaultConfigValues,
+ });
+
+ const onSubmit = (data: ConfigFormData) => {
+ createConfig(data);
+ };
+
+ return (
+
+
+ Configuration (With Form Validation)
+
+
+ refetch()} disabled={isLoading} variant="outline">
+
+ {isLoading ? 'Loading...' : 'Reload Configuration'}
+
+
+ {error && (
+
+
+
+
Configuration Error
+
{error.message}
+
+
+ )}
+
+ {showConfigSetup && (
+
+
+
Initial Configuration Setup
+
Configure your wild-cloud central server settings with real-time validation.
+
+
+
+
+
+ )}
+
+ {config && isConfigured && (
+
+
+
+ โ Configuration loaded successfully
+
+
+
+ {JSON.stringify(config, null, 2)}
+
+
+ )}
+
+
+ Form Validation Status: {form.formState.isValid ? 'โ Valid' : 'โ Has Errors'} |
+ Errors: {Object.keys(form.formState.errors).length}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/ConfigurationSection.tsx b/experimental/app/src/components/ConfigurationSection.tsx
new file mode 100644
index 0000000..f9c51f2
--- /dev/null
+++ b/experimental/app/src/components/ConfigurationSection.tsx
@@ -0,0 +1,156 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FileText, Check, AlertCircle, Loader2 } from 'lucide-react';
+import { useConfig, useMessages } from '../hooks';
+import { configFormSchema, defaultConfigValues, type ConfigFormData } from '../schemas/config';
+import { Message } from './Message';
+import { Card, CardHeader, CardTitle, CardContent, Button, Form, FormField, FormItem, FormLabel, FormControl, FormMessage, Input } from './ui';
+
+export const ConfigurationSection = () => {
+ const {
+ config,
+ isConfigured,
+ showConfigSetup,
+ isLoading,
+ isCreating,
+ error,
+ createConfig,
+ refetch
+ } = useConfig();
+ const { messages } = useMessages();
+
+ const form = useForm({
+ resolver: zodResolver(configFormSchema),
+ defaultValues: defaultConfigValues,
+ });
+
+ const onSubmit = (data: ConfigFormData) => {
+ createConfig(data);
+ };
+
+ return (
+
+
+ Configuration
+
+
+ refetch()} disabled={isLoading} variant="outline">
+
+ {isLoading ? 'Loading...' : 'Reload Configuration'}
+
+
+ {error && (
+
+
+
+
Configuration Error
+
{error.message}
+
+
+ )}
+
+
+
+ {showConfigSetup && (
+
+
+
Initial Configuration Setup
+
Configure key settings for your wild-cloud central server:
+
+
+
+
+
+ )}
+
+ {config && isConfigured && (
+
+
+
+ โ Configuration loaded successfully
+
+
+
+ {JSON.stringify(config, null, 2)}
+
+
+ )}
+
+ {/* Debug info */}
+
+ React Query Status: isLoading={isLoading.toString()}, isConfigured={isConfigured.toString()}, showSetup={showConfigSetup.toString()}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/DhcpComponent.tsx b/experimental/app/src/components/DhcpComponent.tsx
new file mode 100644
index 0000000..2e10cd0
--- /dev/null
+++ b/experimental/app/src/components/DhcpComponent.tsx
@@ -0,0 +1,77 @@
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Wifi, HelpCircle, BookOpen, ExternalLink } from 'lucide-react';
+import { Input, Label } from './ui';
+
+export function DhcpComponent() {
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What is DHCP?
+
+
+ DHCP (Dynamic Host Configuration Protocol) is like an automatic "address assignment system" for your network.
+ When a device joins your network, DHCP automatically gives it an IP address, tells it how to connect to the internet,
+ and provides other network settings - no manual configuration needed!
+
+
+ Without DHCP, you'd need to manually assign IP addresses to every device. DHCP makes it so you can just connect
+ a phone, laptop, or smart device and it automatically gets everything it needs to work on your network.
+
+
+
+ Learn more about DHCP
+
+
+
+
+
+
+
+
+
+
+
+
DHCP Configuration
+
+ Manage DHCP settings and IP address allocation
+
+
+
+
+
+
+ Status:
+ Active
+
+
+
+
+
+ console.log('View DHCP clients')}>
+ View Clients
+
+ console.log('Configure DHCP')}>
+ Configure
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/DnsComponent.tsx b/experimental/app/src/components/DnsComponent.tsx
new file mode 100644
index 0000000..9915df7
--- /dev/null
+++ b/experimental/app/src/components/DnsComponent.tsx
@@ -0,0 +1,73 @@
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { Globe, CheckCircle, BookOpen, ExternalLink } from 'lucide-react';
+
+export function DnsComponent() {
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What is DNS?
+
+
+ DNS (Domain Name System) is like the "phone book" of the internet. Instead of remembering complex IP addresses
+ like "192.168.1.100", you can use friendly names like "my-server.local". When you type a name, DNS translates
+ it to the correct IP address so your devices can find each other.
+
+
+ Your personal cloud runs its own DNS service so devices can easily find services like "photos.home" or "media.local"
+ without needing to remember numbers.
+
+
+
+ Learn more about DNS
+
+
+
+
+
+
+
+
+
+
+
+
DNS Configuration
+
+ Manage DNS settings and domain resolution
+
+
+
+
+
+
+
+ Local resolution: Active
+
+
+
+
DNS Status
+
+ DNS service is running and resolving domains correctly.
+
+
+
+
+ console.log('Test DNS')}>
+ Test DNS
+
+ console.log('Configure DNS')}>
+ Configure
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/DnsmasqSection.tsx b/experimental/app/src/components/DnsmasqSection.tsx
new file mode 100644
index 0000000..2a51edf
--- /dev/null
+++ b/experimental/app/src/components/DnsmasqSection.tsx
@@ -0,0 +1,86 @@
+import { Settings, RotateCcw, AlertCircle } from 'lucide-react';
+import { useDnsmasq, useMessages } from '../hooks';
+import { Message } from './Message';
+import { Card, CardHeader, CardTitle, CardContent, Button } from './ui';
+
+export const DnsmasqSection = () => {
+ const {
+ dnsmasqConfig,
+ generateConfig,
+ isGenerating,
+ generateError,
+ restart,
+ isRestarting,
+ restartError,
+ restartData
+ } = useDnsmasq();
+ const { messages, setMessage } = useMessages();
+
+ // Handle success/error messaging
+ if (generateError) {
+ setMessage('dnsmasq', `Failed to generate dnsmasq config: ${generateError.message}`, 'error');
+ } else if (dnsmasqConfig) {
+ setMessage('dnsmasq', 'Dnsmasq config generated successfully', 'success');
+ }
+
+ if (restartError) {
+ setMessage('dnsmasq', `Failed to restart dnsmasq: ${restartError.message}`, 'error');
+ } else if (restartData) {
+ setMessage('dnsmasq', `Dnsmasq restart: ${restartData.status}`, 'success');
+ }
+ return (
+
+
+ DNS/DHCP Management
+
+
+
+ generateConfig()} disabled={isGenerating} variant="outline">
+
+ {isGenerating ? 'Generating...' : 'Generate Dnsmasq Config'}
+
+ restart()} disabled={isRestarting} variant="outline">
+
+ {isRestarting ? 'Restarting...' : 'Restart Dnsmasq'}
+
+
+
+ {generateError && (
+
+
+
+
Generation Error
+
{generateError.message}
+
+
+ )}
+
+ {restartError && (
+
+
+
+
Restart Error
+
{restartError.message}
+
+
+ )}
+
+ {restartData && (
+
+
+ โ Dnsmasq restart: {restartData.status}
+
+
+ )}
+
+
+
+ {dnsmasqConfig && (
+
+ {dnsmasqConfig}
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/ErrorBoundary.tsx b/experimental/app/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..47c3702
--- /dev/null
+++ b/experimental/app/src/components/ErrorBoundary.tsx
@@ -0,0 +1,166 @@
+import React, { Component as ReactComponent, ErrorInfo, ReactNode } from 'react';
+import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
+import { Button } from './ui/button';
+import { Card, CardHeader, CardTitle, CardContent } from './ui/card';
+
+interface Props {
+ children?: ReactNode;
+ fallback?: ReactNode;
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
+}
+
+interface State {
+ hasError: boolean;
+ error?: Error;
+ errorInfo?: ErrorInfo;
+}
+
+export class ErrorBoundary extends ReactComponent {
+ public state: State = {
+ hasError: false,
+ };
+
+ public static getDerivedStateFromError(error: Error): State {
+ // Update state so the next render will show the fallback UI
+ return { hasError: true, error };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
+
+ this.setState({
+ error,
+ errorInfo,
+ });
+
+ // Call optional error handler
+ if (this.props.onError) {
+ this.props.onError(error, errorInfo);
+ }
+ }
+
+ private handleReset = () => {
+ this.setState({ hasError: false, error: undefined, errorInfo: undefined });
+ };
+
+ private handleReload = () => {
+ window.location.reload();
+ };
+
+ public render() {
+ if (this.state.hasError) {
+ // If a custom fallback is provided, use it
+ if (this.props.fallback) {
+ return this.props.fallback;
+ }
+
+ // Default error UI
+ return ;
+ }
+
+ return this.props.children;
+ }
+}
+
+interface ErrorFallbackProps {
+ error?: Error;
+ errorInfo?: ErrorInfo;
+ onReset: () => void;
+ onReload: () => void;
+}
+
+export const ErrorFallback: React.FC = ({
+ error,
+ errorInfo,
+ onReset,
+ onReload
+}) => {
+ const isDev = process.env.NODE_ENV === 'development';
+
+ return (
+
+
+
+
+
+
+
+ Something went wrong
+
+
+ The application encountered an unexpected error
+
+
+
+
+
+
+
+ Don't worry, your data is safe. You can try the following options:
+
+
+
+
+ Try Again
+
+
+
+ Reload Page
+
+
+
+
+ {isDev && error && (
+
+
+ Error Details (Development Mode)
+
+
+
+
+
Error Message:
+
+ {error.message}
+
+
+
+ {error.stack && (
+
+
Stack Trace:
+
+ {error.stack}
+
+
+ )}
+
+ {errorInfo?.componentStack && (
+
+
Component Stack:
+
+ {errorInfo.componentStack}
+
+
+ )}
+
+
+ )}
+
+ {!isDev && (
+
+
+ If this problem persists, please contact support with details about what you were doing when the error occurred.
+
+
+ )}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/ErrorTester.tsx b/experimental/app/src/components/ErrorTester.tsx
new file mode 100644
index 0000000..cf5109d
--- /dev/null
+++ b/experimental/app/src/components/ErrorTester.tsx
@@ -0,0 +1,47 @@
+import { useState } from 'react';
+import { Button } from './ui/button';
+import { Card, CardHeader, CardTitle, CardContent } from './ui/card';
+import { AlertTriangle } from 'lucide-react';
+
+// Component that can trigger errors for testing
+export const ErrorTester = () => {
+ const [shouldThrow, setShouldThrow] = useState(false);
+
+ if (shouldThrow) {
+ throw new Error('Test error: This is a simulated component crash for testing the error boundary.');
+ }
+
+ return (
+
+
+
+
+ Error Boundary Tester
+
+
+
+
+ This component can be used to test the error boundary functionality in development.
+
+
+
+
+ โ ๏ธ Warning: Clicking the button below will intentionally crash this component to test error handling.
+
+
+
+ setShouldThrow(true)}
+ variant="destructive"
+ size="sm"
+ >
+ Trigger Error
+
+
+
+ Development tool - remove from production builds
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/Message.tsx b/experimental/app/src/components/Message.tsx
new file mode 100644
index 0000000..90adf53
--- /dev/null
+++ b/experimental/app/src/components/Message.tsx
@@ -0,0 +1,43 @@
+import { AlertCircle, CheckCircle, Info } from 'lucide-react';
+import type { Message as MessageType } from '../types';
+import { cn } from '@/lib/utils';
+
+interface MessageProps {
+ message?: MessageType;
+}
+
+export const Message = ({ message }: MessageProps) => {
+ if (!message) return null;
+
+ const getIcon = () => {
+ switch (message.type) {
+ case 'error':
+ return ;
+ case 'success':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getVariantStyles = () => {
+ switch (message.type) {
+ case 'error':
+ return 'border-destructive/50 text-destructive bg-destructive/10';
+ case 'success':
+ return 'border-green-500/50 text-green-700 bg-green-50 dark:bg-green-950 dark:text-green-400';
+ default:
+ return 'border-blue-500/50 text-blue-700 bg-blue-50 dark:bg-blue-950 dark:text-blue-400';
+ }
+ };
+
+ return (
+
+ {getIcon()}
+ {message.message}
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/PxeAssetsSection.tsx b/experimental/app/src/components/PxeAssetsSection.tsx
new file mode 100644
index 0000000..be35d16
--- /dev/null
+++ b/experimental/app/src/components/PxeAssetsSection.tsx
@@ -0,0 +1,49 @@
+import { Download, AlertCircle } from 'lucide-react';
+import { useAssets, useMessages } from '../hooks';
+import { Message } from './Message';
+import { Card, CardHeader, CardTitle, CardContent, Button } from './ui';
+
+export const PxeAssetsSection = () => {
+ const { downloadAssets, isDownloading, error, data } = useAssets();
+ const { messages, setMessage } = useMessages();
+
+ // Handle success/error messaging
+ if (error) {
+ setMessage('assets', `Failed to download assets: ${error.message}`, 'error');
+ } else if (data) {
+ setMessage('assets', `PXE Assets: ${data.status}`, 'success');
+ }
+ return (
+
+
+ PXE Boot Assets
+
+
+ downloadAssets()} disabled={isDownloading} variant="outline">
+
+ {isDownloading ? 'Downloading...' : 'Download/Update PXE Assets'}
+
+
+ {error && (
+
+
+
+
Download Error
+
{error.message}
+
+
+ )}
+
+ {data && (
+
+
+ โ PXE Assets: {data.status}
+
+
+ )}
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/PxeComponent.tsx b/experimental/app/src/components/PxeComponent.tsx
new file mode 100644
index 0000000..3a544f8
--- /dev/null
+++ b/experimental/app/src/components/PxeComponent.tsx
@@ -0,0 +1,73 @@
+import { Card } from './ui/card';
+import { Button } from './ui/button';
+import { HardDrive, BookOpen, ExternalLink } from 'lucide-react';
+
+export function PxeComponent() {
+ return (
+
+ {/* Educational Intro Section */}
+
+
+
+
+
+
+
+ What is PXE Boot?
+
+
+ PXE (Preboot Execution Environment) is like having a "network installer" that can set up computers without
+ needing USB drives or DVDs. When you turn on a computer, instead of booting from its hard drive, it can boot
+ from the network and automatically install an operating system or run diagnostics.
+
+
+ This is especially useful for setting up multiple computers in your cloud infrastructure. PXE can automatically
+ install and configure the same operating system on many machines, making it easy to expand your personal cloud.
+
+
+
+ Learn more about network booting
+
+
+
+
+
+
+
+
+
+
+
+
PXE Configuration
+
+ Manage PXE boot assets and network boot configuration
+
+
+
+
+
+
+ Status:
+ Active
+
+
+
+
Boot Assets
+
+ Manage Talos Linux boot images and iPXE configurations for network booting.
+
+
+
+
+ console.log('View assets')}>
+ View Assets
+
+ console.log('Download PXE assets')}>
+ Download Assets
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/StatusSection.tsx b/experimental/app/src/components/StatusSection.tsx
new file mode 100644
index 0000000..b59eb10
--- /dev/null
+++ b/experimental/app/src/components/StatusSection.tsx
@@ -0,0 +1,72 @@
+import { Server, RefreshCw } from 'lucide-react';
+import { useStatus } from '../hooks';
+import { Card, CardHeader, CardTitle, CardContent, Button } from './ui';
+
+export const StatusSection = () => {
+ const { data: status, isLoading, error, refetch } = useStatus();
+
+ return (
+
+
+
+
+ Server Status
+
+
+
+
+ Current Status
+ refetch()}
+ disabled={isLoading}
+ variant="outline"
+ size="sm"
+ >
+
+ {isLoading ? 'Refreshing...' : 'Refresh'}
+
+
+
+ {error && (
+
+
+ Failed to fetch status: {error.message}
+
+
+ )}
+
+ {status && (
+
+
+
+
Status
+
{status.status}
+
+
+
Version
+
{status.version}
+
+
+
+ {status.uptime && (
+
+
Uptime
+
{status.uptime}
+
+ )}
+
+
+ {JSON.stringify(status, null, 2)}
+
+
+ )}
+
+ {isLoading && !status && (
+
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/SystemStatus.tsx b/experimental/app/src/components/SystemStatus.tsx
new file mode 100644
index 0000000..d68f764
--- /dev/null
+++ b/experimental/app/src/components/SystemStatus.tsx
@@ -0,0 +1,94 @@
+import { RefreshCw, Activity, AlertCircle } from 'lucide-react';
+import { useStatus, useHealth, useMessages } from '../hooks';
+import { formatTimestamp } from '../utils/formatters';
+import { Message } from './Message';
+import { Card, CardHeader, CardTitle, CardContent, Button, Badge } from './ui';
+
+export const SystemStatus = () => {
+ const { data: status, isLoading: statusLoading, error: statusError, refetch } = useStatus();
+ const { mutate: checkHealth, isPending: healthLoading, error: healthError, data: healthData } = useHealth();
+ const { messages, setMessage } = useMessages();
+
+ // Handle health check messaging
+ if (healthError) {
+ setMessage('health', `Health check failed: ${healthError.message}`, 'error');
+ } else if (healthData) {
+ setMessage('health', `Service: ${healthData.service} - Status: ${healthData.status}`, 'success');
+ }
+ return (
+
+
+ System Status
+
+
+
+
refetch()} disabled={statusLoading} variant="outline">
+
+ {statusLoading ? 'Checking...' : 'Refresh Status'}
+
+
checkHealth()} disabled={healthLoading} variant="outline">
+
+ {healthLoading ? 'Checking...' : 'Check Health'}
+
+
+
+ {statusError && (
+
+
+
+
Status Error
+
{statusError.message}
+
+
+ )}
+
+ {healthError && (
+
+
+
+
Health Check Error
+
{healthError.message}
+
+
+ )}
+
+ {healthData && (
+
+
+ โ Service: {healthData.service} - Status: {healthData.status}
+
+
+ )}
+
+
+
+ {status && (
+
+
+
Status
+
+
+ {status.status}
+
+
+
+
Version
+
{status.version}
+
+
+
Uptime
+
{status.uptime}
+
+
+
Last Updated
+
{formatTimestamp(status.timestamp)}
+
+
+ )}
+
+
+ );
+};
\ No newline at end of file
diff --git a/experimental/app/src/components/ThemeToggle.tsx b/experimental/app/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..054d7cc
--- /dev/null
+++ b/experimental/app/src/components/ThemeToggle.tsx
@@ -0,0 +1,52 @@
+import { Moon, Sun, Monitor } from 'lucide-react';
+import { Button } from './ui/button';
+import { useTheme } from '../contexts/ThemeContext';
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ const cycleTheme = () => {
+ if (theme === 'light') {
+ setTheme('dark');
+ } else if (theme === 'dark') {
+ setTheme('system');
+ } else {
+ setTheme('light');
+ }
+ };
+
+ const getIcon = () => {
+ switch (theme) {
+ case 'light':
+ return ;
+ case 'dark':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getLabel = () => {
+ switch (theme) {
+ case 'light':
+ return 'Light mode';
+ case 'dark':
+ return 'Dark mode';
+ default:
+ return 'System theme';
+ }
+ };
+
+ return (
+
+ {getIcon()}
+ {getLabel()}
+
+ );
+}
\ No newline at end of file
diff --git a/experimental/app/src/components/index.ts b/experimental/app/src/components/index.ts
new file mode 100644
index 0000000..448d02d
--- /dev/null
+++ b/experimental/app/src/components/index.ts
@@ -0,0 +1,19 @@
+export { Message } from './Message';
+export { SystemStatus } from './SystemStatus';
+export { ConfigurationSection } from './ConfigurationSection';
+export { ConfigurationForm } from './ConfigurationForm';
+export { StatusSection } from './StatusSection';
+export { DnsmasqSection } from './DnsmasqSection';
+export { PxeAssetsSection } from './PxeAssetsSection';
+export { AppSidebar } from './AppSidebar';
+export { Advanced } from './Advanced';
+export { ConfigEditor } from './ConfigEditor';
+export { ErrorBoundary, ErrorFallback } from './ErrorBoundary';
+export { CloudComponent } from './CloudComponent';
+export { CentralComponent } from './CentralComponent';
+export { DnsComponent } from './DnsComponent';
+export { DhcpComponent } from './DhcpComponent';
+export { PxeComponent } from './PxeComponent';
+export { ClusterNodesComponent } from './ClusterNodesComponent';
+export { ClusterServicesComponent } from './ClusterServicesComponent';
+export { AppsComponent } from './AppsComponent';
\ No newline at end of file
diff --git a/experimental/app/src/components/ui/badge.tsx b/experimental/app/src/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/experimental/app/src/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/experimental/app/src/components/ui/button.tsx b/experimental/app/src/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/experimental/app/src/components/ui/button.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/experimental/app/src/components/ui/card.tsx b/experimental/app/src/components/ui/card.tsx
new file mode 100644
index 0000000..d05bbc6
--- /dev/null
+++ b/experimental/app/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/experimental/app/src/components/ui/collapsible.tsx b/experimental/app/src/components/ui/collapsible.tsx
new file mode 100644
index 0000000..77f86be
--- /dev/null
+++ b/experimental/app/src/components/ui/collapsible.tsx
@@ -0,0 +1,31 @@
+import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
+
+function Collapsible({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function CollapsibleTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function CollapsibleContent({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent }
diff --git a/experimental/app/src/components/ui/dialog.tsx b/experimental/app/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..6cb123b
--- /dev/null
+++ b/experimental/app/src/components/ui/dialog.tsx
@@ -0,0 +1,141 @@
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/experimental/app/src/components/ui/form.tsx b/experimental/app/src/components/ui/form.tsx
new file mode 100644
index 0000000..524b986
--- /dev/null
+++ b/experimental/app/src/components/ui/form.tsx
@@ -0,0 +1,167 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { Slot } from "@radix-ui/react-slot"
+import {
+ Controller,
+ FormProvider,
+ useFormContext,
+ useFormState,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormControl({ ...props }: React.ComponentProps) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message ?? "") : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/experimental/app/src/components/ui/index.ts b/experimental/app/src/components/ui/index.ts
new file mode 100644
index 0000000..92c0b04
--- /dev/null
+++ b/experimental/app/src/components/ui/index.ts
@@ -0,0 +1,27 @@
+export { Button, buttonVariants } from './button';
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './card';
+export { Badge, badgeVariants } from './badge';
+export { Input } from './input';
+export { Label } from './label';
+export { Textarea } from './textarea';
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogClose,
+ DialogTrigger,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+} from './dialog';
+export {
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+} from './form';
\ No newline at end of file
diff --git a/experimental/app/src/components/ui/input.tsx b/experimental/app/src/components/ui/input.tsx
new file mode 100644
index 0000000..03295ca
--- /dev/null
+++ b/experimental/app/src/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/experimental/app/src/components/ui/label.tsx b/experimental/app/src/components/ui/label.tsx
new file mode 100644
index 0000000..ef7133a
--- /dev/null
+++ b/experimental/app/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/experimental/app/src/components/ui/separator.tsx b/experimental/app/src/components/ui/separator.tsx
new file mode 100644
index 0000000..275381c
--- /dev/null
+++ b/experimental/app/src/components/ui/separator.tsx
@@ -0,0 +1,28 @@
+"use client"
+
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Separator }
diff --git a/experimental/app/src/components/ui/sheet.tsx b/experimental/app/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..6906f5b
--- /dev/null
+++ b/experimental/app/src/components/ui/sheet.tsx
@@ -0,0 +1,137 @@
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Sheet({ ...props }: React.ComponentProps) {
+ return
+}
+
+function SheetTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SheetOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetContent({
+ className,
+ children,
+ side = "right",
+ ...props
+}: React.ComponentProps & {
+ side?: "top" | "right" | "bottom" | "left"
+}) {
+ return (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+ )
+}
+
+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SheetTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SheetDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Sheet,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/experimental/app/src/components/ui/sidebar.tsx b/experimental/app/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..c948d1f
--- /dev/null
+++ b/experimental/app/src/components/ui/sidebar.tsx
@@ -0,0 +1,727 @@
+"use client"
+
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva } from "class-variance-authority"
+import type { VariantProps } from "class-variance-authority"
+import { PanelLeftIcon } from "lucide-react"
+
+import { useIsMobile } from "@/hooks/use-mobile"
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Separator } from "@/components/ui/separator"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import { Skeleton } from "@/components/ui/skeleton"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+const SIDEBAR_COOKIE_NAME = "sidebar_state"
+const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
+const SIDEBAR_WIDTH = "16rem"
+const SIDEBAR_WIDTH_MOBILE = "18rem"
+const SIDEBAR_WIDTH_ICON = "3rem"
+const SIDEBAR_KEYBOARD_SHORTCUT = "b"
+
+type SidebarContextProps = {
+ state: "expanded" | "collapsed"
+ open: boolean
+ setOpen: (open: boolean) => void
+ openMobile: boolean
+ setOpenMobile: (open: boolean) => void
+ isMobile: boolean
+ toggleSidebar: () => void
+}
+
+const SidebarContext = React.createContext(null)
+
+function useSidebar() {
+ const context = React.useContext(SidebarContext)
+ if (!context) {
+ throw new Error("useSidebar must be used within a SidebarProvider.")
+ }
+
+ return context
+}
+
+function SidebarProvider({
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+}) {
+ const isMobile = useIsMobile()
+ const [openMobile, setOpenMobile] = React.useState(false)
+
+ // This is the internal state of the sidebar.
+ // We use openProp and setOpenProp for control from outside the component.
+ const [_open, _setOpen] = React.useState(defaultOpen)
+ const open = openProp ?? _open
+ const setOpen = React.useCallback(
+ (value: boolean | ((value: boolean) => boolean)) => {
+ const openState = typeof value === "function" ? value(open) : value
+ if (setOpenProp) {
+ setOpenProp(openState)
+ } else {
+ _setOpen(openState)
+ }
+
+ // This sets the cookie to keep the sidebar state.
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
+ },
+ [setOpenProp, open]
+ )
+
+ // Helper to toggle the sidebar.
+ const toggleSidebar = React.useCallback(() => {
+ return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
+ }, [isMobile, setOpen, setOpenMobile])
+
+ // Adds a keyboard shortcut to toggle the sidebar.
+ React.useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
+ (event.metaKey || event.ctrlKey)
+ ) {
+ event.preventDefault()
+ toggleSidebar()
+ }
+ }
+
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [toggleSidebar])
+
+ // We add a state so that we can do data-state="expanded" or "collapsed".
+ // This makes it easier to style the sidebar with Tailwind classes.
+ const state = open ? "expanded" : "collapsed"
+
+ const contextValue = React.useMemo(
+ () => ({
+ state,
+ open,
+ setOpen,
+ isMobile,
+ openMobile,
+ setOpenMobile,
+ toggleSidebar,
+ }),
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
+ )
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function Sidebar({
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+}) {
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
+
+ if (collapsible === "none") {
+ return (
+
+ {children}
+
+ )
+ }
+
+ if (isMobile) {
+ return (
+
+
+
+ Sidebar
+ Displays the mobile sidebar.
+
+ {children}
+
+
+ )
+ }
+
+ return (
+
+ {/* This is what handles the sidebar gap on desktop */}
+
+
+
+ )
+}
+
+function SidebarTrigger({
+ className,
+ onClick,
+ ...props
+}: React.ComponentProps) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+}
+
+function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+}
+
+function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
+ return (
+
+ )
+}
+
+function SidebarInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarGroupLabel({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupAction({
+ className,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarGroupContent({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
+ },
+ size: {
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function SidebarMenuButton({
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+} & VariantProps) {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+}
+
+function SidebarMenuAction({
+ className,
+ asChild = false,
+ showOnHover = false,
+ ...props
+}: React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+}) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+ svg]:size-4 [&>svg]:shrink-0",
+ // Increases the hit area of the button on mobile.
+ "after:absolute after:-inset-2 md:after:hidden",
+ "peer-data-[size=sm]/menu-button:top-1",
+ "peer-data-[size=default]/menu-button:top-1.5",
+ "peer-data-[size=lg]/menu-button:top-2.5",
+ "group-data-[collapsible=icon]:hidden",
+ showOnHover &&
+ "peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function SidebarMenuBadge({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSkeleton({
+ className,
+ showIcon = false,
+ ...props
+}: React.ComponentProps<"div"> & {
+ showIcon?: boolean
+}) {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+}
+
+function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubItem({
+ className,
+ ...props
+}: React.ComponentProps<"li">) {
+ return (
+
+ )
+}
+
+function SidebarMenuSubButton({
+ asChild = false,
+ size = "md",
+ isActive = false,
+ className,
+ ...props
+}: React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+}) {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ size === "sm" && "text-xs",
+ size === "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupAction,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarInput,
+ SidebarInset,
+ SidebarMenu,
+ SidebarMenuAction,
+ SidebarMenuBadge,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSkeleton,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ SidebarProvider,
+ SidebarRail,
+ SidebarSeparator,
+ SidebarTrigger,
+ useSidebar,
+}
diff --git a/experimental/app/src/components/ui/skeleton.tsx b/experimental/app/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..32ea0ef
--- /dev/null
+++ b/experimental/app/src/components/ui/skeleton.tsx
@@ -0,0 +1,13 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/experimental/app/src/components/ui/textarea.tsx b/experimental/app/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/experimental/app/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/experimental/app/src/components/ui/tooltip.tsx b/experimental/app/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..71ee0fe
--- /dev/null
+++ b/experimental/app/src/components/ui/tooltip.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/experimental/app/src/contexts/ThemeContext.tsx b/experimental/app/src/contexts/ThemeContext.tsx
new file mode 100644
index 0000000..be42357
--- /dev/null
+++ b/experimental/app/src/contexts/ThemeContext.tsx
@@ -0,0 +1,73 @@
+import { createContext, useContext, useEffect, useState } from 'react';
+
+type Theme = 'dark' | 'light' | 'system';
+
+type ThemeProviderProps = {
+ children: React.ReactNode;
+ defaultTheme?: Theme;
+ storageKey?: string;
+};
+
+type ThemeProviderState = {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+};
+
+const initialState: ThemeProviderState = {
+ theme: 'system',
+ setTheme: () => null,
+};
+
+const ThemeProviderContext = createContext(initialState);
+
+export function ThemeProvider({
+ children,
+ defaultTheme = 'system',
+ storageKey = 'wild-central-theme',
+ ...props
+}: ThemeProviderProps) {
+ const [theme, setTheme] = useState(
+ () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
+ );
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ root.classList.remove('light', 'dark');
+
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
+ .matches
+ ? 'dark'
+ : 'light';
+
+ root.classList.add(systemTheme);
+ return;
+ }
+
+ root.classList.add(theme);
+ }, [theme]);
+
+ const value = {
+ theme,
+ setTheme: (theme: Theme) => {
+ localStorage.setItem(storageKey, theme);
+ setTheme(theme);
+ },
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+export const useTheme = () => {
+ const context = useContext(ThemeProviderContext);
+
+ if (context === undefined)
+ throw new Error('useTheme must be used within a ThemeProvider');
+
+ return context;
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/__tests__/useConfig.test.ts b/experimental/app/src/hooks/__tests__/useConfig.test.ts
new file mode 100644
index 0000000..186b9c8
--- /dev/null
+++ b/experimental/app/src/hooks/__tests__/useConfig.test.ts
@@ -0,0 +1,165 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor, act } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import React from 'react';
+import { useConfig } from '../useConfig';
+import { apiService } from '../../services/api';
+
+// Mock the API service
+vi.mock('../../services/api', () => ({
+ apiService: {
+ getConfig: vi.fn(),
+ createConfig: vi.fn(),
+ },
+}));
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) => (
+ React.createElement(QueryClientProvider, { client: queryClient }, children)
+ );
+};
+
+describe('useConfig', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should fetch config successfully when configured', async () => {
+ const mockConfigResponse = {
+ configured: true,
+ config: {
+ server: { host: '0.0.0.0', port: 5055 },
+ cloud: {
+ domain: 'wildcloud.local',
+ internalDomain: 'cluster.local',
+ dhcpRange: '192.168.8.100,192.168.8.200',
+ dns: { ip: '192.168.8.50' },
+ router: { ip: '192.168.8.1' },
+ dnsmasq: { interface: 'eth0' },
+ },
+ cluster: {
+ endpointIp: '192.168.8.60',
+ nodes: { talos: { version: 'v1.8.0' } },
+ },
+ },
+ };
+
+ vi.mocked(apiService.getConfig).mockResolvedValue(mockConfigResponse);
+
+ const { result } = renderHook(() => useConfig(), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.showConfigSetup).toBe(false);
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.config).toEqual(mockConfigResponse.config);
+ expect(result.current.isConfigured).toBe(true);
+ expect(result.current.showConfigSetup).toBe(false);
+ expect(result.current.error).toBeNull();
+ });
+
+ it('should show config setup when not configured', async () => {
+ const mockConfigResponse = {
+ configured: false,
+ message: 'No configuration found',
+ };
+
+ vi.mocked(apiService.getConfig).mockResolvedValue(mockConfigResponse);
+
+ const { result } = renderHook(() => useConfig(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.config).toBeNull();
+ expect(result.current.isConfigured).toBe(false);
+ expect(result.current.showConfigSetup).toBe(true);
+ });
+
+ it('should create config successfully', async () => {
+ const mockConfigResponse = {
+ configured: false,
+ message: 'No configuration found',
+ };
+
+ const mockCreateResponse = {
+ status: 'Configuration created successfully',
+ };
+
+ const newConfig = {
+ server: { host: '0.0.0.0', port: 5055 },
+ cloud: {
+ domain: 'wildcloud.local',
+ internalDomain: 'cluster.local',
+ dhcpRange: '192.168.8.100,192.168.8.200',
+ dns: { ip: '192.168.8.50' },
+ router: { ip: '192.168.8.1' },
+ dnsmasq: { interface: 'eth0' },
+ },
+ cluster: {
+ endpointIp: '192.168.8.60',
+ nodes: { talos: { version: 'v1.8.0' } },
+ },
+ };
+
+ vi.mocked(apiService.getConfig).mockResolvedValue(mockConfigResponse);
+ vi.mocked(apiService.createConfig).mockResolvedValue(mockCreateResponse);
+
+ const { result } = renderHook(() => useConfig(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.showConfigSetup).toBe(true);
+
+ // Create config
+ await act(async () => {
+ result.current.createConfig(newConfig);
+ });
+
+ await waitFor(() => {
+ expect(result.current.isCreating).toBe(false);
+ });
+
+ expect(apiService.createConfig).toHaveBeenCalledWith(newConfig);
+ });
+
+ it('should handle error when fetching config fails', async () => {
+ const mockError = new Error('Network error');
+ vi.mocked(apiService.getConfig).mockRejectedValue(mockError);
+
+ const { result } = renderHook(() => useConfig(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.error).toEqual(mockError);
+ expect(result.current.config).toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/experimental/app/src/hooks/__tests__/useMessages.test.ts b/experimental/app/src/hooks/__tests__/useMessages.test.ts
new file mode 100644
index 0000000..8c39c82
--- /dev/null
+++ b/experimental/app/src/hooks/__tests__/useMessages.test.ts
@@ -0,0 +1,127 @@
+import { describe, it, expect } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useMessages } from '../useMessages';
+
+describe('useMessages', () => {
+ it('should initialize with empty messages', () => {
+ const { result } = renderHook(() => useMessages());
+
+ expect(result.current.messages).toEqual({});
+ });
+
+ it('should set a message', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test', 'Test message', 'success');
+ });
+
+ expect(result.current.messages).toEqual({
+ test: { message: 'Test message', type: 'success' }
+ });
+ });
+
+ it('should set multiple messages', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('success', 'Success message', 'success');
+ result.current.setMessage('error', 'Error message', 'error');
+ result.current.setMessage('info', 'Info message', 'info');
+ });
+
+ expect(result.current.messages).toEqual({
+ success: { message: 'Success message', type: 'success' },
+ error: { message: 'Error message', type: 'error' },
+ info: { message: 'Info message', type: 'info' },
+ });
+ });
+
+ it('should update existing message', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test', 'First message', 'info');
+ });
+
+ expect(result.current.messages.test).toEqual({
+ message: 'First message',
+ type: 'info'
+ });
+
+ act(() => {
+ result.current.setMessage('test', 'Updated message', 'error');
+ });
+
+ expect(result.current.messages.test).toEqual({
+ message: 'Updated message',
+ type: 'error'
+ });
+ });
+
+ it('should clear a specific message', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test1', 'Message 1', 'info');
+ result.current.setMessage('test2', 'Message 2', 'success');
+ });
+
+ expect(Object.keys(result.current.messages)).toHaveLength(2);
+
+ act(() => {
+ result.current.clearMessage('test1');
+ });
+
+ expect(result.current.messages).toEqual({
+ test2: { message: 'Message 2', type: 'success' }
+ });
+ });
+
+ it('should clear message by setting to null', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test', 'Test message', 'info');
+ });
+
+ expect(result.current.messages.test).toBeDefined();
+
+ act(() => {
+ result.current.setMessage('test', null);
+ });
+
+ expect(result.current.messages.test).toBeUndefined();
+ });
+
+ it('should clear all messages', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test1', 'Message 1', 'info');
+ result.current.setMessage('test2', 'Message 2', 'success');
+ result.current.setMessage('test3', 'Message 3', 'error');
+ });
+
+ expect(Object.keys(result.current.messages)).toHaveLength(3);
+
+ act(() => {
+ result.current.clearAllMessages();
+ });
+
+ expect(result.current.messages).toEqual({});
+ });
+
+ it('should default to info type when type not specified', () => {
+ const { result } = renderHook(() => useMessages());
+
+ act(() => {
+ result.current.setMessage('test', 'Test message');
+ });
+
+ expect(result.current.messages.test).toEqual({
+ message: 'Test message',
+ type: 'info'
+ });
+ });
+});
\ No newline at end of file
diff --git a/experimental/app/src/hooks/__tests__/useStatus.test.ts b/experimental/app/src/hooks/__tests__/useStatus.test.ts
new file mode 100644
index 0000000..1564c06
--- /dev/null
+++ b/experimental/app/src/hooks/__tests__/useStatus.test.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import React from 'react';
+import { useStatus } from '../useStatus';
+import { apiService } from '../../services/api';
+
+// Mock the API service
+vi.mock('../../services/api', () => ({
+ apiService: {
+ getStatus: vi.fn(),
+ },
+}));
+
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) => (
+ React.createElement(QueryClientProvider, { client: queryClient }, children)
+ );
+};
+
+describe('useStatus', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should fetch status successfully', async () => {
+ const mockStatus = {
+ status: 'running',
+ version: '1.0.0',
+ uptime: '2 hours',
+ timestamp: '2024-01-01T00:00:00Z',
+ };
+
+ vi.mocked(apiService.getStatus).mockResolvedValue(mockStatus);
+
+ const { result } = renderHook(() => useStatus(), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(true);
+ expect(result.current.data).toBeUndefined();
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.data).toEqual(mockStatus);
+ expect(result.current.error).toBeNull();
+ expect(apiService.getStatus).toHaveBeenCalledTimes(1);
+ });
+
+ it('should handle error when fetching status fails', async () => {
+ const mockError = new Error('Network error');
+ vi.mocked(apiService.getStatus).mockRejectedValue(mockError);
+
+ const { result } = renderHook(() => useStatus(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(result.current.data).toBeUndefined();
+ expect(result.current.error).toEqual(mockError);
+ });
+
+ it('should refetch data when refetch is called', async () => {
+ const mockStatus = {
+ status: 'running',
+ version: '1.0.0',
+ uptime: '2 hours',
+ timestamp: '2024-01-01T00:00:00Z',
+ };
+
+ vi.mocked(apiService.getStatus).mockResolvedValue(mockStatus);
+
+ const { result } = renderHook(() => useStatus(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+
+ expect(apiService.getStatus).toHaveBeenCalledTimes(1);
+
+ // Trigger refetch
+ result.current.refetch();
+
+ await waitFor(() => {
+ expect(apiService.getStatus).toHaveBeenCalledTimes(2);
+ });
+ });
+});
\ No newline at end of file
diff --git a/experimental/app/src/hooks/index.ts b/experimental/app/src/hooks/index.ts
new file mode 100644
index 0000000..31d8dd5
--- /dev/null
+++ b/experimental/app/src/hooks/index.ts
@@ -0,0 +1,7 @@
+export { useMessages } from './useMessages';
+export { useStatus } from './useStatus';
+export { useHealth } from './useHealth';
+export { useConfig } from './useConfig';
+export { useConfigYaml } from './useConfigYaml';
+export { useDnsmasq } from './useDnsmasq';
+export { useAssets } from './useAssets';
\ No newline at end of file
diff --git a/experimental/app/src/hooks/use-mobile.ts b/experimental/app/src/hooks/use-mobile.ts
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/experimental/app/src/hooks/use-mobile.ts
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/experimental/app/src/hooks/useAssets.ts b/experimental/app/src/hooks/useAssets.ts
new file mode 100644
index 0000000..f3c3e5e
--- /dev/null
+++ b/experimental/app/src/hooks/useAssets.ts
@@ -0,0 +1,19 @@
+import { useMutation } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+
+interface AssetsResponse {
+ status: string;
+}
+
+export const useAssets = () => {
+ const downloadMutation = useMutation({
+ mutationFn: apiService.downloadPXEAssets,
+ });
+
+ return {
+ downloadAssets: downloadMutation.mutate,
+ isDownloading: downloadMutation.isPending,
+ error: downloadMutation.error,
+ data: downloadMutation.data,
+ };
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useConfig.ts b/experimental/app/src/hooks/useConfig.ts
new file mode 100644
index 0000000..01db5ed
--- /dev/null
+++ b/experimental/app/src/hooks/useConfig.ts
@@ -0,0 +1,52 @@
+import { useState, useEffect } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+import type { Config } from '../types';
+
+interface ConfigResponse {
+ configured: boolean;
+ config?: Config;
+ message?: string;
+}
+
+interface CreateConfigResponse {
+ status: string;
+}
+
+export const useConfig = () => {
+ const queryClient = useQueryClient();
+ const [showConfigSetup, setShowConfigSetup] = useState(false);
+
+ const configQuery = useQuery({
+ queryKey: ['config'],
+ queryFn: () => apiService.getConfig(),
+ });
+
+ // Update showConfigSetup based on query data
+ useEffect(() => {
+ if (configQuery.data) {
+ setShowConfigSetup(configQuery.data.configured === false);
+ }
+ }, [configQuery.data]);
+
+ const createConfigMutation = useMutation({
+ mutationFn: apiService.createConfig,
+ onSuccess: () => {
+ // Invalidate and refetch config after successful creation
+ queryClient.invalidateQueries({ queryKey: ['config'] });
+ setShowConfigSetup(false);
+ },
+ });
+
+ return {
+ config: configQuery.data?.config || null,
+ isConfigured: configQuery.data?.configured || false,
+ showConfigSetup,
+ setShowConfigSetup,
+ isLoading: configQuery.isLoading,
+ isCreating: createConfigMutation.isPending,
+ error: configQuery.error || createConfigMutation.error,
+ createConfig: createConfigMutation.mutate,
+ refetch: configQuery.refetch,
+ };
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useConfigYaml.ts b/experimental/app/src/hooks/useConfigYaml.ts
new file mode 100644
index 0000000..002e181
--- /dev/null
+++ b/experimental/app/src/hooks/useConfigYaml.ts
@@ -0,0 +1,40 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+
+export const useConfigYaml = () => {
+ const queryClient = useQueryClient();
+
+ const configYamlQuery = useQuery({
+ queryKey: ['config', 'yaml'],
+ queryFn: () => apiService.getConfigYaml(),
+ staleTime: 30000, // Consider data fresh for 30 seconds
+ retry: true,
+ });
+
+ const updateConfigYamlMutation = useMutation({
+ mutationFn: (data: string) => apiService.updateConfigYaml(data),
+ onSuccess: () => {
+ // Invalidate both YAML and JSON config queries
+ queryClient.invalidateQueries({ queryKey: ['config'] });
+ },
+ });
+
+ // Check if error is 404 (endpoint doesn't exist)
+ const isEndpointMissing = configYamlQuery.error &&
+ configYamlQuery.error instanceof Error &&
+ configYamlQuery.error.message.includes('404');
+
+ // Only pass through real errors
+ const actualError = (configYamlQuery.error instanceof Error ? configYamlQuery.error : null) ||
+ (updateConfigYamlMutation.error instanceof Error ? updateConfigYamlMutation.error : null);
+
+ return {
+ yamlContent: configYamlQuery.data || '',
+ isLoading: configYamlQuery.isLoading,
+ error: actualError,
+ isEndpointMissing,
+ isUpdating: updateConfigYamlMutation.isPending,
+ updateYaml: updateConfigYamlMutation.mutate,
+ refetch: configYamlQuery.refetch,
+ };
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useDnsmasq.ts b/experimental/app/src/hooks/useDnsmasq.ts
new file mode 100644
index 0000000..56fbb9e
--- /dev/null
+++ b/experimental/app/src/hooks/useDnsmasq.ts
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+
+interface DnsmasqResponse {
+ status: string;
+}
+
+export const useDnsmasq = () => {
+ const [dnsmasqConfig, setDnsmasqConfig] = useState('');
+
+ const generateConfigMutation = useMutation({
+ mutationFn: apiService.getDnsmasqConfig,
+ onSuccess: (data) => {
+ setDnsmasqConfig(data);
+ },
+ });
+
+ const restartMutation = useMutation({
+ mutationFn: apiService.restartDnsmasq,
+ });
+
+ return {
+ dnsmasqConfig,
+ generateConfig: generateConfigMutation.mutate,
+ isGenerating: generateConfigMutation.isPending,
+ generateError: generateConfigMutation.error,
+ restart: restartMutation.mutate,
+ isRestarting: restartMutation.isPending,
+ restartError: restartMutation.error,
+ restartData: restartMutation.data,
+ };
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useHealth.ts b/experimental/app/src/hooks/useHealth.ts
new file mode 100644
index 0000000..6037107
--- /dev/null
+++ b/experimental/app/src/hooks/useHealth.ts
@@ -0,0 +1,13 @@
+import { useMutation } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+
+interface HealthResponse {
+ service: string;
+ status: string;
+}
+
+export const useHealth = () => {
+ return useMutation({
+ mutationFn: apiService.getHealth,
+ });
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useMessages.ts b/experimental/app/src/hooks/useMessages.ts
new file mode 100644
index 0000000..a7bef13
--- /dev/null
+++ b/experimental/app/src/hooks/useMessages.ts
@@ -0,0 +1,33 @@
+import { useState } from 'react';
+import type { Messages } from '../types';
+
+export const useMessages = () => {
+ const [messages, setMessages] = useState({});
+
+ const setMessage = (key: string, message: string | null, type: 'info' | 'success' | 'error' = 'info') => {
+ if (message === null) {
+ setMessages(prev => {
+ const newMessages = { ...prev };
+ delete newMessages[key];
+ return newMessages;
+ });
+ } else {
+ setMessages(prev => ({ ...prev, [key]: { message, type } }));
+ }
+ };
+
+ const clearMessage = (key: string) => {
+ setMessage(key, null);
+ };
+
+ const clearAllMessages = () => {
+ setMessages({});
+ };
+
+ return {
+ messages,
+ setMessage,
+ clearMessage,
+ clearAllMessages,
+ };
+};
\ No newline at end of file
diff --git a/experimental/app/src/hooks/useStatus.ts b/experimental/app/src/hooks/useStatus.ts
new file mode 100644
index 0000000..fd5a3b0
--- /dev/null
+++ b/experimental/app/src/hooks/useStatus.ts
@@ -0,0 +1,11 @@
+import { useQuery } from '@tanstack/react-query';
+import { apiService } from '../services/api';
+import type { Status } from '../types';
+
+export const useStatus = () => {
+ return useQuery({
+ queryKey: ['status'],
+ queryFn: apiService.getStatus,
+ refetchInterval: 30000, // Refetch every 30 seconds
+ });
+};
\ No newline at end of file
diff --git a/experimental/app/src/index.css b/experimental/app/src/index.css
new file mode 100644
index 0000000..98de84c
--- /dev/null
+++ b/experimental/app/src/index.css
@@ -0,0 +1,120 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(50.59% 0.12582 244.557);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/experimental/app/src/lib/queryClient.ts b/experimental/app/src/lib/queryClient.ts
new file mode 100644
index 0000000..ee1fcd8
--- /dev/null
+++ b/experimental/app/src/lib/queryClient.ts
@@ -0,0 +1,15 @@
+import { QueryClient } from '@tanstack/react-query';
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ gcTime: 10 * 60 * 1000, // 10 minutes
+ refetchOnWindowFocus: false,
+ },
+ mutations: {
+ retry: 1,
+ },
+ },
+});
\ No newline at end of file
diff --git a/experimental/app/src/lib/utils.ts b/experimental/app/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/experimental/app/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/experimental/app/src/main.tsx b/experimental/app/src/main.tsx
new file mode 100644
index 0000000..7ba9fd1
--- /dev/null
+++ b/experimental/app/src/main.tsx
@@ -0,0 +1,24 @@
+import { StrictMode } from 'react';
+import ReactDOM from 'react-dom/client';
+import { QueryClientProvider } from '@tanstack/react-query';
+import './index.css';
+import App from './App';
+import { ThemeProvider } from './contexts/ThemeContext';
+import { queryClient } from './lib/queryClient';
+import { ErrorBoundary } from './components/ErrorBoundary';
+
+const root = ReactDOM.createRoot(
+ document.getElementById('root') as HTMLElement
+);
+
+root.render(
+
+
+
+
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/experimental/app/src/schemas/__tests__/config.test.ts b/experimental/app/src/schemas/__tests__/config.test.ts
new file mode 100644
index 0000000..1b49c6e
--- /dev/null
+++ b/experimental/app/src/schemas/__tests__/config.test.ts
@@ -0,0 +1,330 @@
+import { describe, it, expect } from 'vitest';
+import { configFormSchema, defaultConfigValues } from '../config';
+
+describe('config schema validation', () => {
+ describe('valid configurations', () => {
+ it('should validate default configuration', () => {
+ const result = configFormSchema.safeParse(defaultConfigValues);
+ expect(result.success).toBe(true);
+ });
+
+ it('should validate complete configuration', () => {
+ const validConfig = {
+ server: {
+ host: '0.0.0.0',
+ port: 5055,
+ },
+ cloud: {
+ domain: 'wildcloud.local',
+ internalDomain: 'cluster.local',
+ dhcpRange: '192.168.8.100,192.168.8.200',
+ dns: { ip: '192.168.8.50' },
+ router: { ip: '192.168.8.1' },
+ dnsmasq: { interface: 'eth0' },
+ },
+ cluster: {
+ endpointIp: '192.168.8.60',
+ nodes: { talos: { version: 'v1.8.0' } },
+ },
+ };
+
+ const result = configFormSchema.safeParse(validConfig);
+ expect(result.success).toBe(true);
+ });
+ });
+
+ describe('server validation', () => {
+ it('should reject empty host', () => {
+ const config = {
+ ...defaultConfigValues,
+ server: { ...defaultConfigValues.server, host: '' },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error.errors[0].path).toEqual(['server', 'host']);
+ expect(result.error.errors[0].message).toBe('Host is required');
+ }
+ });
+
+ it('should reject invalid port ranges', () => {
+ const invalidPorts = [0, -1, 65536, 99999];
+
+ invalidPorts.forEach(port => {
+ const config = {
+ ...defaultConfigValues,
+ server: { ...defaultConfigValues.server, port },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ });
+ });
+
+ it('should accept valid port ranges', () => {
+ const validPorts = [1, 80, 443, 5055, 65535];
+
+ validPorts.forEach(port => {
+ const config = {
+ ...defaultConfigValues,
+ server: { ...defaultConfigValues.server, port },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+
+ describe('IP address validation', () => {
+ it('should reject invalid IP addresses', () => {
+ const invalidIPs = [
+ '256.1.1.1',
+ '192.168.1',
+ '192.168.1.256',
+ 'not-an-ip',
+ '192.168.1.1.1',
+ '',
+ ];
+
+ invalidIPs.forEach(ip => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dns: { ip },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ });
+ });
+
+ it('should accept valid IP addresses', () => {
+ const validIPs = [
+ '192.168.1.1',
+ '10.0.0.1',
+ '172.16.0.1',
+ '127.0.0.1',
+ '0.0.0.0',
+ '255.255.255.255',
+ ];
+
+ validIPs.forEach(ip => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dns: { ip },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+
+ describe('domain validation', () => {
+ it('should reject invalid domains', () => {
+ const invalidDomains = [
+ '',
+ '.com',
+ 'domain.',
+ 'domain..com',
+ 'domain-.com',
+ '-domain.com',
+ 'domain.c',
+ 'very-long-domain-name-that-exceeds-the-maximum-allowed-length-for-a-domain-label.com',
+ ];
+
+ invalidDomains.forEach(domain => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ domain,
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success, `Domain "${domain}" should be invalid but passed validation`).toBe(false);
+ });
+ });
+
+ it('should accept valid domains', () => {
+ const validDomains = [
+ 'wildcloud.local',
+ 'example.com',
+ 'sub.domain.com',
+ 'localhost',
+ 'test123.example.org',
+ 'my-domain.net',
+ ];
+
+ validDomains.forEach(domain => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ domain,
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+
+ describe('DHCP range validation', () => {
+ it('should reject invalid DHCP ranges', () => {
+ const invalidRanges = [
+ '',
+ '192.168.1.1',
+ '192.168.1.1,',
+ ',192.168.1.200',
+ '192.168.1.1-192.168.1.200',
+ '192.168.1.1,192.168.1.256',
+ 'start,end',
+ ];
+
+ invalidRanges.forEach(dhcpRange => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dhcpRange,
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ });
+ });
+
+ it('should accept valid DHCP ranges', () => {
+ const validRanges = [
+ '192.168.1.100,192.168.1.200',
+ '10.0.0.10,10.0.0.100',
+ '172.16.1.1,172.16.1.254',
+ ];
+
+ validRanges.forEach(dhcpRange => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dhcpRange,
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+
+ describe('version validation', () => {
+ it('should reject invalid versions', () => {
+ const invalidVersions = [
+ '',
+ '1.8.0',
+ 'v1.8',
+ 'v1.8.0.1',
+ 'version1.8.0',
+ 'v1.8.0-beta',
+ ];
+
+ invalidVersions.forEach(version => {
+ const config = {
+ ...defaultConfigValues,
+ cluster: {
+ ...defaultConfigValues.cluster,
+ nodes: {
+ talos: { version },
+ },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ });
+ });
+
+ it('should accept valid versions', () => {
+ const validVersions = [
+ 'v1.8.0',
+ 'v1.0.0',
+ 'v10.20.30',
+ 'v0.0.1',
+ ];
+
+ validVersions.forEach(version => {
+ const config = {
+ ...defaultConfigValues,
+ cluster: {
+ ...defaultConfigValues.cluster,
+ nodes: {
+ talos: { version },
+ },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+
+ describe('network interface validation', () => {
+ it('should reject invalid interfaces', () => {
+ const invalidInterfaces = [
+ '',
+ 'eth-0',
+ 'eth.0',
+ 'eth 0',
+ 'eth/0',
+ ];
+
+ invalidInterfaces.forEach(interfaceName => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dnsmasq: { interface: interfaceName },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(false);
+ });
+ });
+
+ it('should accept valid interfaces', () => {
+ const validInterfaces = [
+ 'eth0',
+ 'eth1',
+ 'enp0s3',
+ 'wlan0',
+ 'lo',
+ 'br0',
+ ];
+
+ validInterfaces.forEach(interfaceName => {
+ const config = {
+ ...defaultConfigValues,
+ cloud: {
+ ...defaultConfigValues.cloud,
+ dnsmasq: { interface: interfaceName },
+ },
+ };
+
+ const result = configFormSchema.safeParse(config);
+ expect(result.success).toBe(true);
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/experimental/app/src/schemas/config.ts b/experimental/app/src/schemas/config.ts
new file mode 100644
index 0000000..bdb99ab
--- /dev/null
+++ b/experimental/app/src/schemas/config.ts
@@ -0,0 +1,184 @@
+import { z } from 'zod';
+
+// Network validation helpers
+const ipAddressSchema = z.string().regex(
+ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
+ 'Must be a valid IP address'
+);
+
+const domainSchema = z.string().regex(
+ /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$/,
+ 'Must be a valid domain name'
+);
+
+const dhcpRangeSchema = z.string().regex(
+ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?),(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
+ 'Must be in format: start_ip,end_ip (e.g., 192.168.1.100,192.168.1.200)'
+);
+
+const interfaceSchema = z.string().regex(
+ /^[a-zA-Z0-9]+$/,
+ 'Must be a valid network interface name (e.g., eth0, enp0s3)'
+);
+
+const versionSchema = z.string().regex(
+ /^v\d+\.\d+\.\d+$/,
+ 'Must be a valid version format (e.g., v1.8.0)'
+);
+
+// Server configuration schema
+const serverConfigSchema = z.object({
+ host: z.string().min(1, 'Host is required').default('0.0.0.0'),
+ port: z.number()
+ .int('Port must be an integer')
+ .min(1, 'Port must be at least 1')
+ .max(65535, 'Port must be at most 65535')
+ .default(5055),
+});
+
+// Cloud DNS configuration schema
+const cloudDnsSchema = z.object({
+ ip: ipAddressSchema,
+});
+
+// Cloud router configuration schema
+const cloudRouterSchema = z.object({
+ ip: ipAddressSchema,
+});
+
+// Cloud dnsmasq configuration schema
+const cloudDnsmasqSchema = z.object({
+ interface: interfaceSchema,
+});
+
+// Cloud configuration schema
+const cloudConfigSchema = z.object({
+ domain: domainSchema,
+ internalDomain: domainSchema,
+ dhcpRange: dhcpRangeSchema,
+ dns: cloudDnsSchema,
+ router: cloudRouterSchema,
+ dnsmasq: cloudDnsmasqSchema,
+});
+
+// Talos configuration schema
+const talosConfigSchema = z.object({
+ version: versionSchema,
+});
+
+// Nodes configuration schema
+const nodesConfigSchema = z.object({
+ talos: talosConfigSchema,
+});
+
+// Cluster configuration schema
+const clusterConfigSchema = z.object({
+ endpointIp: ipAddressSchema,
+ nodes: nodesConfigSchema,
+});
+
+// Wildcloud configuration schema (optional)
+const wildcloudConfigSchema = z.object({
+ repository: z.string().min(1, 'Repository is required'),
+ currentPhase: z.enum(['setup', 'infrastructure', 'cluster', 'apps']).optional(),
+ completedPhases: z.array(z.enum(['setup', 'infrastructure', 'cluster', 'apps'])).optional(),
+}).optional();
+
+// Main configuration schema
+export const configSchema = z.object({
+ server: serverConfigSchema,
+ cloud: cloudConfigSchema,
+ cluster: clusterConfigSchema,
+ wildcloud: wildcloudConfigSchema,
+});
+
+// Form schema for creating new configurations (some fields can be optional for partial updates)
+export const configFormSchema = z.object({
+ server: z.object({
+ host: z.string().min(1, 'Host is required'),
+ port: z.coerce.number()
+ .int('Port must be an integer')
+ .min(1, 'Port must be at least 1')
+ .max(65535, 'Port must be at most 65535'),
+ }),
+ cloud: z.object({
+ domain: z.string().min(1, 'Domain is required').refine(
+ (val) => domainSchema.safeParse(val).success,
+ 'Must be a valid domain name'
+ ),
+ internalDomain: z.string().min(1, 'Internal domain is required').refine(
+ (val) => domainSchema.safeParse(val).success,
+ 'Must be a valid domain name'
+ ),
+ dhcpRange: z.string().min(1, 'DHCP range is required').refine(
+ (val) => dhcpRangeSchema.safeParse(val).success,
+ 'Must be in format: start_ip,end_ip'
+ ),
+ dns: z.object({
+ ip: z.string().min(1, 'DNS IP is required').refine(
+ (val) => ipAddressSchema.safeParse(val).success,
+ 'Must be a valid IP address'
+ ),
+ }),
+ router: z.object({
+ ip: z.string().min(1, 'Router IP is required').refine(
+ (val) => ipAddressSchema.safeParse(val).success,
+ 'Must be a valid IP address'
+ ),
+ }),
+ dnsmasq: z.object({
+ interface: z.string().min(1, 'Interface is required').refine(
+ (val) => interfaceSchema.safeParse(val).success,
+ 'Must be a valid network interface name'
+ ),
+ }),
+ }),
+ cluster: z.object({
+ endpointIp: z.string().min(1, 'Endpoint IP is required').refine(
+ (val) => ipAddressSchema.safeParse(val).success,
+ 'Must be a valid IP address'
+ ),
+ nodes: z.object({
+ talos: z.object({
+ version: z.string().min(1, 'Talos version is required').refine(
+ (val) => versionSchema.safeParse(val).success,
+ 'Must be a valid version format (e.g., v1.8.0)'
+ ),
+ }),
+ }),
+ }),
+});
+
+// Type exports
+export type Config = z.infer;
+export type ConfigFormData = z.infer;
+
+// Default values for the form
+export const defaultConfigValues: ConfigFormData = {
+ server: {
+ host: '0.0.0.0',
+ port: 5055,
+ },
+ cloud: {
+ domain: 'wildcloud.local',
+ internalDomain: 'cluster.local',
+ dhcpRange: '192.168.8.100,192.168.8.200',
+ dns: {
+ ip: '192.168.8.50',
+ },
+ router: {
+ ip: '192.168.8.1',
+ },
+ dnsmasq: {
+ interface: 'eth0',
+ },
+ },
+ cluster: {
+ endpointIp: '192.168.8.60',
+ nodes: {
+ talos: {
+ version: 'v1.8.0',
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/experimental/app/src/services/api.ts b/experimental/app/src/services/api.ts
new file mode 100644
index 0000000..3c69b0d
--- /dev/null
+++ b/experimental/app/src/services/api.ts
@@ -0,0 +1,92 @@
+import type { Status, ConfigResponse, Config, HealthResponse, StatusResponse } from '../types';
+
+const API_BASE = 'http://localhost:5055';
+
+class ApiService {
+ private baseUrl: string;
+
+ constructor(baseUrl: string = API_BASE) {
+ this.baseUrl = baseUrl;
+ }
+
+ private async request(endpoint: string, options?: RequestInit): Promise {
+ const url = `${this.baseUrl}${endpoint}`;
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.json();
+ }
+
+ private async requestText(endpoint: string, options?: RequestInit): Promise {
+ const url = `${this.baseUrl}${endpoint}`;
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return response.text();
+ }
+
+ async getStatus(): Promise {
+ return this.request('/api/status');
+ }
+
+ async getHealth(): Promise {
+ return this.request('/api/v1/health');
+ }
+
+ async getConfig(): Promise {
+ return this.request('/api/v1/config');
+ }
+
+ async getConfigYaml(): Promise {
+ return this.requestText('/api/v1/config/yaml');
+ }
+
+ async updateConfigYaml(yamlContent: string): Promise {
+ return this.request('/api/v1/config/yaml', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'text/plain' },
+ body: yamlContent
+ });
+ }
+
+ async createConfig(config: Config): Promise {
+ return this.request('/api/v1/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(config)
+ });
+ }
+
+ async updateConfig(config: Config): Promise {
+ return this.request('/api/v1/config', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(config)
+ });
+ }
+
+ async getDnsmasqConfig(): Promise {
+ return this.requestText('/api/v1/dnsmasq/config');
+ }
+
+ async restartDnsmasq(): Promise {
+ return this.request('/api/v1/dnsmasq/restart', {
+ method: 'POST'
+ });
+ }
+
+ async downloadPXEAssets(): Promise {
+ return this.request('/api/v1/pxe/assets', {
+ method: 'POST'
+ });
+ }
+}
+
+export const apiService = new ApiService();
+export default ApiService;
\ No newline at end of file
diff --git a/experimental/app/src/test/setup.ts b/experimental/app/src/test/setup.ts
new file mode 100644
index 0000000..eccd9d7
--- /dev/null
+++ b/experimental/app/src/test/setup.ts
@@ -0,0 +1,23 @@
+import '@testing-library/jest-dom/vitest';
+
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(), // deprecated
+ removeListener: vi.fn(), // deprecated
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+
+// Mock ResizeObserver
+global.ResizeObserver = vi.fn().mockImplementation(() => ({
+ observe: vi.fn(),
+ unobserve: vi.fn(),
+ disconnect: vi.fn(),
+}));
\ No newline at end of file
diff --git a/experimental/app/src/test/test-utils.tsx b/experimental/app/src/test/test-utils.tsx
new file mode 100644
index 0000000..927e3df
--- /dev/null
+++ b/experimental/app/src/test/test-utils.tsx
@@ -0,0 +1,35 @@
+import React, { ReactElement } from 'react';
+import { render, RenderOptions } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ThemeProvider } from '../contexts/ThemeContext';
+
+// Custom render function that includes providers
+const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ });
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const customRender = (
+ ui: ReactElement,
+ options?: Omit,
+) => render(ui, { wrapper: AllTheProviders, ...options });
+
+export * from '@testing-library/react';
+export { customRender as render };
\ No newline at end of file
diff --git a/experimental/app/src/types/index.ts b/experimental/app/src/types/index.ts
new file mode 100644
index 0000000..a185b64
--- /dev/null
+++ b/experimental/app/src/types/index.ts
@@ -0,0 +1,86 @@
+export interface Status {
+ status: string;
+ version: string;
+ uptime: string;
+ timestamp: string;
+}
+
+export interface ServerConfig {
+ host: string;
+ port: number;
+}
+
+export interface CloudDns {
+ ip: string;
+}
+
+export interface CloudRouter {
+ ip: string;
+}
+
+export interface CloudDnsmasq {
+ interface: string;
+}
+
+export interface CloudConfig {
+ domain: string;
+ internalDomain: string;
+ dhcpRange: string;
+ dns: CloudDns;
+ router: CloudRouter;
+ dnsmasq: CloudDnsmasq;
+}
+
+export interface TalosConfig {
+ version: string;
+}
+
+export interface NodesConfig {
+ talos: TalosConfig;
+}
+
+export interface ClusterConfig {
+ endpointIp: string;
+ nodes: NodesConfig;
+}
+
+export interface WildcloudConfig {
+ repository: string;
+ currentPhase?: 'setup' | 'infrastructure' | 'cluster' | 'apps';
+ completedPhases?: ('setup' | 'infrastructure' | 'cluster' | 'apps')[];
+}
+
+export interface Config {
+ server: ServerConfig;
+ cloud: CloudConfig;
+ cluster: ClusterConfig;
+ wildcloud?: WildcloudConfig;
+}
+
+export interface ConfigResponse {
+ configured: boolean;
+ config?: Config;
+ message?: string;
+}
+
+export interface Message {
+ message: string;
+ type: 'info' | 'success' | 'error';
+}
+
+export interface LoadingState {
+ [key: string]: boolean;
+}
+
+export interface Messages {
+ [key: string]: Message;
+}
+
+export interface HealthResponse {
+ service: string;
+ status: string;
+}
+
+export interface StatusResponse {
+ status: string;
+}
\ No newline at end of file
diff --git a/experimental/app/src/utils/formatters.ts b/experimental/app/src/utils/formatters.ts
new file mode 100644
index 0000000..6028b36
--- /dev/null
+++ b/experimental/app/src/utils/formatters.ts
@@ -0,0 +1,3 @@
+export const formatTimestamp = (timestamp: string): string => {
+ return new Date(timestamp).toLocaleString();
+};
\ No newline at end of file
diff --git a/experimental/app/src/utils/yamlParser.ts b/experimental/app/src/utils/yamlParser.ts
new file mode 100644
index 0000000..7042f3c
--- /dev/null
+++ b/experimental/app/src/utils/yamlParser.ts
@@ -0,0 +1,61 @@
+import { Config } from '../types';
+
+// Simple YAML to JSON parser for basic configuration
+export const parseSimpleYaml = (yamlText: string): Config => {
+ const config: Config = {
+ cloud: {
+ domain: '',
+ internalDomain: '',
+ dhcpRange: '',
+ dns: { ip: '' },
+ router: { ip: '' },
+ dnsmasq: { interface: '' }
+ },
+ cluster: {
+ endpointIp: '',
+ nodes: { talos: { version: '' } }
+ },
+ server: { host: '', port: 0 }
+ };
+
+ const lines = yamlText.split('\n');
+ let currentSection: 'cloud' | 'cluster' | 'server' | null = null;
+ let currentSubsection: string | null = null;
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) continue;
+
+ if (trimmed.startsWith('cloud:')) currentSection = 'cloud';
+ else if (trimmed.startsWith('cluster:')) currentSection = 'cluster';
+ else if (trimmed.startsWith('server:')) currentSection = 'server';
+ else if (trimmed.startsWith('dns:')) currentSubsection = 'dns';
+ else if (trimmed.startsWith('router:')) currentSubsection = 'router';
+ else if (trimmed.startsWith('dnsmasq:')) currentSubsection = 'dnsmasq';
+ else if (trimmed.startsWith('nodes:')) currentSubsection = 'nodes';
+ else if (trimmed.startsWith('talos:')) currentSubsection = 'talos';
+ else if (trimmed.includes(':')) {
+ const [key, value] = trimmed.split(':').map(s => s.trim());
+ const cleanValue = value.replace(/"/g, '');
+
+ if (currentSection === 'cloud') {
+ if (currentSubsection === 'dns') (config.cloud.dns as any)[key] = cleanValue;
+ else if (currentSubsection === 'router') (config.cloud.router as any)[key] = cleanValue;
+ else if (currentSubsection === 'dnsmasq') (config.cloud.dnsmasq as any)[key] = cleanValue;
+ else (config.cloud as any)[key] = cleanValue;
+ } else if (currentSection === 'cluster') {
+ if (currentSubsection === 'nodes') {
+ // Skip nodes level
+ } else if (currentSubsection === 'talos') {
+ (config.cluster.nodes.talos as any)[key] = cleanValue;
+ } else {
+ (config.cluster as any)[key] = cleanValue;
+ }
+ } else if (currentSection === 'server') {
+ (config.server as any)[key] = key === 'port' ? parseInt(cleanValue) : cleanValue;
+ }
+ }
+ }
+
+ return config;
+};
\ No newline at end of file
diff --git a/experimental/app/src/vite-env.d.ts b/experimental/app/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/experimental/app/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/experimental/app/tsconfig.app.json b/experimental/app/tsconfig.app.json
new file mode 100644
index 0000000..3d79573
--- /dev/null
+++ b/experimental/app/tsconfig.app.json
@@ -0,0 +1,37 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": [
+ "ES2020",
+ "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,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/experimental/app/tsconfig.json b/experimental/app/tsconfig.json
new file mode 100644
index 0000000..20ff94b
--- /dev/null
+++ b/experimental/app/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "files": [],
+ "references": [
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/experimental/app/tsconfig.node.json b/experimental/app/tsconfig.node.json
new file mode 100644
index 0000000..9728af2
--- /dev/null
+++ b/experimental/app/tsconfig.node.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2022",
+ "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"]
+}
diff --git a/experimental/app/vite.config.ts b/experimental/app/vite.config.ts
new file mode 100644
index 0000000..38a0f37
--- /dev/null
+++ b/experimental/app/vite.config.ts
@@ -0,0 +1,14 @@
+import path from "path"
+import tailwindcss from "@tailwindcss/vite"
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+})
diff --git a/experimental/app/vitest.config.ts b/experimental/app/vitest.config.ts
new file mode 100644
index 0000000..112e798
--- /dev/null
+++ b/experimental/app/vitest.config.ts
@@ -0,0 +1,21 @@
+///
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import { resolve } from 'path';
+import { fileURLToPath, URL } from 'node:url';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": fileURLToPath(new URL('./src', import.meta.url)),
+ },
+ },
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./src/test/setup.ts'],
+ css: true,
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ },
+});
\ No newline at end of file
diff --git a/experimental/daemon/Makefile b/experimental/daemon/Makefile
new file mode 100644
index 0000000..7f2a26f
--- /dev/null
+++ b/experimental/daemon/Makefile
@@ -0,0 +1,90 @@
+# Default target
+.DEFAULT_GOAL := help
+
+# Build configuration
+BINARY_NAME := wild-api
+VERSION ?= 0.1.1
+BUILD_DIR := build
+
+# Go build configuration
+GO_VERSION := $(shell go version | cut -d' ' -f3)
+GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
+LDFLAGS := -X main.Version=$(VERSION) -X main.GitCommit=$(GIT_COMMIT) -X main.BuildTime=$(BUILD_TIME)
+
+.PHONY: help build clean test run install check fmt vet lint deps-check version
+
+# Usage: $(call package_deb,architecture,binary_name)
+help:
+ @echo "๐๏ธ Wild Cloud API Build System"
+ @echo ""
+ @echo "๐ฆ Build targets (compile binaries):"
+ @echo " build - Build for current architecture"
+ @echo ""
+ @echo "๐ Quality assurance:"
+ @echo " check - Run all checks (fmt + vet + test)"
+ @echo " fmt - Format Go code"
+ @echo " vet - Run go vet"
+ @echo " test - Run tests"
+ @echo ""
+ @echo "๐ ๏ธ Development:"
+ @echo " run - Run application locally"
+ @echo " clean - Remove all build artifacts"
+ @echo " deps-check - Verify and tidy dependencies"
+ @echo " version - Show build information"
+ @echo " install - Install to system"
+ @echo ""
+ @echo "๐ Directory structure:"
+ @echo " build/ - Intermediate build artifacts"
+
+build:
+ @echo "Building $(BINARY_NAME) for current architecture..."
+ @mkdir -p $(BUILD_DIR)
+ go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) .
+ @echo "โ
Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
+
+clean:
+ @echo "๐งน Cleaning build artifacts..."
+ @rm -rf $(BUILD_DIR) $(DIST_DIR) $(DEB_DIR)-* $(DEB_DIR)
+ @go clean
+ @echo "โ
Clean complete"
+
+test:
+ @echo "๐งช Running tests..."
+ @go test -v ./...
+
+run:
+ @echo "๐ Running $(BINARY_NAME)..."
+ @go run -ldflags="$(LDFLAGS)" .
+
+# Code quality targets
+fmt:
+ @echo "๐จ Formatting code..."
+ @go fmt ./...
+ @echo "โ
Format complete"
+
+vet:
+ @echo "๐ Running go vet..."
+ @go vet ./...
+ @echo "โ
Vet complete"
+
+check: fmt vet test
+ @echo "โ
All checks passed"
+
+# Dependency management
+deps-check:
+ @echo "๐ฆ Checking dependencies..."
+ @go mod verify
+ @go mod tidy
+ @echo "โ
Dependencies verified"
+
+# Version information
+version:
+ @echo "Version: $(VERSION)"
+ @echo "Git Commit: $(GIT_COMMIT)"
+ @echo "Build Time: $(BUILD_TIME)"
+ @echo "Go Version: $(GO_VERSION)"
+
+dev:
+ go run . &
+ echo "Server started on http://localhost:5055"
\ No newline at end of file
diff --git a/experimental/daemon/README.md b/experimental/daemon/README.md
new file mode 100644
index 0000000..30d6640
--- /dev/null
+++ b/experimental/daemon/README.md
@@ -0,0 +1 @@
+# Wild-cloud API Backend Service
diff --git a/experimental/daemon/build/wild-api b/experimental/daemon/build/wild-api
new file mode 100755
index 0000000..a163841
Binary files /dev/null and b/experimental/daemon/build/wild-api differ
diff --git a/experimental/daemon/go.mod b/experimental/daemon/go.mod
new file mode 100644
index 0000000..03b728e
--- /dev/null
+++ b/experimental/daemon/go.mod
@@ -0,0 +1,8 @@
+module wild-cloud-central
+
+go 1.21
+
+require (
+ github.com/gorilla/mux v1.8.1
+ gopkg.in/yaml.v3 v3.0.1
+)
diff --git a/experimental/daemon/go.sum b/experimental/daemon/go.sum
new file mode 100644
index 0000000..3e12cf5
--- /dev/null
+++ b/experimental/daemon/go.sum
@@ -0,0 +1,6 @@
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/experimental/daemon/internal/config/config.go b/experimental/daemon/internal/config/config.go
new file mode 100644
index 0000000..42e2dce
--- /dev/null
+++ b/experimental/daemon/internal/config/config.go
@@ -0,0 +1,94 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v3"
+)
+
+// Config represents the main configuration structure
+type Config struct {
+ Wildcloud struct {
+ Repository string `yaml:"repository" json:"repository"`
+ CurrentPhase string `yaml:"currentPhase" json:"currentPhase"`
+ CompletedPhases []string `yaml:"completedPhases" json:"completedPhases"`
+ } `yaml:"wildcloud" json:"wildcloud"`
+ Server struct {
+ Port int `yaml:"port" json:"port"`
+ Host string `yaml:"host" json:"host"`
+ } `yaml:"server" json:"server"`
+ Cloud struct {
+ Domain string `yaml:"domain" json:"domain"`
+ InternalDomain string `yaml:"internalDomain" json:"internalDomain"`
+ DNS struct {
+ IP string `yaml:"ip" json:"ip"`
+ } `yaml:"dns" json:"dns"`
+ Router struct {
+ IP string `yaml:"ip" json:"ip"`
+ } `yaml:"router" json:"router"`
+ DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"`
+ Dnsmasq struct {
+ Interface string `yaml:"interface" json:"interface"`
+ } `yaml:"dnsmasq" json:"dnsmasq"`
+ } `yaml:"cloud" json:"cloud"`
+ Cluster struct {
+ EndpointIP string `yaml:"endpointIp" json:"endpointIp"`
+ Nodes struct {
+ Talos struct {
+ Version string `yaml:"version" json:"version"`
+ } `yaml:"talos" json:"talos"`
+ } `yaml:"nodes" json:"nodes"`
+ } `yaml:"cluster" json:"cluster"`
+}
+
+// Load loads configuration from the specified path
+func Load(configPath string) (*Config, error) {
+ data, err := os.ReadFile(configPath)
+ if err != nil {
+ return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
+ }
+
+ config := &Config{}
+ if err := yaml.Unmarshal(data, config); err != nil {
+ return nil, fmt.Errorf("parsing config file: %w", err)
+ }
+
+ // Set defaults
+ if config.Server.Port == 0 {
+ config.Server.Port = 5055
+ }
+ if config.Server.Host == "" {
+ config.Server.Host = "0.0.0.0"
+ }
+
+ return config, nil
+}
+
+// Save saves the configuration to the specified path
+func Save(config *Config, configPath string) error {
+ // Ensure the directory exists
+ if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
+ return fmt.Errorf("creating config directory: %w", err)
+ }
+
+ data, err := yaml.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("marshaling config: %w", err)
+ }
+
+ return os.WriteFile(configPath, data, 0644)
+}
+
+// IsEmpty checks if the configuration is empty or uninitialized
+func (c *Config) IsEmpty() bool {
+ if c == nil {
+ return true
+ }
+
+ // Check if any essential fields are empty
+ return c.Cloud.Domain == "" ||
+ c.Cloud.DNS.IP == "" ||
+ c.Cluster.Nodes.Talos.Version == ""
+}
\ No newline at end of file
diff --git a/experimental/daemon/internal/data/paths.go b/experimental/daemon/internal/data/paths.go
new file mode 100644
index 0000000..94bed62
--- /dev/null
+++ b/experimental/daemon/internal/data/paths.go
@@ -0,0 +1,130 @@
+package data
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+// Paths represents the data directory paths configuration
+type Paths struct {
+ ConfigFile string
+ DataDir string
+ LogsDir string
+ AssetsDir string
+ DnsmasqConf string
+}
+
+// Manager handles data directory management
+type Manager struct {
+ dataDir string
+ isDev bool
+}
+
+// NewManager creates a new data manager
+func NewManager() *Manager {
+ return &Manager{}
+}
+
+// Initialize sets up the data directory structure
+func (m *Manager) Initialize() error {
+ // Detect environment: development vs production
+ m.isDev = m.isDevelopmentMode()
+
+ var dataDir string
+ if m.isDev {
+ // Development mode: use .wildcloud in current directory
+ cwd, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("failed to get current directory: %w", err)
+ }
+ dataDir = filepath.Join(cwd, ".wildcloud")
+ log.Printf("Running in development mode, using data directory: %s", dataDir)
+ } else {
+ // Production mode: use standard Linux directories
+ dataDir = "/var/lib/wild-cloud-central"
+ log.Printf("Running in production mode, using data directory: %s", dataDir)
+ }
+
+ m.dataDir = dataDir
+
+ // Create directory structure
+ paths := m.GetPaths()
+
+ // Create all necessary directories
+ for _, dir := range []string{paths.DataDir, paths.LogsDir, paths.AssetsDir} {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("failed to create directory %s: %w", dir, err)
+ }
+ }
+
+ log.Printf("Data directory structure initialized at: %s", dataDir)
+ return nil
+}
+
+// isDevelopmentMode detects if we're running in development mode
+func (m *Manager) isDevelopmentMode() bool {
+ // Check multiple indicators for development mode
+
+ // 1. Check if GO_ENV is set to development
+ if env := os.Getenv("GO_ENV"); env == "development" {
+ return true
+ }
+
+ // 2. Check if running as systemd service (has INVOCATION_ID)
+ if os.Getenv("INVOCATION_ID") != "" {
+ return false // Running under systemd
+ }
+
+ // 3. Check if running from a typical development location
+ if exe, err := os.Executable(); err == nil {
+ // If executable is in current directory or contains "wild-central" without being in /usr/bin
+ if strings.Contains(exe, "/usr/bin") || strings.Contains(exe, "/usr/local/bin") {
+ return false
+ }
+ if filepath.Base(exe) == "wild-central" && !strings.HasPrefix(exe, "/") {
+ return true
+ }
+ }
+
+ // 4. Check if we can write to /var/lib (if not, probably development)
+ if _, err := os.Stat("/var/lib"); err != nil {
+ return true
+ }
+
+ // 5. Default to development if uncertain
+ return true
+}
+
+// GetPaths returns the appropriate paths for the current environment
+func (m *Manager) GetPaths() Paths {
+ if m.isDev {
+ return Paths{
+ ConfigFile: filepath.Join(m.dataDir, "config.yaml"),
+ DataDir: m.dataDir,
+ LogsDir: filepath.Join(m.dataDir, "logs"),
+ AssetsDir: filepath.Join(m.dataDir, "assets"),
+ DnsmasqConf: filepath.Join(m.dataDir, "dnsmasq.conf"),
+ }
+ } else {
+ return Paths{
+ ConfigFile: "/etc/wild-cloud-central/config.yaml",
+ DataDir: m.dataDir,
+ LogsDir: "/var/log/wild-cloud-central",
+ AssetsDir: "/var/www/html/wild-central",
+ DnsmasqConf: "/etc/dnsmasq.conf",
+ }
+ }
+}
+
+// GetDataDir returns the current data directory
+func (m *Manager) GetDataDir() string {
+ return m.dataDir
+}
+
+// IsDevelopment returns true if running in development mode
+func (m *Manager) IsDevelopment() bool {
+ return m.isDev
+}
\ No newline at end of file
diff --git a/experimental/daemon/internal/dnsmasq/config.go b/experimental/daemon/internal/dnsmasq/config.go
new file mode 100644
index 0000000..5d27baa
--- /dev/null
+++ b/experimental/daemon/internal/dnsmasq/config.go
@@ -0,0 +1,97 @@
+package dnsmasq
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+
+ "wild-cloud-central/internal/config"
+)
+
+// ConfigGenerator handles dnsmasq configuration generation
+type ConfigGenerator struct{}
+
+// NewConfigGenerator creates a new dnsmasq config generator
+func NewConfigGenerator() *ConfigGenerator {
+ return &ConfigGenerator{}
+}
+
+// Generate creates a dnsmasq configuration from the app config
+func (g *ConfigGenerator) Generate(cfg *config.Config) string {
+ template := `# Configuration file for dnsmasq.
+
+# Basic Settings
+interface=%s
+listen-address=%s
+domain-needed
+bogus-priv
+no-resolv
+
+# DNS Local Resolution - Central server handles these domains authoritatively
+local=/%s/
+address=/%s/%s
+local=/%s/
+address=/%s/%s
+server=1.1.1.1
+server=8.8.8.8
+
+# --- DHCP Settings ---
+dhcp-range=%s,12h
+dhcp-option=3,%s
+dhcp-option=6,%s
+
+# --- PXE Booting ---
+enable-tftp
+tftp-root=/var/ftpd
+
+dhcp-match=set:efi-x86_64,option:client-arch,7
+dhcp-boot=tag:efi-x86_64,ipxe.efi
+dhcp-boot=tag:!efi-x86_64,undionly.kpxe
+
+dhcp-match=set:efi-arm64,option:client-arch,11
+dhcp-boot=tag:efi-arm64,ipxe-arm64.efi
+
+dhcp-userclass=set:ipxe,iPXE
+dhcp-boot=tag:ipxe,http://%s/boot.ipxe
+
+log-queries
+log-dhcp
+`
+
+ return fmt.Sprintf(template,
+ cfg.Cloud.Dnsmasq.Interface,
+ cfg.Cloud.DNS.IP,
+ cfg.Cloud.Domain,
+ cfg.Cloud.Domain,
+ cfg.Cluster.EndpointIP,
+ cfg.Cloud.InternalDomain,
+ cfg.Cloud.InternalDomain,
+ cfg.Cluster.EndpointIP,
+ cfg.Cloud.DHCPRange,
+ cfg.Cloud.Router.IP,
+ cfg.Cloud.DNS.IP,
+ cfg.Cloud.DNS.IP,
+ )
+}
+
+// WriteConfig writes the dnsmasq configuration to the specified path
+func (g *ConfigGenerator) WriteConfig(cfg *config.Config, configPath string) error {
+ configContent := g.Generate(cfg)
+
+ log.Printf("Writing dnsmasq config to: %s", configPath)
+ if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
+ return fmt.Errorf("writing dnsmasq config: %w", err)
+ }
+
+ return nil
+}
+
+// RestartService restarts the dnsmasq service
+func (g *ConfigGenerator) RestartService() error {
+ cmd := exec.Command("sudo", "/usr/bin/systemctl", "restart", "dnsmasq.service")
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to restart dnsmasq: %w", err)
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/experimental/daemon/internal/handlers/dnsmasq.go b/experimental/daemon/internal/handlers/dnsmasq.go
new file mode 100644
index 0000000..4811db6
--- /dev/null
+++ b/experimental/daemon/internal/handlers/dnsmasq.go
@@ -0,0 +1,45 @@
+package handlers
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+)
+
+// GetDnsmasqConfigHandler handles requests to view the dnsmasq configuration
+func (app *App) GetDnsmasqConfigHandler(w http.ResponseWriter, r *http.Request) {
+ if app.Config == nil || app.Config.IsEmpty() {
+ http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
+ return
+ }
+
+ config := app.DnsmasqManager.Generate(app.Config)
+ w.Header().Set("Content-Type", "text/plain")
+ w.Write([]byte(config))
+}
+
+// RestartDnsmasqHandler handles requests to restart the dnsmasq service
+func (app *App) RestartDnsmasqHandler(w http.ResponseWriter, r *http.Request) {
+ if app.Config == nil || app.Config.IsEmpty() {
+ http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
+ return
+ }
+
+ // Update dnsmasq config first
+ paths := app.DataManager.GetPaths()
+ if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
+ log.Printf("Failed to update dnsmasq config: %v", err)
+ http.Error(w, "Failed to update dnsmasq config", http.StatusInternalServerError)
+ return
+ }
+
+ // Restart dnsmasq service
+ if err := app.DnsmasqManager.RestartService(); err != nil {
+ log.Printf("Failed to restart dnsmasq: %v", err)
+ http.Error(w, "Failed to restart dnsmasq service", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "restarted"})
+}
\ No newline at end of file
diff --git a/experimental/daemon/internal/handlers/handlers.go b/experimental/daemon/internal/handlers/handlers.go
new file mode 100644
index 0000000..40aca8a
--- /dev/null
+++ b/experimental/daemon/internal/handlers/handlers.go
@@ -0,0 +1,263 @@
+package handlers
+
+import (
+ "encoding/json"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "wild-cloud-central/internal/config"
+ "wild-cloud-central/internal/data"
+ "wild-cloud-central/internal/dnsmasq"
+)
+
+// App represents the application with its dependencies
+type App struct {
+ Config *config.Config
+ StartTime time.Time
+ DataManager *data.Manager
+ DnsmasqManager *dnsmasq.ConfigGenerator
+}
+
+// NewApp creates a new application instance
+func NewApp() *App {
+ return &App{
+ StartTime: time.Now(),
+ DataManager: data.NewManager(),
+ DnsmasqManager: dnsmasq.NewConfigGenerator(),
+ }
+}
+
+// HealthHandler handles health check requests
+func (app *App) HealthHandler(w http.ResponseWriter, r *http.Request) {
+ response := map[string]string{
+ "status": "healthy",
+ "service": "wild-cloud-central",
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// StatusHandler handles status requests for the UI
+func (app *App) StatusHandler(w http.ResponseWriter, r *http.Request) {
+ uptime := time.Since(app.StartTime)
+
+ response := map[string]interface{}{
+ "status": "running",
+ "version": "1.0.0",
+ "uptime": uptime.String(),
+ "timestamp": time.Now().UnixMilli(),
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(response)
+}
+
+// GetConfigHandler handles configuration retrieval requests
+func (app *App) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ // Always reload config from file on each request
+ paths := app.DataManager.GetPaths()
+ cfg, err := config.Load(paths.ConfigFile)
+ if err != nil {
+ log.Printf("Failed to load config from file: %v", err)
+ response := map[string]interface{}{
+ "configured": false,
+ "message": "No configuration found. Please POST a configuration to /api/v1/config to get started.",
+ }
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ // Update the cached config with fresh data
+ app.Config = cfg
+
+ // Check if config is empty/uninitialized
+ if cfg.IsEmpty() {
+ response := map[string]interface{}{
+ "configured": false,
+ "message": "Configuration is incomplete. Please complete the setup.",
+ }
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ response := map[string]interface{}{
+ "configured": true,
+ "config": cfg,
+ }
+ json.NewEncoder(w).Encode(response)
+}
+
+// CreateConfigHandler handles configuration creation requests
+func (app *App) CreateConfigHandler(w http.ResponseWriter, r *http.Request) {
+ // Only allow config creation if no config exists
+ if app.Config != nil && !app.Config.IsEmpty() {
+ http.Error(w, "Configuration already exists. Use PUT to update.", http.StatusConflict)
+ return
+ }
+
+ var newConfig config.Config
+ if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
+ return
+ }
+
+ // Set defaults
+ if newConfig.Server.Port == 0 {
+ newConfig.Server.Port = 5055
+ }
+ if newConfig.Server.Host == "" {
+ newConfig.Server.Host = "0.0.0.0"
+ }
+
+ app.Config = &newConfig
+
+ // Persist config to file
+ paths := app.DataManager.GetPaths()
+ if err := config.Save(app.Config, paths.ConfigFile); err != nil {
+ log.Printf("Failed to save config: %v", err)
+ http.Error(w, "Failed to save config", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "created"})
+}
+
+// UpdateConfigHandler handles configuration update requests
+func (app *App) UpdateConfigHandler(w http.ResponseWriter, r *http.Request) {
+ // Check if config exists
+ if app.Config == nil || app.Config.IsEmpty() {
+ http.Error(w, "No configuration exists. Use POST to create initial configuration.", http.StatusNotFound)
+ return
+ }
+
+ var newConfig config.Config
+ if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
+ http.Error(w, "Invalid JSON", http.StatusBadRequest)
+ return
+ }
+
+ app.Config = &newConfig
+
+ // Persist config to file
+ paths := app.DataManager.GetPaths()
+ if err := config.Save(app.Config, paths.ConfigFile); err != nil {
+ log.Printf("Failed to save config: %v", err)
+ http.Error(w, "Failed to save config", http.StatusInternalServerError)
+ return
+ }
+
+ // Regenerate and apply dnsmasq config
+ if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
+ log.Printf("Failed to update dnsmasq config: %v", err)
+ http.Error(w, "Failed to update dnsmasq config", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
+}
+
+// GetConfigYamlHandler handles raw YAML config file retrieval
+func (app *App) GetConfigYamlHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ paths := app.DataManager.GetPaths()
+
+ // Read the raw config file
+ yamlContent, err := os.ReadFile(paths.ConfigFile)
+ if err != nil {
+ if os.IsNotExist(err) {
+ http.Error(w, "Configuration file not found", http.StatusNotFound)
+ return
+ }
+ log.Printf("Failed to read config file: %v", err)
+ http.Error(w, "Failed to read configuration file", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.Write(yamlContent)
+}
+
+// UpdateConfigYamlHandler handles raw YAML config file updates
+func (app *App) UpdateConfigYamlHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPut {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Read the raw YAML content from request body
+ yamlContent, err := io.ReadAll(r.Body)
+ if err != nil {
+ log.Printf("Failed to read request body: %v", err)
+ http.Error(w, "Failed to read request body", http.StatusBadRequest)
+ return
+ }
+
+ paths := app.DataManager.GetPaths()
+
+ // Write the raw YAML content to file
+ if err := os.WriteFile(paths.ConfigFile, yamlContent, 0644); err != nil {
+ log.Printf("Failed to write config file: %v", err)
+ http.Error(w, "Failed to write configuration file", http.StatusInternalServerError)
+ return
+ }
+
+ // Try to reload the config to validate it and update the in-memory config
+ newConfig, err := config.Load(paths.ConfigFile)
+ if err != nil {
+ log.Printf("Warning: Saved YAML config but failed to parse it: %v", err)
+ // File was written but parsing failed - this is a validation warning
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]interface{}{
+ "status": "saved_with_warnings",
+ "warning": "Configuration saved but contains validation errors: " + err.Error(),
+ }
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ // Update in-memory config if parsing succeeded
+ app.Config = newConfig
+
+ // Try to regenerate dnsmasq config if the new config is valid
+ if err := app.DnsmasqManager.WriteConfig(app.Config, paths.DnsmasqConf); err != nil {
+ log.Printf("Warning: Failed to update dnsmasq config: %v", err)
+ // Config was saved but dnsmasq update failed
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]interface{}{
+ "status": "saved_with_warnings",
+ "warning": "Configuration saved but failed to update dnsmasq config: " + err.Error(),
+ }
+ json.NewEncoder(w).Encode(response)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "updated"})
+}
+
+// CORSMiddleware adds CORS headers to responses
+func (app *App) CORSMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/experimental/daemon/internal/handlers/pxe.go b/experimental/daemon/internal/handlers/pxe.go
new file mode 100644
index 0000000..bf20f93
--- /dev/null
+++ b/experimental/daemon/internal/handlers/pxe.go
@@ -0,0 +1,138 @@
+package handlers
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+// DownloadPXEAssetsHandler handles requests to download PXE boot assets
+func (app *App) DownloadPXEAssetsHandler(w http.ResponseWriter, r *http.Request) {
+ if app.Config == nil || app.Config.IsEmpty() {
+ http.Error(w, "No configuration available. Please configure the system first.", http.StatusPreconditionFailed)
+ return
+ }
+
+ if err := app.downloadTalosAssets(); err != nil {
+ log.Printf("Failed to download PXE assets: %v", err)
+ http.Error(w, "Failed to download PXE assets", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]string{"status": "downloaded"})
+}
+
+// downloadTalosAssets downloads Talos Linux PXE assets
+func (app *App) downloadTalosAssets() error {
+ // Get assets directory from data paths
+ paths := app.DataManager.GetPaths()
+ assetsDir := filepath.Join(paths.AssetsDir, "talos")
+
+ log.Printf("Downloading Talos assets to: %s", assetsDir)
+ if err := os.MkdirAll(filepath.Join(assetsDir, "amd64"), 0755); err != nil {
+ return fmt.Errorf("creating assets directory: %w", err)
+ }
+
+ // Create Talos bare metal configuration (schematic format)
+ bareMetalConfig := `customization:
+ extraKernelArgs:
+ - net.ifnames=0
+ systemExtensions:
+ officialExtensions:
+ - siderolabs/gvisor
+ - siderolabs/intel-ucode`
+
+ // Create Talos schematic
+ var buf bytes.Buffer
+ buf.WriteString(bareMetalConfig)
+
+ resp, err := http.Post("https://factory.talos.dev/schematics", "text/yaml", &buf)
+ if err != nil {
+ return fmt.Errorf("creating Talos schematic: %w", err)
+ }
+ defer resp.Body.Close()
+
+ var schematic struct {
+ ID string `json:"id"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&schematic); err != nil {
+ return fmt.Errorf("decoding schematic response: %w", err)
+ }
+
+ log.Printf("Created Talos schematic with ID: %s", schematic.ID)
+
+ // Download kernel
+ kernelURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/kernel-amd64",
+ schematic.ID, app.Config.Cluster.Nodes.Talos.Version)
+ if err := downloadFile(kernelURL, filepath.Join(assetsDir, "amd64", "vmlinuz")); err != nil {
+ return fmt.Errorf("downloading kernel: %w", err)
+ }
+
+ // Download initramfs
+ initramfsURL := fmt.Sprintf("https://pxe.factory.talos.dev/image/%s/%s/initramfs-amd64.xz",
+ schematic.ID, app.Config.Cluster.Nodes.Talos.Version)
+ if err := downloadFile(initramfsURL, filepath.Join(assetsDir, "amd64", "initramfs.xz")); err != nil {
+ return fmt.Errorf("downloading initramfs: %w", err)
+ }
+
+ // Create boot.ipxe file
+ bootScript := fmt.Sprintf(`#!ipxe
+imgfree
+kernel http://%s/amd64/vmlinuz talos.platform=metal console=tty0 init_on_alloc=1 slab_nomerge pti=on consoleblank=0 nvme_core.io_timeout=4294967295 printk.devkmsg=on ima_template=ima-ng ima_appraise=fix ima_hash=sha512 selinux=1 net.ifnames=0
+initrd http://%s/amd64/initramfs.xz
+boot
+`, app.Config.Cloud.DNS.IP, app.Config.Cloud.DNS.IP)
+
+ if err := os.WriteFile(filepath.Join(assetsDir, "boot.ipxe"), []byte(bootScript), 0644); err != nil {
+ return fmt.Errorf("writing boot script: %w", err)
+ }
+
+ // Download iPXE bootloaders
+ tftpDir := filepath.Join(paths.AssetsDir, "tftp")
+ if err := os.MkdirAll(tftpDir, 0755); err != nil {
+ return fmt.Errorf("creating tftp directory: %w", err)
+ }
+
+ bootloaders := map[string]string{
+ "http://boot.ipxe.org/ipxe.efi": filepath.Join(tftpDir, "ipxe.efi"),
+ "http://boot.ipxe.org/undionly.kpxe": filepath.Join(tftpDir, "undionly.kpxe"),
+ "http://boot.ipxe.org/arm64-efi/ipxe.efi": filepath.Join(tftpDir, "ipxe-arm64.efi"),
+ }
+
+ for url, path := range bootloaders {
+ if err := downloadFile(url, path); err != nil {
+ return fmt.Errorf("downloading %s: %w", url, err)
+ }
+ }
+
+ log.Printf("Successfully downloaded PXE assets")
+ return nil
+}
+
+// downloadFile downloads a file from a URL to a local path
+func downloadFile(url, filepath string) error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("bad status: %s", resp.Status)
+ }
+
+ out, err := os.Create(filepath)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ _, err = io.Copy(out, resp.Body)
+ return err
+}
\ No newline at end of file
diff --git a/experimental/daemon/main.go b/experimental/daemon/main.go
new file mode 100644
index 0000000..355d867
--- /dev/null
+++ b/experimental/daemon/main.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/mux"
+
+ "wild-cloud-central/internal/config"
+ "wild-cloud-central/internal/handlers"
+)
+
+func main() {
+ // Create application instance
+ app := handlers.NewApp()
+
+ // Initialize data directory
+ if err := app.DataManager.Initialize(); err != nil {
+ log.Fatalf("Failed to initialize data directory: %v", err)
+ }
+
+ // Load configuration if it exists
+ paths := app.DataManager.GetPaths()
+ if cfg, err := config.Load(paths.ConfigFile); err != nil {
+ log.Printf("No configuration found, starting with empty config: %v", err)
+ } else {
+ app.Config = cfg
+ log.Printf("Configuration loaded successfully")
+ }
+
+ // Set up HTTP router
+ router := mux.NewRouter()
+ setupRoutes(app, router)
+
+ // Use default server settings if config is empty
+ host := "0.0.0.0"
+ port := 5055
+ if app.Config != nil && app.Config.Server.Host != "" {
+ host = app.Config.Server.Host
+ }
+ if app.Config != nil && app.Config.Server.Port != 0 {
+ port = app.Config.Server.Port
+ }
+
+ addr := fmt.Sprintf("%s:%d", host, port)
+ log.Printf("Starting wild-cloud-central server on %s", addr)
+
+ if err := http.ListenAndServe(addr, router); err != nil {
+ log.Fatal("Server failed to start:", err)
+ }
+}
+
+func setupRoutes(app *handlers.App, router *mux.Router) {
+ // Add CORS middleware
+ router.Use(app.CORSMiddleware)
+
+ // API v1 routes
+ router.HandleFunc("/api/v1/health", app.HealthHandler).Methods("GET")
+ router.HandleFunc("/api/v1/config", app.GetConfigHandler).Methods("GET")
+ router.HandleFunc("/api/v1/config", app.UpdateConfigHandler).Methods("PUT")
+ router.HandleFunc("/api/v1/config", app.CreateConfigHandler).Methods("POST")
+ router.HandleFunc("/api/v1/config/yaml", app.GetConfigYamlHandler).Methods("GET")
+ router.HandleFunc("/api/v1/config/yaml", app.UpdateConfigYamlHandler).Methods("PUT")
+ router.HandleFunc("/api/v1/dnsmasq/config", app.GetDnsmasqConfigHandler).Methods("GET")
+ router.HandleFunc("/api/v1/dnsmasq/restart", app.RestartDnsmasqHandler).Methods("POST")
+ router.HandleFunc("/api/v1/pxe/assets", app.DownloadPXEAssetsHandler).Methods("POST")
+
+ // UI-specific endpoints
+ router.HandleFunc("/api/status", app.StatusHandler).Methods("GET")
+
+ // Serve static files
+ router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))
+}
\ No newline at end of file
diff --git a/experimental/daemon/tests/integration/debug-container.sh b/experimental/daemon/tests/integration/debug-container.sh
new file mode 100755
index 0000000..c229d0f
--- /dev/null
+++ b/experimental/daemon/tests/integration/debug-container.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+set -e
+
+echo "๐ณ Starting wild-cloud-central debug container..."
+
+# Build the Docker image if it doesn't exist
+if ! docker images | grep -q wild-cloud-central-test; then
+ echo "๐จ Building Docker image..."
+ docker build -t wild-cloud-central-test .
+fi
+
+echo ""
+echo "๐ง Starting container with shell access..."
+echo ""
+echo "๐ Access points:"
+echo " - Management UI: http://localhost:9080"
+echo " - API directly: http://localhost:9081"
+echo ""
+echo "๐ก Inside the container you can:"
+echo " - Start services manually: /test-installation.sh"
+echo " - Check logs: journalctl or service status"
+echo " - Test APIs: curl http://localhost:5055/api/v1/health"
+echo " - Modify config: nano /etc/wild-cloud-central/config.yaml"
+echo " - View web files: ls /var/www/html/wild-central/"
+echo ""
+
+# Run container with shell access
+docker run --rm -it \
+ -p 127.0.0.1:9081:5055 \
+ -p 127.0.0.1:9080:80 \
+ -p 127.0.0.1:9053:53/udp \
+ -p 127.0.0.1:9067:67/udp \
+ -p 127.0.0.1:9069:69/udp \
+ --cap-add=NET_ADMIN \
+ --cap-add=NET_BIND_SERVICE \
+ --name wild-central-debug \
+ wild-cloud-central-test \
+ /bin/bash
\ No newline at end of file
diff --git a/experimental/daemon/tests/integration/start-background.sh b/experimental/daemon/tests/integration/start-background.sh
new file mode 100755
index 0000000..82a5856
--- /dev/null
+++ b/experimental/daemon/tests/integration/start-background.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+
+set -e
+
+echo "๐ Starting wild-cloud-central in background..."
+
+# Build the Docker image if it doesn't exist
+if ! docker images | grep -q wild-cloud-central-test; then
+ echo "๐จ Building Docker image..."
+ docker build -t wild-cloud-central-test .
+fi
+
+# Stop any existing container
+docker rm -f wild-central-bg 2>/dev/null || true
+
+echo "๐ Starting services in background..."
+
+# Start container in background
+docker run -d \
+ --name wild-central-bg \
+ -p 127.0.0.1:9081:5055 \
+ -p 127.0.0.1:9080:80 \
+ -p 127.0.0.1:9053:53/udp \
+ -p 127.0.0.1:9067:67/udp \
+ -p 127.0.0.1:9069:69/udp \
+ --cap-add=NET_ADMIN \
+ --cap-add=NET_BIND_SERVICE \
+ wild-cloud-central-test \
+ /bin/bash -c '
+ # Start nginx
+ nginx &
+
+ # Start dnsmasq
+ dnsmasq --keep-in-foreground --log-facility=- &
+
+ # Start wild-cloud-central
+ /usr/bin/wild-cloud-central &
+
+ # Wait indefinitely
+ tail -f /dev/null
+ '
+
+echo "โณ Waiting for services to start..."
+sleep 5
+
+# Test if services are running
+if curl -s http://localhost:9081/api/v1/health > /dev/null 2>&1; then
+ echo "โ
Services started successfully!"
+ echo ""
+ echo "๐ Access points (localhost only):"
+ echo " - Management UI: http://localhost:9080"
+ echo " - API: http://localhost:9081/api/v1/health"
+ echo " - DNS: localhost:9053 (for testing)"
+ echo " - DHCP: localhost:9067 (for testing)"
+ echo " - TFTP: localhost:9069 (for testing)"
+ echo ""
+ echo "๐ง Container management:"
+ echo " - View logs: docker logs wild-central-bg"
+ echo " - Stop services: docker stop wild-central-bg"
+ echo " - Remove container: docker rm wild-central-bg"
+ echo ""
+ echo "๐ก Test commands:"
+ echo " curl http://localhost:9081/api/v1/health"
+ echo " dig @localhost -p 9053 wildcloud.local"
+ echo " curl http://localhost:9081/api/v1/dnsmasq/config"
+else
+ echo "โ Services failed to start. Check logs with: docker logs wild-central-bg"
+ exit 1
+fi
\ No newline at end of file
diff --git a/experimental/daemon/tests/integration/start-interactive.sh b/experimental/daemon/tests/integration/start-interactive.sh
new file mode 100755
index 0000000..ac54bf3
--- /dev/null
+++ b/experimental/daemon/tests/integration/start-interactive.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+
+set -e
+
+echo "๐ Starting wild-cloud-central for interactive testing..."
+
+# Build the Docker image if it doesn't exist
+if ! docker images | grep -q wild-cloud-central-test; then
+ echo "๐จ Building Docker image..."
+ docker build -t wild-cloud-central-test .
+fi
+
+echo ""
+echo "๐ Starting services... This will take a few seconds."
+echo ""
+echo "๐ Access points:"
+echo " - Management UI: http://localhost:9080"
+echo " - API directly: http://localhost:9081"
+echo " - Health check: http://localhost:9081/api/v1/health"
+echo ""
+echo "๐ง Available API endpoints:"
+echo " - GET /api/v1/health"
+echo " - GET /api/v1/config"
+echo " - PUT /api/v1/config"
+echo " - GET /api/v1/dnsmasq/config"
+echo " - POST /api/v1/dnsmasq/restart"
+echo " - POST /api/v1/pxe/assets"
+echo ""
+echo "๐ก Example commands to try:"
+echo " curl http://localhost:9081/api/v1/health"
+echo " curl http://localhost:9081/api/v1/config"
+echo " curl http://localhost:9081/api/v1/dnsmasq/config"
+echo " curl -X POST http://localhost:9081/api/v1/pxe/assets"
+echo ""
+echo "๐ Press Ctrl+C to stop all services"
+echo ""
+
+# Create a custom startup script that keeps services running
+docker run --rm -it \
+ -p 127.0.0.1:9081:5055 \
+ -p 127.0.0.1:9080:80 \
+ -p 127.0.0.1:9053:53/udp \
+ -p 127.0.0.1:9067:67/udp \
+ -p 127.0.0.1:9069:69/udp \
+ --cap-add=NET_ADMIN \
+ --cap-add=NET_BIND_SERVICE \
+ --name wild-central-interactive \
+ wild-cloud-central-test \
+ /bin/bash -c '
+ echo "๐ง Starting all services..."
+
+ # Start nginx
+ nginx &
+ NGINX_PID=$!
+
+ # Start dnsmasq
+ dnsmasq --keep-in-foreground --log-facility=- &
+ DNSMASQ_PID=$!
+
+ # Start wild-cloud-central
+ /usr/bin/wild-cloud-central &
+ SERVICE_PID=$!
+
+ # Wait for services to start
+ sleep 3
+
+ echo "โ
All services started!"
+ echo " - nginx (PID: $NGINX_PID)"
+ echo " - dnsmasq (PID: $DNSMASQ_PID)"
+ echo " - wild-cloud-central (PID: $SERVICE_PID)"
+ echo ""
+ echo "๐ Services are now available:"
+ echo " - Web UI: http://localhost:9080"
+ echo " - API: http://localhost:9081"
+ echo ""
+
+ # Function to handle shutdown
+ shutdown() {
+ echo ""
+ echo "๐ Shutting down services..."
+ kill $SERVICE_PID $DNSMASQ_PID $NGINX_PID 2>/dev/null || true
+ echo "โ
Shutdown complete."
+ exit 0
+ }
+
+ # Set up signal handlers
+ trap shutdown SIGTERM SIGINT
+
+ # Keep container running and wait for signals
+ echo "โจ Container is ready! Press Ctrl+C to stop."
+ wait
+ '
\ No newline at end of file
diff --git a/experimental/daemon/tests/integration/stop-background.sh b/experimental/daemon/tests/integration/stop-background.sh
new file mode 100755
index 0000000..12470bb
--- /dev/null
+++ b/experimental/daemon/tests/integration/stop-background.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+echo "๐ Stopping wild-cloud-central background services..."
+
+if docker ps | grep -q wild-central-bg; then
+ docker stop wild-central-bg
+ docker rm wild-central-bg
+ echo "โ
Services stopped and container removed."
+else
+ echo "โน๏ธ No background services running."
+fi
\ No newline at end of file
diff --git a/experimental/daemon/tests/integration/test-docker.sh b/experimental/daemon/tests/integration/test-docker.sh
new file mode 100755
index 0000000..168a7f2
--- /dev/null
+++ b/experimental/daemon/tests/integration/test-docker.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+set -e
+
+echo "๐งช Testing wild-cloud-central Docker installation..."
+
+# Change to project root directory
+cd "$(dirname "$0")/../.."
+
+# Build the Docker image
+echo "๐จ Building Docker image..."
+docker build -t wild-cloud-central-test .
+
+# Run the container to test installation
+echo "๐ Running installation test..."
+echo "Access points after container starts:"
+echo " - Management UI: http://localhost:9080"
+echo " - API directly: http://localhost:9055"
+echo ""
+docker run --rm -p 9055:5055 -p 9080:80 wild-cloud-central-test
\ No newline at end of file
diff --git a/experimental/daemon/tests/test-installation.sh b/experimental/daemon/tests/test-installation.sh
new file mode 100755
index 0000000..6a0dab6
--- /dev/null
+++ b/experimental/daemon/tests/test-installation.sh
@@ -0,0 +1,146 @@
+#!/bin/bash
+
+set -e
+
+echo "๐ Testing wild-cloud-central installation..."
+
+# Verify the binary was installed
+echo "โ
Checking binary installation..."
+if [ -f "/usr/bin/wild-cloud-central" ]; then
+ echo " Binary installed at /usr/bin/wild-cloud-central"
+else
+ echo "โ Binary not found at /usr/bin/wild-cloud-central"
+ exit 1
+fi
+
+# Verify config was installed
+echo "โ
Checking configuration..."
+if [ -f "/etc/wild-cloud-central/config.yaml" ]; then
+ echo " Config installed at /etc/wild-cloud-central/config.yaml"
+else
+ echo "โ Config not found at /etc/wild-cloud-central/config.yaml"
+ exit 1
+fi
+
+# Verify systemd service file was installed
+echo "โ
Checking systemd service..."
+if [ -f "/etc/systemd/system/wild-cloud-central.service" ]; then
+ echo " Service file installed at /etc/systemd/system/wild-cloud-central.service"
+else
+ echo "โ Service file not found"
+ exit 1
+fi
+
+# Verify nginx config was installed
+echo "โ
Checking nginx configuration..."
+if [ -f "/etc/nginx/sites-available/wild-central" ]; then
+ echo " Nginx config installed at /etc/nginx/sites-available/wild-central"
+ # Enable the site for testing
+ ln -sf /etc/nginx/sites-available/wild-central /etc/nginx/sites-enabled/
+ rm -f /etc/nginx/sites-enabled/default
+else
+ echo "โ Nginx config not found"
+ exit 1
+fi
+
+# Verify web assets were installed
+echo "โ
Checking web assets..."
+if [ -f "/var/www/html/wild-central/index.html" ]; then
+ echo " Web assets installed at /var/www/html/wild-central/"
+else
+ echo "โ Web assets not found"
+ exit 1
+fi
+
+# Start nginx (simulating systemd)
+echo "๐ง Starting nginx..."
+nginx &
+NGINX_PID=$!
+
+# Start dnsmasq (simulating systemd)
+echo "๐ง Starting dnsmasq..."
+dnsmasq --keep-in-foreground --log-facility=- &
+DNSMASQ_PID=$!
+
+# Start wild-cloud-central service (simulating systemd)
+echo "๐ง Starting wild-cloud-central service..."
+/usr/bin/wild-cloud-central &
+SERVICE_PID=$!
+
+# Wait for service to start
+echo "โณ Waiting for services to start..."
+sleep 5
+
+# Test health endpoint
+echo "๐ฉบ Testing health endpoint..."
+if curl -s http://localhost:5055/api/v1/health | grep -q "healthy"; then
+ echo " โ
Health check passed"
+else
+ echo " โ Health check failed"
+ exit 1
+fi
+
+# Test configuration endpoint
+echo "๐ง Testing configuration endpoint..."
+CONFIG_RESPONSE=$(curl -s http://localhost:5055/api/v1/config)
+if echo "$CONFIG_RESPONSE" | grep -q "Server"; then
+ echo " โ
Configuration endpoint working"
+else
+ echo " โ Configuration endpoint failed"
+ echo " Response: $CONFIG_RESPONSE"
+ echo " Checking if service is still running..."
+ if kill -0 $SERVICE_PID 2>/dev/null; then
+ echo " Service is running"
+ else
+ echo " Service has died"
+ fi
+ exit 1
+fi
+
+# Test dnsmasq config generation
+echo "๐ง Testing dnsmasq config generation..."
+if curl -s http://localhost:5055/api/v1/dnsmasq/config | grep -q "interface"; then
+ echo " โ
Dnsmasq config generation working"
+else
+ echo " โ Dnsmasq config generation failed"
+ exit 1
+fi
+
+# Test web interface accessibility (through nginx)
+echo "๐ Testing web interface..."
+if curl -s http://localhost:80/ | grep -q "Wild Cloud Central"; then
+ echo " โ
Web interface accessible through nginx"
+else
+ echo " โ Web interface not accessible"
+ exit 1
+fi
+
+echo ""
+echo "๐ All installation tests passed!"
+echo ""
+echo "Services running:"
+echo " - wild-cloud-central: http://localhost:5055"
+echo " - Web interface: http://localhost:80"
+echo " - API health: http://localhost:5055/api/v1/health"
+echo ""
+echo "Installation simulation successful! ๐"
+
+# Keep services running for manual testing
+echo "Services will continue running. Press Ctrl+C to stop."
+
+# Function to handle shutdown
+shutdown() {
+ echo ""
+ echo "๐ Shutting down services..."
+ kill $SERVICE_PID 2>/dev/null || true
+ kill $DNSMASQ_PID 2>/dev/null || true
+ kill $NGINX_PID 2>/dev/null || true
+ echo "Shutdown complete."
+ exit 0
+}
+
+# Set up signal handlers
+trap shutdown SIGTERM SIGINT
+
+# Wait for signals
+wait
\ No newline at end of file