diff --git a/.gitignore b/.gitignore
index 15e369f..10a35e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,5 @@
node_modules
-
-# Dashboard UI source at repo root only (do not ignore public/dashboard/)
-/dashboard/
+dashboard/node_modules
# Local git hooks + secret-stripping scripts (run npm run git:setup)
scripts/
diff --git a/dashboard/components.json b/dashboard/components.json
new file mode 100644
index 0000000..6296a74
--- /dev/null
+++ b/dashboard/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/dashboard/index.html b/dashboard/index.html
new file mode 100644
index 0000000..1b14a02
--- /dev/null
+++ b/dashboard/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+ SupportBot Dashboard
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
new file mode 100644
index 0000000..d6ca904
--- /dev/null
+++ b/dashboard/package-lock.json
@@ -0,0 +1,4977 @@
+{
+ "name": "supportbot-dashboard",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "supportbot-dashboard",
+ "version": "1.0.0",
+ "dependencies": {
+ "@hugeicons/core-free-icons": "^4.1.4",
+ "@hugeicons/react": "^1.1.6",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "autoprefixer": "^10.5.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "emoji-picker-element": "^1.29.1",
+ "lucide-react": "^1.16.0",
+ "postcss": "^8.5.14",
+ "react": "^19.1.0",
+ "react-colorful": "^5.7.0",
+ "react-dom": "^19.1.0",
+ "react-router-dom": "^7.6.2",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.6.0",
+ "tailwindcss": "^3.4.17",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@types/node": "^25.8.0",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.2",
+ "@vitejs/plugin-react": "^4.5.0",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
+ "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
+ "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
+ "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@hugeicons/core-free-icons": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.1.4.tgz",
+ "integrity": "sha512-vkMvlnW7Atqh7juhZCadYDvqABWcEeVAkavAK6HJBFbYkLCme5CN51Oc91D5j5ruDja+HQPjMfWkTxxulcwvAQ==",
+ "license": "MIT"
+ },
+ "node_modules/@hugeicons/react": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@hugeicons/react/-/react-1.1.6.tgz",
+ "integrity": "sha512-c2LhXJMAW5wN1pC/smBXG0YPqUON6ceR/ZdXHCjEI9KvB+hjtqYjmzIxok5hAQOeXGz0WtORgCQMzqewFKAZwg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
+ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
+ "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
+ "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
+ "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
+ "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
+ "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
+ "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
+ "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
+ "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.4"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
+ "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
+ "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
+ "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+ "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+ "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+ "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+ "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+ "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+ "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+ "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+ "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+ "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+ "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+ "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+ "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+ "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+ "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+ "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+ "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+ "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+ "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+ "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+ "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+ "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+ "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+ "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+ "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.8.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
+ "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.29",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz",
+ "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001792",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
+ "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.357",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz",
+ "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==",
+ "license": "ISC"
+ },
+ "node_modules/emoji-picker-element": {
+ "version": "1.29.1",
+ "resolved": "https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.29.1.tgz",
+ "integrity": "sha512-TOiHzu9Dqib3x4MwcAi3wi3RdyT4SoeB4b15AvH1ks4SBwTl7DeebhZ0d3x6dNi4XfNU7IGRZ7NBQllj0RqwrQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
+ "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mz": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "thenify-all": "^1.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.44",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz",
+ "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-colorful": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.7.0.tgz",
+ "integrity": "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
+ "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.15.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
+ "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.15.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.60.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
+ "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.60.4",
+ "@rollup/rollup-android-arm64": "4.60.4",
+ "@rollup/rollup-darwin-arm64": "4.60.4",
+ "@rollup/rollup-darwin-x64": "4.60.4",
+ "@rollup/rollup-freebsd-arm64": "4.60.4",
+ "@rollup/rollup-freebsd-x64": "4.60.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+ "@rollup/rollup-linux-arm64-musl": "4.60.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+ "@rollup/rollup-linux-loong64-musl": "4.60.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-gnu": "4.60.4",
+ "@rollup/rollup-linux-x64-musl": "4.60.4",
+ "@rollup/rollup-openbsd-x64": "4.60.4",
+ "@rollup/rollup-openharmony-arm64": "4.60.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+ "@rollup/rollup-win32-x64-gnu": "4.60.4",
+ "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/sonner": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
+ "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
+ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.1",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "tinyglobby": "^0.2.11",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
+ "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss-animate": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
+ "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || insiders"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "license": "MIT",
+ "dependencies": {
+ "any-promise": "^1.0.0"
+ }
+ },
+ "node_modules/thenify-all": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-interface-checker": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ }
+ }
+}
diff --git a/dashboard/package.json b/dashboard/package.json
new file mode 100644
index 0000000..61f872b
--- /dev/null
+++ b/dashboard/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "supportbot-dashboard",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@hugeicons/core-free-icons": "^4.1.4",
+ "@hugeicons/react": "^1.1.6",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "autoprefixer": "^10.5.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "emoji-picker-element": "^1.29.1",
+ "lucide-react": "^1.16.0",
+ "postcss": "^8.5.14",
+ "react": "^19.1.0",
+ "react-colorful": "^5.7.0",
+ "react-dom": "^19.1.0",
+ "react-router-dom": "^7.6.2",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.6.0",
+ "tailwindcss": "^3.4.17",
+ "tailwindcss-animate": "^1.0.7"
+ },
+ "devDependencies": {
+ "@types/node": "^25.8.0",
+ "@types/react": "^19.1.2",
+ "@types/react-dom": "^19.1.2",
+ "@vitejs/plugin-react": "^4.5.0",
+ "typescript": "^5.8.3",
+ "vite": "^6.3.5"
+ }
+}
diff --git a/dashboard/postcss.config.js b/dashboard/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/dashboard/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx
new file mode 100644
index 0000000..8e7ea31
--- /dev/null
+++ b/dashboard/src/App.tsx
@@ -0,0 +1,119 @@
+import { Navigate, Route, Routes, useLocation } from "react-router-dom";
+import Layout from "./components/Layout";
+import { useAuth } from "./context/AuthContext";
+import { useSetup } from "./context/SetupContext";
+import Configs from "./pages/Configs";
+import Login from "./pages/Login";
+import Setup from "./pages/Setup";
+import { HomeRedirect } from "./components/home-redirect";
+import { RequirePermission } from "./components/require-permission";
+import Settings from "./pages/Settings";
+import SystemDetails from "./pages/SystemDetails";
+import Logs from "./pages/Logs";
+import Transcripts from "./pages/Transcripts";
+import Users from "./pages/Users";
+import Addons from "./pages/Addons";
+
+function Protected({ children }: { children: React.ReactNode }) {
+ const { isAuthenticated, loading } = useAuth();
+ if (loading) {
+ return (
+
+ Loading…
+
+ );
+ }
+ if (!isAuthenticated) return ;
+ return <>{children}>;
+}
+
+export default function App() {
+ const { complete, loading } = useSetup();
+ const location = useLocation();
+
+ if (loading) {
+ return (
+
+ Loading…
+
+ );
+ }
+
+ if (!complete) {
+ return (
+
+ } />
+ } />
+
+ );
+ }
+
+ if (location.pathname === "/setup") {
+ return ;
+ }
+
+ return (
+
+ } />
+
+
+
+ }
+ >
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+ } />
+
+ );
+}
diff --git a/dashboard/src/api/client.ts b/dashboard/src/api/client.ts
new file mode 100644
index 0000000..8d1cba0
--- /dev/null
+++ b/dashboard/src/api/client.ts
@@ -0,0 +1,581 @@
+import type {
+ DashboardPermissions,
+ DashboardRole,
+ DashboardUser,
+ StoredDashboardUser,
+} from "@/lib/permissions";
+
+export type { DashboardUser, DashboardPermissions, StoredDashboardUser, DashboardRole };
+
+const REQUEST_TIMEOUT_MS = 25_000;
+
+async function request(
+ path: string,
+ options: RequestInit = {},
+): Promise {
+ const headers = new Headers(options.headers);
+ if (!headers.has("Content-Type") && options.body) {
+ headers.set("Content-Type", "application/json");
+ }
+
+ const controller = new AbortController();
+ const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+
+ let res: Response;
+ try {
+ res = await fetch(path, {
+ ...options,
+ headers,
+ credentials: "include",
+ signal: controller.signal,
+ });
+ } catch (err) {
+ if (err instanceof Error && err.name === "AbortError") {
+ throw new Error(
+ "Request timed out. The API may still be starting or the bot is restarting — try again.",
+ );
+ }
+ throw new Error(
+ err instanceof Error
+ ? err.message
+ : "Could not reach the API. Is the bot process running?",
+ );
+ } finally {
+ window.clearTimeout(timeout);
+ }
+
+ const body = await res.json().catch(() => ({}));
+
+ if (!res.ok) {
+ throw new Error(body.error || `Request failed (${res.status})`);
+ }
+
+ return body as T;
+}
+
+export type LogType = "Output" | "Warn" | "Error";
+
+export interface LogEntry {
+ type: LogType;
+ timestamp: string | null;
+ message: string;
+}
+
+export interface LogsPayload {
+ entries: LogEntry[];
+ cursor: Record;
+}
+
+export interface BotRestartResult {
+ success: boolean;
+ error?: string;
+ commands?: number;
+}
+
+export interface ApiResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+ message?: string;
+ restartRequired?: boolean;
+ reloaded?: string[];
+ botRestart?: BotRestartResult;
+}
+
+export interface CatalogAddon {
+ id: string;
+ name: string;
+ description: string;
+ repositoryUrl: string;
+ jsFiles: string[];
+ configFiles: string[];
+ fileCount: number;
+ installed?: boolean;
+}
+
+export interface UpdateCheckResult {
+ current: string;
+ latest: string;
+ updateAvailable: boolean;
+ repository: string;
+ branch: string;
+ zipUrl: string;
+}
+
+export type NotificationType = "bot" | "update" | "info" | "error";
+
+export interface DashboardNotification {
+ id: string;
+ type: NotificationType;
+ title: string;
+ message: string;
+ href: string | null;
+ createdAt: number;
+ read: boolean;
+}
+
+export interface NotificationsPayload {
+ items: DashboardNotification[];
+ unreadCount: number;
+}
+
+export type ModuleSetupStatus = "todo" | "progress" | "done";
+
+export interface ModuleStatusInfo {
+ status: ModuleSetupStatus;
+ subtitle: string;
+ passed?: number;
+ total?: number;
+}
+
+export interface GuildSummary {
+ id: string;
+ name: string;
+ memberCount: number | null;
+ icon: string | null;
+}
+
+export type GuildHealthStatus =
+ | "ok"
+ | "missing_config"
+ | "not_in_guild"
+ | "extra_guilds"
+ | "offline";
+
+export interface GuildHealth {
+ status: GuildHealthStatus;
+ configuredGuildId: string | null;
+ message: string | null;
+ configuredGuild: GuildSummary | null;
+ extraGuilds: GuildSummary[];
+}
+
+export type DashboardAlertSeverity = "error" | "warning" | "info";
+
+export interface DashboardAlertPayload {
+ id: string;
+ severity: DashboardAlertSeverity;
+ category: string;
+ title: string;
+ message: string;
+ action?: { label: string; href: string };
+ meta?: Record;
+}
+
+export interface GuildEmojiResource {
+ id: string;
+ name: string;
+ animated: boolean;
+ url: string | null;
+}
+
+export interface GuildResources {
+ guildId: string;
+ guildName: string;
+ roles: { id: string; name: string; color: string | null }[];
+ channels: { id: string; name: string; type: number }[];
+ categories: { id: string; name: string }[];
+ emojis: GuildEmojiResource[];
+}
+
+export interface BotStats {
+ bot: {
+ username: string;
+ id: string;
+ avatar: string;
+ inviteUrl?: string;
+ version: string;
+ ping: number;
+ uptime: number;
+ };
+ servers: number;
+ users: number;
+ tickets: { total: number; open: number; closed: number };
+ modules?: Record;
+ hosting: {
+ ram_used: string;
+ ram_total: string;
+ ram_percent: number;
+ cpu_load: string;
+ uptime: string;
+ };
+}
+
+export function maskConfigSecrets(content: string, filename: string): string {
+ if (filename !== "supportbot") return content;
+ return content.replace(
+ /^(\s*Token:\s*)["'][^"']*["']/m,
+ '$1"BOT_TOKEN"',
+ );
+}
+
+export const api = {
+ me: () => request>("/api/auth/me"),
+
+ logout: () =>
+ request>("/api/auth/logout", { method: "POST" }),
+
+ health: () =>
+ request<
+ ApiResponse<{
+ dashboard: boolean;
+ botReady: boolean;
+ oauthEnabled?: boolean;
+ }>
+ >("/api/health"),
+
+ stats: () => request>("/api/stats"),
+
+ getAlerts: () => request>("/api/alerts"),
+
+ getGuildStatus: () =>
+ request>("/api/guild/status"),
+
+ getGuildResources: (refresh = false) =>
+ request>(
+ `/api/guild/resources${refresh ? "?refresh=1" : ""}`,
+ ),
+
+ leaveGuild: (guildId: string) =>
+ request>(`/api/guild/${guildId}/leave`, {
+ method: "POST",
+ }),
+
+ listConfigs: () =>
+ [
+ "supportbot",
+ "ticket-panel",
+ "commands",
+ "messages",
+ "supportbot-ai",
+ ] as const,
+
+ getConfigRaw: (file: string) =>
+ request>(`/api/configs/${file}`),
+
+ saveConfigRaw: (filename: string, content: string) =>
+ request>("/api/configs/raw", {
+ method: "PUT",
+ body: JSON.stringify({ filename, content }),
+ }),
+
+ getConfigJson: (file: string) =>
+ request>>(`/api/configs/json/${file}`),
+
+ updateConfigFields: (updates: Record) =>
+ request>("/api/configs/update-fields/multi", {
+ method: "POST",
+ body: JSON.stringify(updates),
+ }),
+
+ checkForUpdates: () =>
+ request>("/api/system/update-check"),
+
+ runUpdate: (version: string) =>
+ request>("/api/system/update", {
+ method: "POST",
+ body: JSON.stringify({ version }),
+ }),
+
+ reloadConfigs: (file?: string) =>
+ request>("/api/system/reload-configs", {
+ method: "POST",
+ body: JSON.stringify(file ? { file } : {}),
+ }),
+
+ getLogsMeta: () =>
+ request<
+ ApiResponse<{
+ types: LogType[];
+ files: { type: LogType; date: string; file: string | null; size: number }[];
+ }>
+ >("/api/system/logs/meta"),
+
+ getLogs: (params: {
+ types?: LogType[];
+ cursor?: Record;
+ tail?: number;
+ }) => {
+ const q = new URLSearchParams();
+ if (params.types?.length) q.set("types", params.types.join(","));
+ if (params.cursor && Object.keys(params.cursor).length > 0) {
+ q.set("cursor", JSON.stringify(params.cursor));
+ }
+ if (params.tail != null) q.set("tail", String(params.tail));
+ const qs = q.toString();
+ return request>(
+ `/api/system/logs${qs ? `?${qs}` : ""}`,
+ );
+ },
+
+ listTranscripts: () =>
+ request<
+ ApiResponse<
+ {
+ id: string;
+ filename: string;
+ createdAt: string;
+ size?: number;
+ ticketName?: string | null;
+ }[]
+ >
+ >("/api/system/transcripts"),
+
+ getTranscript: (id: string) =>
+ request>(`/api/system/transcripts/${id}`),
+
+ getTranscriptSettings: () =>
+ request<
+ ApiResponse<{
+ autoDelete: { enabled: boolean; afterDays: number };
+ publicAccess: { enabled: boolean };
+ publicBasePath?: string;
+ }>
+ >("/api/system/transcript-settings"),
+
+ updateTranscriptSettings: (body: {
+ autoDelete?: { enabled?: boolean; afterDays?: number };
+ publicAccess?: { enabled?: boolean };
+ }) =>
+ request<
+ ApiResponse<{
+ autoDelete: { enabled: boolean; afterDays: number };
+ publicAccess: { enabled: boolean };
+ publicBasePath?: string;
+ }>
+ >("/api/system/transcript-settings", {
+ method: "PUT",
+ body: JSON.stringify(body),
+ }),
+
+ deleteTranscript: (id: string) =>
+ request>(`/api/system/transcripts/${encodeURIComponent(id)}`, {
+ method: "DELETE",
+ }),
+
+ purgeTranscripts: () =>
+ request<
+ ApiResponse<{ deleted: number; skipped: boolean }>
+ >("/api/system/transcripts/purge", { method: "POST" }),
+
+ getBranding: () =>
+ request<
+ ApiResponse<{
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+ }>
+ >("/api/branding"),
+
+ updateBranding: (body: { title?: string; pageTitle?: string }) =>
+ request<
+ ApiResponse<{
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+ }>
+ >("/api/branding", {
+ method: "PUT",
+ body: JSON.stringify(body),
+ }),
+
+ uploadBrandingFavicon: (dataUrl: string) =>
+ request<
+ ApiResponse<{
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+ }>
+ >("/api/branding/favicon", {
+ method: "POST",
+ body: JSON.stringify({ dataUrl }),
+ }),
+
+ removeBrandingFavicon: () =>
+ request<
+ ApiResponse<{
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+ }>
+ >("/api/branding/favicon", { method: "DELETE" }),
+
+ resetBranding: () =>
+ request<
+ ApiResponse<{
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+ }>
+ >("/api/branding/reset", { method: "POST" }),
+
+ getTranscriptTemplate: () =>
+ request>>(
+ "/api/system/transcript-template",
+ ),
+
+ saveTranscriptTemplate: (data: Record) =>
+ request>>(
+ "/api/system/transcript-template",
+ {
+ method: "PUT",
+ body: JSON.stringify(data),
+ },
+ ),
+
+ previewTranscriptTemplate: async (
+ data: Record,
+ ): Promise => {
+ const controller = new AbortController();
+ const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
+ try {
+ const res = await fetch("/api/system/transcript-template/preview", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(data),
+ signal: controller.signal,
+ });
+ if (!res.ok) {
+ throw new Error(`Preview failed (${res.status})`);
+ }
+ return await res.text();
+ } finally {
+ window.clearTimeout(timeout);
+ }
+ },
+
+ getNotifications: () =>
+ request>("/api/notifications"),
+
+ markNotificationsRead: (ids: string[] | "all") =>
+ request>("/api/notifications/read", {
+ method: "POST",
+ body: JSON.stringify({ ids }),
+ }),
+
+ dismissNotification: (id: string) =>
+ request>(`/api/notifications/${id}`, {
+ method: "DELETE",
+ }),
+
+ listDashboardUsers: () =>
+ request<
+ ApiResponse<{
+ users: StoredDashboardUser[];
+ roles: DashboardRole[];
+ configFiles: string[];
+ }>
+ >("/api/dashboard-users"),
+
+ addDashboardUser: (body: {
+ id: string;
+ role: DashboardRole;
+ username?: string;
+ globalName?: string;
+ permissions?: DashboardPermissions;
+ enabled?: boolean;
+ }) =>
+ request>("/api/dashboard-users", {
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+
+ updateDashboardUser: (
+ id: string,
+ body: {
+ role?: DashboardRole;
+ enabled?: boolean;
+ username?: string;
+ globalName?: string;
+ permissions?: DashboardPermissions;
+ },
+ ) =>
+ request>(`/api/dashboard-users/${id}`, {
+ method: "PATCH",
+ body: JSON.stringify(body),
+ }),
+
+ removeDashboardUser: (id: string) =>
+ request>(`/api/dashboard-users/${id}`, {
+ method: "DELETE",
+ }),
+
+ getAddonsCatalog: () =>
+ request<
+ ApiResponse<{
+ repository: string;
+ addons: CatalogAddon[];
+ }>
+ >("/api/addons/catalog"),
+
+ getAddonsInstalled: () =>
+ request<
+ ApiResponse<{
+ installed: { id: string; name: string; jsFiles: string[]; configFiles: string[] }[];
+ configs: string[];
+ }>
+ >("/api/addons/installed"),
+
+ listAddonConfigs: () =>
+ request>("/api/addons/configs"),
+
+ installAddon: (name: string) =>
+ request>("/api/addons/install", {
+ method: "POST",
+ body: JSON.stringify({ name }),
+ }),
+
+ getAddonConfigJson: (file: string) =>
+ request>>(
+ `/api/addons/configs/json/${encodeURIComponent(file)}`,
+ ),
+
+ saveAddonConfig: (filename: string, data: Record) =>
+ request>("/api/addons/configs", {
+ method: "PUT",
+ body: JSON.stringify({ filename, data }),
+ }),
+
+ getSetupStatus: () =>
+ request>("/api/setup/status"),
+
+ validateSetup: (body: Record) =>
+ request>(
+ "/api/setup/validate",
+ { method: "POST", body: JSON.stringify(body) },
+ ),
+
+ completeSetup: (body: Record) =>
+ request<
+ ApiResponse<{ setupComplete: boolean; botRestart?: BotRestartResult }>
+ >("/api/setup/complete", {
+ method: "POST",
+ body: JSON.stringify(body),
+ }),
+};
+
+export function displayDiscordName(
+ user: Pick,
+): string {
+ return user.globalName || user.username;
+}
+
+export function formatUptime(seconds: number): string {
+ const d = Math.floor(seconds / 86400);
+ const h = Math.floor((seconds % 86400) / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ if (d > 0) return `${d}d ${h}h`;
+ if (h > 0) return `${h}h ${m}m`;
+ return `${m}m`;
+}
diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx
new file mode 100644
index 0000000..2a9fa4c
--- /dev/null
+++ b/dashboard/src/components/Layout.tsx
@@ -0,0 +1,26 @@
+import { Outlet } from "react-router-dom";
+import { AppSidebar } from "@/components/app-sidebar";
+import { DashboardHeader } from "@/components/dashboard-header";
+import { DashboardAlerts } from "@/components/dashboard-alerts";
+import { BotStatsProvider } from "@/context/BotStatsContext";
+import { NotificationsProvider } from "@/context/NotificationsContext";
+import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
+
+export default function Layout() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/addons/addon-config-editor.tsx b/dashboard/src/components/addons/addon-config-editor.tsx
new file mode 100644
index 0000000..62624a9
--- /dev/null
+++ b/dashboard/src/components/addons/addon-config-editor.tsx
@@ -0,0 +1,166 @@
+import { useCallback, useEffect, useState } from "react";
+import { AlertCircleIcon, FloppyDiskIcon } from "@hugeicons/core-free-icons";
+import { api } from "@/api/client";
+import { ConfigFieldInput } from "@/components/config/config-field";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import { schemaFromObject } from "@/lib/generic-config-schema";
+import { getByPath, setByPath } from "@/lib/config-utils";
+import { useAuth } from "@/context/AuthContext";
+import { notifyConfigSave } from "@/lib/notify-config-save";
+
+interface AddonConfigEditorProps {
+ filename: string;
+}
+
+export function AddonConfigEditor({ filename }: AddonConfigEditorProps) {
+ const { canEditConfig } = useAuth();
+ const readOnly = !canEditConfig("supportbot");
+ const [data, setData] = useState | null>(null);
+ const [original, setOriginal] = useState | null>(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [message, setMessage] = useState("");
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.getAddonConfigJson(filename);
+ const raw = (res.data ?? {}) as Record;
+ setData(raw);
+ setOriginal(structuredClone(raw));
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load config");
+ } finally {
+ setLoading(false);
+ }
+ }, [filename]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const schema = data ? schemaFromObject(data) : null;
+
+ function patch(path: string, value: unknown) {
+ setData((prev) => (prev ? setByPath(prev, path, value) : prev));
+ }
+
+ async function handleSave() {
+ if (readOnly || !data) return;
+ setSaving(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await notifyConfigSave(() =>
+ api.saveAddonConfig(filename, data),
+ );
+ setMessage(res.message || "Saved.");
+ await load();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Save failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!data || !schema) {
+ return (
+
+ {error || "Failed to load"}
+
+ );
+ }
+
+ const dirty =
+ original && JSON.stringify(data) !== JSON.stringify(original);
+
+ return (
+
+
+ {dirty ? (
+ Unsaved changes
+ ) : null}
+
+ Reload
+
+ {!readOnly ? (
+
+
+ {saving ? "Saving…" : "Save"}
+
+ ) : (
+ View only
+ )}
+
+
+ {error ? (
+
+
+ {error}
+
+ ) : null}
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+ {schema.map((section) => (
+
+
+ {section.title}
+ {section.description ? (
+ {section.description}
+ ) : null}
+
+
+ {section.fields.map((field) => (
+
+ patch(field.path, v)}
+ />
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/dashboard/src/components/app-sidebar.tsx b/dashboard/src/components/app-sidebar.tsx
new file mode 100644
index 0000000..74ea8c3
--- /dev/null
+++ b/dashboard/src/components/app-sidebar.tsx
@@ -0,0 +1,268 @@
+import type { IconSvgElement } from "@hugeicons/react";
+import {
+ AiBrain01Icon,
+ ArrowDown01Icon,
+ BotIcon,
+ CommandIcon,
+ ComputerTerminal01Icon,
+ DashboardSquare02Icon,
+ File02Icon,
+ Message01Icon,
+ PlusSignIcon,
+ PackageIcon,
+ Settings02Icon,
+ Ticket01Icon,
+ UserMultiple02Icon,
+} from "@hugeicons/core-free-icons";
+import { NavLink, useSearchParams } from "react-router-dom";
+import { useAuth } from "@/context/AuthContext";
+import { useBranding } from "@/context/BrandingContext";
+import { useBotStats } from "@/context/BotStatsContext";
+import { useAddonConfigs } from "@/hooks/use-addon-configs";
+import {
+ addonConfigFileParam,
+ addonConfigLabel,
+} from "@/lib/addon-config";
+import { canViewConfig, hasPermission } from "@/lib/permissions";
+import { Icon } from "@/components/icon";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarRail,
+} from "@/components/ui/sidebar";
+import { cn } from "@/lib/utils";
+
+const mainNav: {
+ title: string;
+ url: string;
+ icon: IconSvgElement;
+ end?: boolean;
+ permission?: string;
+}[] = [
+ { title: "Dashboard", url: "/", icon: DashboardSquare02Icon, end: true, permission: "overview" },
+ { title: "Logs", url: "/logs", icon: ComputerTerminal01Icon, permission: "logs" },
+ { title: "Settings", url: "/settings", icon: Settings02Icon, permission: "settings.view" },
+ { title: "Addons", url: "/addons", icon: PackageIcon, permission: "settings.view" },
+ { title: "Users", url: "/users", icon: UserMultiple02Icon, permission: "users.view" },
+];
+
+const sections: {
+ label: string;
+ items: { title: string; url: string; icon: IconSvgElement }[];
+}[] = [
+ {
+ label: "Tickets & support",
+ items: [
+ {
+ title: "Ticket system",
+ url: "/configs?file=ticket-panel",
+ icon: Ticket01Icon,
+ },
+ {
+ title: "Bot config",
+ url: "/configs?file=supportbot",
+ icon: BotIcon,
+ },
+ { title: "Transcripts", url: "/transcripts", icon: File02Icon },
+ ],
+ },
+ {
+ label: "Community",
+ items: [
+ {
+ title: "Messages",
+ url: "/configs?file=messages",
+ icon: Message01Icon,
+ },
+ {
+ title: "AI assistant",
+ url: "/configs?file=supportbot-ai",
+ icon: AiBrain01Icon,
+ },
+ ],
+ },
+ {
+ label: "Server control",
+ items: [
+ { title: "commands.yml", url: "/configs?file=commands", icon: CommandIcon },
+ {
+ title: "Staff roles",
+ url: "/configs?file=supportbot",
+ icon: UserMultiple02Icon,
+ },
+ ],
+ },
+];
+
+function configFileFromUrl(url: string) {
+ return new URL(url, "http://x").searchParams.get("file");
+}
+
+export function AppSidebar() {
+ const { branding } = useBranding();
+ const { permissions } = useAuth();
+ const { stats } = useBotStats();
+ const { files: addonConfigFiles } = useAddonConfigs();
+ const [searchParams] = useSearchParams();
+ const activeFile = searchParams.get("file");
+
+ const addonSection =
+ canViewConfig(permissions, "supportbot") && addonConfigFiles.length > 0
+ ? {
+ label: "Addons",
+ items: addonConfigFiles.map((filename) => ({
+ title: addonConfigLabel(filename),
+ url: `/configs?file=${addonConfigFileParam(filename)}`,
+ icon: PackageIcon,
+ })),
+ }
+ : null;
+
+ const visibleMainNav = mainNav.filter(
+ (item) => !item.permission || hasPermission(permissions, item.permission),
+ );
+
+ const visibleSections = sections
+ .map((section) => ({
+ ...section,
+ items: section.items.filter((item) => {
+ const file = configFileFromUrl(item.url);
+ if (file && item.url.includes("/configs")) {
+ return canViewConfig(permissions, file);
+ }
+ if (item.url === "/transcripts") {
+ return hasPermission(permissions, "transcripts");
+ }
+ if (item.url === "/commands") {
+ return canViewConfig(permissions, "commands");
+ }
+ return true;
+ }),
+ }))
+ .filter((section) => section.items.length > 0);
+
+ const allSections = addonSection
+ ? [...visibleSections, addonSection]
+ : visibleSections;
+
+ const serverLabel = stats
+ ? `${stats.servers} server${stats.servers === 1 ? "" : "s"}`
+ : "Your server";
+
+ return (
+
+
+
+ {branding.hasFavicon && branding.faviconUrl ? (
+
+ ) : null}
+
+ {branding.title}
+
+
+
+
+
+
+
+
+ {serverLabel}
+
+
+
+
+
+ Connected via API
+ {stats ? (
+
+ {stats.users.toLocaleString()} members tracked
+
+ ) : null}
+
+
+
+
+
+
+
+
+ {visibleMainNav.map((item) => (
+
+
+
+ cn(isActive && "bg-sidebar-accent font-medium")
+ }
+ >
+
+ {item.title}
+
+
+
+ ))}
+
+
+
+
+ {allSections.map((section) => (
+
+
+ {section.label}
+
+
+
+ {section.items.map((item) => {
+ const file = configFileFromUrl(item.url);
+ const isActive =
+ Boolean(file) &&
+ activeFile === file &&
+ item.url.includes("/configs");
+ return (
+
+
+
+
+ {item.title}
+
+
+
+ );
+ })}
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/config/commands-editor.tsx b/dashboard/src/components/config/commands-editor.tsx
new file mode 100644
index 0000000..ec9d0bd
--- /dev/null
+++ b/dashboard/src/components/config/commands-editor.tsx
@@ -0,0 +1,106 @@
+import type { IconSvgElement } from "@hugeicons/react";
+import { CommandIcon } from "@hugeicons/core-free-icons";
+import { Icon } from "@/components/icon";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+
+export interface CommandEntry {
+ key: string;
+ Command?: string;
+ Description?: string;
+ Enabled?: boolean;
+ Permission?: string[];
+}
+
+interface CommandsEditorProps {
+ data: Record;
+ onChange: (data: Record) => void;
+}
+
+function isCommandBlock(v: unknown): v is CommandEntry {
+ return (
+ v !== null &&
+ typeof v === "object" &&
+ "Command" in (v as Record)
+ );
+}
+
+export function getCommandEntries(
+ data: Record,
+): CommandEntry[] {
+ return Object.entries(data)
+ .filter(([, v]) => isCommandBlock(v))
+ .map(([key, v]) => ({ key, ...(v as CommandEntry) }));
+}
+
+export function CommandsEditor({ data, onChange }: CommandsEditorProps) {
+ const entries = getCommandEntries(data);
+
+ function updateEntry(
+ key: string,
+ patch: Partial,
+ ) {
+ const current = data[key] as Record;
+ onChange({
+ ...data,
+ [key]: { ...current, ...patch },
+ });
+ }
+
+ if (entries.length === 0) {
+ return (
+
+ No commands found in this config.
+
+ );
+ }
+
+ return (
+
+ {entries.map((entry) => (
+
+
+
+
+
+ {entry.key}
+
+
+
+ Enabled
+
+ updateEntry(entry.key, { Enabled: checked })
+ }
+ />
+
+
+ Slash command
+
+ updateEntry(entry.key, { Command: e.target.value })
+ }
+ className="border-border bg-secondary/30 font-mono"
+ />
+
+
+ Description
+
+ updateEntry(entry.key, { Description: e.target.value })
+ }
+ className="border-border bg-secondary/30"
+ />
+
+
+
+ ))}
+
+ );
+}
diff --git a/dashboard/src/components/config/config-field.tsx b/dashboard/src/components/config/config-field.tsx
new file mode 100644
index 0000000..e7e98d3
--- /dev/null
+++ b/dashboard/src/components/config/config-field.tsx
@@ -0,0 +1,248 @@
+import type { ConfigField } from "@/lib/config-schemas";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { ColorPicker } from "@/components/ui/color-picker";
+import { normalizeHex } from "@/lib/normalize-hex";
+import { StringListEditor } from "./string-list-editor";
+import { GoogleFontPicker } from "./google-font-picker";
+import {
+ DiscordResourcePicker,
+ type DiscordResourceKind,
+} from "./discord-resource-picker";
+import { EmojiPicker } from "./emoji-picker";
+
+const SECRET_PLACEHOLDER = "BOT_TOKEN";
+const AI_KEY_PLACEHOLDER = "MODEL_API_KEY";
+
+interface ConfigFieldInputProps {
+ field: ConfigField;
+ value: unknown;
+ onChange: (value: unknown) => void;
+ readOnly?: boolean;
+}
+
+export function ConfigFieldInput({
+ field,
+ value,
+ onChange,
+ readOnly,
+}: ConfigFieldInputProps) {
+ const id = field.path.replace(/\./g, "-");
+
+ if (field.type === "googleFont") {
+ return (
+
+ );
+ }
+
+ if (field.type === "boolean") {
+ return (
+
+
+
+ {field.label}
+
+ {field.description ? (
+
{field.description}
+ ) : null}
+
+
+
+ );
+ }
+
+ if (field.type === "stringList") {
+ return (
+
+
{field.label}
+ {field.description ? (
+
{field.description}
+ ) : null}
+
+
+ );
+ }
+
+ if (field.type === "select" && field.options) {
+ return (
+
+ {field.label}
+
+
+
+
+
+ {field.options.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+ );
+ }
+
+ if (field.type === "color") {
+ const hex = normalizeHex(String(value ?? "#000000"));
+ return (
+
+
{field.label}
+ {field.description ? (
+
{field.description}
+ ) : null}
+
+
+ onChange(e.target.value)}
+ onBlur={(e) => onChange(normalizeHex(e.target.value, hex))}
+ className="border-border bg-secondary/30 font-mono uppercase"
+ placeholder="#000000"
+ maxLength={7}
+ />
+
+
+ );
+ }
+
+ if (field.type === "emoji") {
+ return (
+
+ );
+ }
+
+ if (
+ field.type === "discordChannel" ||
+ field.type === "discordRole" ||
+ field.type === "discordCategory"
+ ) {
+ const kind: DiscordResourceKind =
+ field.type === "discordRole"
+ ? "role"
+ : field.type === "discordCategory"
+ ? "category"
+ : "channel";
+
+ return (
+
+ );
+ }
+
+ if (field.type === "secret") {
+ const display =
+ value === SECRET_PLACEHOLDER ||
+ value === AI_KEY_PLACEHOLDER ||
+ !value
+ ? ""
+ : "••••••••••••";
+ return (
+
+
{field.label}
+ {field.description ? (
+
{field.description}
+ ) : null}
+
{
+ const v = e.target.value;
+ if (v) onChange(v);
+ else onChange(field.placeholder ?? SECRET_PLACEHOLDER);
+ }}
+ className="border-border bg-secondary/30"
+ />
+
+ );
+ }
+
+ const sharedClass = "border-border bg-secondary/30";
+
+ if (field.type === "textarea") {
+ return (
+
+ {field.label}
+
+ );
+ }
+
+ return (
+
+
{field.label}
+ {field.description ? (
+
{field.description}
+ ) : null}
+
+ onChange(
+ field.type === "number"
+ ? Number(e.target.value)
+ : e.target.value,
+ )
+ }
+ placeholder={
+ field.placeholder ??
+ (field.type === "discordId" ? "Discord snowflake ID" : undefined)
+ }
+ className={sharedClass}
+ />
+
+ );
+}
diff --git a/dashboard/src/components/config/config-visual-editor.tsx b/dashboard/src/components/config/config-visual-editor.tsx
new file mode 100644
index 0000000..8c0fb35
--- /dev/null
+++ b/dashboard/src/components/config/config-visual-editor.tsx
@@ -0,0 +1,346 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { AlertCircleIcon, FloppyDiskIcon } from "@hugeicons/core-free-icons";
+import { api, maskConfigSecrets } from "@/api/client";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import { getSchema, type ConfigSchema } from "@/lib/config-schemas";
+import { getByPath, setByPath } from "@/lib/config-utils";
+import { mergeSchemaWithData } from "@/lib/merge-config-schema";
+import { useAuth } from "@/context/AuthContext";
+import { notifyConfigSave } from "@/lib/notify-config-save";
+import { Textarea } from "@/components/ui/textarea";
+import { ConfigFieldInput } from "./config-field";
+import { CommandsEditor } from "./commands-editor";
+
+type EditorMode = "visual" | "yaml";
+
+interface ConfigVisualEditorProps {
+ configFile: string;
+}
+
+function maskSecretsInData(
+ file: string,
+ data: Record,
+): Record {
+ let next = structuredClone(data);
+ if (file === "supportbot") {
+ const token = getByPath(next, "General.Token");
+ if (token && token !== "BOT_TOKEN") {
+ next = setByPath(next, "General.Token", "BOT_TOKEN");
+ }
+ }
+ if (file === "supportbot-ai") {
+ const key = getByPath(next, "General.Model_API_Key");
+ if (key && key !== "MODEL_API_KEY") {
+ next = setByPath(next, "General.Model_API_Key", "MODEL_API_KEY");
+ }
+ }
+ return next;
+}
+
+function buildUpdates(
+ file: string,
+ schema: ConfigSchema,
+ data: Record,
+ original: Record,
+): Record {
+ const updates: Record = {};
+ for (const section of schema) {
+ for (const field of section.fields) {
+ const val = getByPath(data, field.path);
+ const orig = getByPath(original, field.path);
+ if (field.type === "secret") {
+ if (
+ val === "BOT_TOKEN" ||
+ val === "MODEL_API_KEY" ||
+ val === "" ||
+ val === undefined
+ ) {
+ continue;
+ }
+ }
+ if (JSON.stringify(val) !== JSON.stringify(orig)) {
+ updates[`${file}:${field.path}`] = val;
+ }
+ }
+ }
+ return updates;
+}
+
+export function ConfigVisualEditor({ configFile }: ConfigVisualEditorProps) {
+ const { canEditConfig } = useAuth();
+ const readOnly = !canEditConfig(configFile);
+ const [data, setData] = useState | null>(null);
+ const [original, setOriginal] = useState | null>(
+ null,
+ );
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [message, setMessage] = useState("");
+ const [mode, setMode] = useState("visual");
+ const [rawYaml, setRawYaml] = useState("");
+ const [rawOriginal, setRawOriginal] = useState("");
+
+ const baseSchema = getSchema(configFile);
+
+ const effectiveSchema = useMemo(() => {
+ if (!baseSchema || baseSchema === "commands" || !data) return baseSchema;
+ return mergeSchemaWithData(baseSchema, data);
+ }, [baseSchema, data]);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.getConfigJson(configFile);
+ const raw = (res.data ?? {}) as Record;
+ const masked = maskSecretsInData(configFile, raw);
+ setData(masked);
+ setOriginal(masked);
+
+ const rawRes = await api.getConfigRaw(configFile);
+ const content = rawRes.data ?? "";
+ const maskedRaw = maskConfigSecrets(content, configFile);
+ setRawYaml(maskedRaw);
+ setRawOriginal(maskedRaw);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load config");
+ } finally {
+ setLoading(false);
+ }
+ }, [configFile]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ function patch(path: string, value: unknown) {
+ setData((prev) => (prev ? setByPath(prev, path, value) : prev));
+ }
+
+ async function handleSaveYaml() {
+ if (readOnly || rawYaml === rawOriginal) return;
+ setSaving(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await notifyConfigSave(() =>
+ api.saveConfigRaw(configFile, rawYaml),
+ );
+ setMessage(res.message || "Config saved.");
+ await load();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Save failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handleSave() {
+ if (readOnly || !data || !original) return;
+ if (mode === "yaml") {
+ await handleSaveYaml();
+ return;
+ }
+ setSaving(true);
+ setError("");
+ setMessage("");
+ try {
+ if (baseSchema === "commands") {
+ const updates: Record = {};
+ for (const key of Object.keys(data)) {
+ if (JSON.stringify(data[key]) !== JSON.stringify(original[key])) {
+ updates[`${configFile}:${key}`] = data[key];
+ }
+ }
+ if (Object.keys(updates).length === 0) {
+ setMessage("No changes to save.");
+ return;
+ }
+ const res = await notifyConfigSave(() =>
+ api.updateConfigFields(updates),
+ );
+ setMessage(res.message || "");
+ } else if (effectiveSchema && effectiveSchema !== "commands") {
+ const updates = buildUpdates(
+ configFile,
+ effectiveSchema,
+ data,
+ original,
+ );
+ if (Object.keys(updates).length === 0) {
+ setMessage("No changes to save.");
+ return;
+ }
+ const res = await notifyConfigSave(() =>
+ api.updateConfigFields(updates),
+ );
+ setMessage(res.message || "");
+ }
+ await load();
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Save failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ if (baseSchema === null) {
+ return (
+
+
+ No visual editor available for this file.
+
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ {error || "Failed to load"}
+
+ );
+ }
+
+ const dirtyVisual =
+ original && JSON.stringify(data) !== JSON.stringify(original);
+ const dirtyYaml = rawYaml !== rawOriginal;
+ const dirty = mode === "yaml" ? dirtyYaml : dirtyVisual;
+
+ return (
+
+
+
+ setMode("visual")}
+ >
+ Visual
+
+ setMode("yaml")}
+ >
+ YAML
+
+
+
+ {dirty ? (
+ Unsaved changes
+ ) : null}
+
+ Reload
+
+ {!readOnly ? (
+
+
+ {saving ? "Saving…" : "Save"}
+
+ ) : (
+ View only
+ )}
+
+
+
+ {error ? (
+
+
+ {error}
+
+ ) : null}
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+ {mode === "yaml" ? (
+
+
+ Raw YAML
+
+ Edit the full file. Use this for nested lists (e.g. welcome buttons)
+ or any option not in Visual mode.
+
+
+
+
+
+ ) : baseSchema === "commands" ? (
+
setData(next)}
+ />
+ ) : effectiveSchema && effectiveSchema !== "commands" ? (
+ effectiveSchema.map((section) => (
+
+
+ {section.title}
+ {section.description ? (
+ {section.description}
+ ) : null}
+
+
+ {section.fields.map((field) => (
+
+ patch(field.path, v)}
+ />
+
+ ))}
+
+
+ ))
+ ) : null}
+
+ );
+}
diff --git a/dashboard/src/components/config/discord-resource-picker.tsx b/dashboard/src/components/config/discord-resource-picker.tsx
new file mode 100644
index 0000000..fba7e30
--- /dev/null
+++ b/dashboard/src/components/config/discord-resource-picker.tsx
@@ -0,0 +1,136 @@
+import { useEffect, useMemo, useState } from "react";
+import { api } from "@/api/client";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+export type DiscordResourceKind = "channel" | "role" | "category";
+
+interface DiscordResourcePickerProps {
+ id: string;
+ label: string;
+ description?: string;
+ kind: DiscordResourceKind;
+ value: unknown;
+ onChange: (value: unknown) => void;
+ readOnly?: boolean;
+}
+
+import type { GuildResources } from "@/api/client";
+
+export function DiscordResourcePicker({
+ id,
+ label,
+ description,
+ kind,
+ value,
+ onChange,
+ readOnly,
+}: DiscordResourcePickerProps) {
+ const [resources, setResources] = useState(null);
+ const [loadError, setLoadError] = useState("");
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ setLoading(true);
+ api
+ .getGuildResources()
+ .then((res) => {
+ if (cancelled) return;
+ if (res.data) {
+ setResources(res.data);
+ setLoadError("");
+ }
+ })
+ .catch((err) => {
+ if (!cancelled) {
+ setLoadError(
+ err instanceof Error ? err.message : "Could not load server list",
+ );
+ }
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const options = useMemo(() => {
+ if (!resources) return [];
+ if (kind === "role") return resources.roles;
+ if (kind === "category") return resources.categories;
+ return resources.channels;
+ }, [resources, kind]);
+
+ const current = String(value ?? "");
+ const matched = options.find((o) => o.id === current);
+
+ return (
+
+
{label}
+ {description ? (
+
{description}
+ ) : null}
+
+ {resources ? (
+
onChange(next)}
+ >
+
+
+
+
+ {options.map((opt) => (
+
+ {opt.name}
+
+ {opt.id}
+
+
+ ))}
+
+
+ ) : null}
+
+
onChange(e.target.value)}
+ placeholder={
+ kind === "role"
+ ? "Role ID (ROLE_ID)"
+ : kind === "category"
+ ? "Category ID"
+ : "Channel ID"
+ }
+ className="border-border bg-secondary/30 font-mono text-sm"
+ />
+
+ {loadError ? (
+
{loadError}
+ ) : null}
+ {!loadError && !resources && !loading ? (
+
+ Set General.GuildId and invite the bot to pick from a list.
+
+ ) : null}
+
+ );
+}
diff --git a/dashboard/src/components/config/emoji-picker.tsx b/dashboard/src/components/config/emoji-picker.tsx
new file mode 100644
index 0000000..f509242
--- /dev/null
+++ b/dashboard/src/components/config/emoji-picker.tsx
@@ -0,0 +1,304 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Search01Icon } from "@hugeicons/core-free-icons";
+import { api, type GuildResources } from "@/api/client";
+import { Icon } from "@/components/icon";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { UnicodeEmojiPanel } from "@/components/config/unicode-emoji-panel";
+import {
+ emojiCdnUrl,
+ formatCustomEmoji,
+ parseEmojiValue,
+ resolveEmojiPreview,
+ type GuildEmojiOption,
+} from "@/lib/emoji-format";
+import { cn } from "@/lib/utils";
+
+interface EmojiPickerProps {
+ id: string;
+ label: string;
+ description?: string;
+ value: unknown;
+ onChange: (value: unknown) => void;
+ readOnly?: boolean;
+}
+
+function EmojiPreview({
+ value,
+ guildEmojis,
+ size = "md",
+}: {
+ value: unknown;
+ guildEmojis: GuildEmojiOption[];
+ size?: "md" | "lg";
+}) {
+ const parsed = resolveEmojiPreview(value, guildEmojis);
+ const imgSize = size === "lg" ? "size-10" : "size-7";
+ const textSize = size === "lg" ? "text-3xl" : "text-2xl";
+
+ if (parsed.kind === "empty") {
+ return (
+ No emoji selected
+ );
+ }
+
+ if (parsed.kind === "custom" && parsed.id) {
+ return (
+
+
+
+ :{parsed.name}:
+
+ {parsed.value}
+
+
+
+ );
+ }
+
+ return (
+
+ {parsed.value}
+
+ );
+}
+
+export function EmojiPicker({
+ id,
+ label,
+ description,
+ value,
+ onChange,
+ readOnly,
+}: EmojiPickerProps) {
+ const [open, setOpen] = useState(false);
+ const [tab, setTab] = useState<"server" | "unicode">("server");
+ const [resources, setResources] = useState(null);
+ const [loadError, setLoadError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [query, setQuery] = useState("");
+
+ const loadGuildEmojis = useCallback(async (refresh = false) => {
+ setLoading(true);
+ setLoadError("");
+ try {
+ const res = await api.getGuildResources(refresh);
+ if (res.data) {
+ setResources(res.data);
+ if (!Array.isArray(res.data.emojis)) {
+ setLoadError(
+ "Server response is missing emojis. Restart the bot after updating.",
+ );
+ }
+ } else {
+ setLoadError("No guild data returned.");
+ }
+ } catch (err) {
+ setLoadError(
+ err instanceof Error ? err.message : "Could not load server emojis",
+ );
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!open) return;
+ void loadGuildEmojis(true);
+ }, [open, loadGuildEmojis]);
+
+ const guildEmojis = useMemo(
+ () => (Array.isArray(resources?.emojis) ? resources.emojis : []),
+ [resources],
+ );
+
+ const filteredGuild = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ if (!q) return guildEmojis;
+ return guildEmojis.filter((e) => e.name.toLowerCase().includes(q));
+ }, [guildEmojis, query]);
+
+ const current = String(value ?? "");
+ const parsed = parseEmojiValue(current);
+
+ function selectUnicode(emoji: string) {
+ onChange(emoji);
+ setOpen(false);
+ }
+
+ function selectGuild(emoji: GuildEmojiOption) {
+ onChange(formatCustomEmoji(emoji.name, emoji.id, emoji.animated));
+ setOpen(false);
+ }
+
+ function clearEmoji() {
+ onChange("");
+ }
+
+ return (
+
+
{label}
+ {description ? (
+
{description}
+ ) : null}
+
+
+
+
+
+
+ setOpen(true)}
+ >
+ Choose emoji
+
+ {current ? (
+
+ Clear
+
+ ) : null}
+
+
+
+
+
+
+ Choose emoji
+
+ Server emojis save as{" "}
+ <:name:id>
+ . Standard emojis save as Unicode characters.
+
+
+
+ setTab(v as "server" | "unicode")}
+ className="flex min-h-0 flex-1 flex-col"
+ >
+
+ Server
+ All emojis
+
+
+
+
+
+ setQuery(e.target.value)}
+ className="h-9 border-border bg-secondary/30 pl-9"
+ />
+
+
+ {resources ? (
+
+ {resources.guildName} · {guildEmojis.length} custom emoji
+ {guildEmojis.length === 1 ? "" : "s"}
+
+ ) : null}
+
+
+ {loading ? (
+
+ {Array.from({ length: 24 }).map((_, i) => (
+
+ ))}
+
+ ) : loadError ? (
+
+
{loadError}
+
void loadGuildEmojis(true)}
+ >
+ Retry
+
+
+ ) : filteredGuild.length === 0 ? (
+
+ {guildEmojis.length === 0
+ ? "No custom emojis in this server, or the bot needs the Guild Emojis intent (restart after update)."
+ : "No emojis match your search."}
+
+ ) : (
+
+ {filteredGuild.map((emoji) => {
+ const selected =
+ parsed.kind === "custom" && parsed.id === emoji.id;
+ return (
+
selectGuild(emoji)}
+ >
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
onChange(e.target.value)}
+ placeholder="Unicode or <:name:id>"
+ className="border-border bg-secondary/30 font-mono text-xs"
+ />
+
+ );
+}
diff --git a/dashboard/src/components/config/google-font-picker.tsx b/dashboard/src/components/config/google-font-picker.tsx
new file mode 100644
index 0000000..c971ef2
--- /dev/null
+++ b/dashboard/src/components/config/google-font-picker.tsx
@@ -0,0 +1,74 @@
+import { useEffect } from "react";
+import type { ConfigField } from "@/lib/config-schemas";
+import {
+ GOOGLE_FONTS,
+ SYSTEM_FONT_ID,
+ fontFamilyCss,
+ injectGoogleFontsStylesheet,
+ normalizeGoogleFontValue,
+} from "@/lib/google-fonts";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+interface GoogleFontPickerProps {
+ field: ConfigField;
+ value: unknown;
+ onChange: (value: unknown) => void;
+ readOnly?: boolean;
+}
+
+export function GoogleFontPicker({
+ field,
+ value,
+ onChange,
+ readOnly,
+}: GoogleFontPickerProps) {
+ const id = field.path.replace(/\./g, "-");
+ const selected = normalizeGoogleFontValue(value);
+
+ useEffect(() => {
+ injectGoogleFontsStylesheet();
+ }, []);
+
+ return (
+
+
{field.label}
+ {field.description ? (
+
{field.description}
+ ) : null}
+
+
+
+
+
+
+ System default
+
+ {GOOGLE_FONTS.map((font) => (
+
+ {font.label}
+
+ ))}
+
+
+
+ );
+}
diff --git a/dashboard/src/components/config/string-list-editor.tsx b/dashboard/src/components/config/string-list-editor.tsx
new file mode 100644
index 0000000..24dbe4a
--- /dev/null
+++ b/dashboard/src/components/config/string-list-editor.tsx
@@ -0,0 +1,56 @@
+import { Delete02Icon, PlusSignIcon } from "@hugeicons/core-free-icons";
+import { Icon } from "@/components/icon";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+
+interface StringListEditorProps {
+ value: string[];
+ onChange: (value: string[]) => void;
+ placeholder?: string;
+}
+
+export function StringListEditor({
+ value,
+ onChange,
+ placeholder = "Add item…",
+}: StringListEditorProps) {
+ const items = Array.isArray(value) ? value : [];
+
+ return (
+
+ {items.map((item, index) => (
+
+ {
+ const next = [...items];
+ next[index] = e.target.value;
+ onChange(next);
+ }}
+ placeholder={placeholder}
+ className="border-border bg-secondary/30"
+ />
+ onChange(items.filter((_, i) => i !== index))}
+ >
+
+
+
+ ))}
+
onChange([...items, ""])}
+ >
+
+ Add line
+
+
+ );
+}
diff --git a/dashboard/src/components/config/unicode-emoji-panel.tsx b/dashboard/src/components/config/unicode-emoji-panel.tsx
new file mode 100644
index 0000000..16531a6
--- /dev/null
+++ b/dashboard/src/components/config/unicode-emoji-panel.tsx
@@ -0,0 +1,41 @@
+import { useEffect, useRef } from "react";
+import "emoji-picker-element";
+
+interface UnicodeEmojiPanelProps {
+ onPick: (unicode: string) => void;
+}
+
+export function UnicodeEmojiPanel({ onPick }: UnicodeEmojiPanelProps) {
+ const hostRef = useRef(null);
+
+ useEffect(() => {
+ const host = hostRef.current;
+ if (!host) return;
+
+ const picker = document.createElement("emoji-picker");
+ picker.className = "unicode-emoji-picker-el";
+ picker.setAttribute("data-theme", "dark");
+
+ const handler = (event: Event) => {
+ const detail = (event as CustomEvent<{ unicode?: string; emoji?: { unicode?: string } }>)
+ .detail;
+ const unicode = detail?.unicode ?? detail?.emoji?.unicode;
+ if (unicode) onPick(unicode);
+ };
+
+ picker.addEventListener("emoji-click", handler);
+ host.appendChild(picker);
+
+ return () => {
+ picker.removeEventListener("emoji-click", handler);
+ picker.remove();
+ };
+ }, [onPick]);
+
+ return (
+
+ );
+}
diff --git a/dashboard/src/components/dashboard-alerts/dashboard-alert-card.tsx b/dashboard/src/components/dashboard-alerts/dashboard-alert-card.tsx
new file mode 100644
index 0000000..d7b9e5c
--- /dev/null
+++ b/dashboard/src/components/dashboard-alerts/dashboard-alert-card.tsx
@@ -0,0 +1,112 @@
+import { Link } from "react-router-dom";
+import {
+ AlertCircleIcon,
+ InformationCircleIcon,
+ Logout03Icon,
+} from "@hugeicons/core-free-icons";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ SEVERITY_STYLES,
+ type DashboardAlertPayload,
+ type DashboardAlertRenderContext,
+ type DashboardAlertSeverity,
+} from "@/lib/dashboard-alerts";
+
+function severityIcon(severity: DashboardAlertSeverity) {
+ if (severity === "info") return InformationCircleIcon;
+ return AlertCircleIcon;
+}
+
+interface DashboardAlertCardProps {
+ alert: DashboardAlertPayload;
+ context: DashboardAlertRenderContext;
+}
+
+export function DashboardAlertCard({ alert, context }: DashboardAlertCardProps) {
+ const styles = SEVERITY_STYLES[alert.severity];
+ const extraGuilds = alert.meta?.extraGuilds ?? [];
+ const guildStatus = alert.meta?.status;
+ const moduleProgress = alert.meta?.progress;
+
+ return (
+
+
+ {alert.title}
+
+ {alert.message}
+
+ {alert.category === "guild" && guildStatus === "missing_config" ? (
+
+ Open{" "}
+
+ Bot config
+ {" "}
+ and set Server (guild) ID {" "}
+ (enable Developer Mode in Discord, then right-click your server → Copy
+ Server ID).
+
+ ) : null}
+
+ {alert.category === "modules" && moduleProgress ? (
+
+ Progress: {moduleProgress}
+
+ ) : null}
+
+ {extraGuilds.length > 0 ? (
+
+ {extraGuilds.map((g) => (
+
+
+ {g.name}
+ {g.id}
+
+ {context.canLeaveGuild ? (
+ void context.onLeaveGuild(g.id)}
+ >
+
+ {context.leavingGuildId === g.id ? "Leaving…" : "Leave server"}
+
+ ) : null}
+
+ ))}
+
+ ) : null}
+
+ {context.actionError ? (
+ {context.actionError}
+ ) : null}
+
+
+ {alert.action ? (
+
+ {alert.action.label}
+
+ ) : null}
+ {alert.category === "guild" ? (
+
+ The dashboard stays online while you fix this. Some features may be
+ limited until setup is complete.
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/dashboard/src/components/dashboard-alerts/dashboard-alerts.tsx b/dashboard/src/components/dashboard-alerts/dashboard-alerts.tsx
new file mode 100644
index 0000000..e9ebe70
--- /dev/null
+++ b/dashboard/src/components/dashboard-alerts/dashboard-alerts.tsx
@@ -0,0 +1,65 @@
+import { useCallback, useEffect, useState } from "react";
+import { api } from "@/api/client";
+import { useAuth } from "@/context/AuthContext";
+import {
+ dashboardAlertBuilder,
+ type DashboardAlertPayload,
+} from "@/lib/dashboard-alerts";
+import { DashboardAlertCard } from "./dashboard-alert-card";
+
+const REFRESH_MS = 15_000;
+
+export function DashboardAlerts() {
+ const { hasPermission } = useAuth();
+ const canLeave = hasPermission("settings.update");
+ const [alerts, setAlerts] = useState([]);
+ const [leavingGuildId, setLeavingGuildId] = useState(null);
+ const [actionError, setActionError] = useState("");
+
+ const refresh = useCallback(async () => {
+ try {
+ const next = await dashboardAlertBuilder.loadAll();
+ setAlerts(next);
+ setActionError("");
+ } catch {
+ /* ignore — banners are best-effort */
+ }
+ }, []);
+
+ useEffect(() => {
+ void refresh();
+ const id = window.setInterval(() => void refresh(), REFRESH_MS);
+ return () => window.clearInterval(id);
+ }, [refresh]);
+
+ async function leaveGuild(guildId: string) {
+ if (!canLeave) return;
+ setLeavingGuildId(guildId);
+ setActionError("");
+ try {
+ await api.leaveGuild(guildId);
+ await refresh();
+ } catch (err) {
+ setActionError(err instanceof Error ? err.message : "Could not leave server");
+ } finally {
+ setLeavingGuildId(null);
+ }
+ }
+
+ if (alerts.length === 0) return null;
+
+ const renderContext = {
+ canLeaveGuild: canLeave,
+ onLeaveGuild: leaveGuild,
+ leavingGuildId,
+ actionError,
+ };
+
+ return (
+
+ {alerts.map((alert) => (
+
+ ))}
+
+ );
+}
diff --git a/dashboard/src/components/dashboard-alerts/index.ts b/dashboard/src/components/dashboard-alerts/index.ts
new file mode 100644
index 0000000..ea30090
--- /dev/null
+++ b/dashboard/src/components/dashboard-alerts/index.ts
@@ -0,0 +1,2 @@
+export { DashboardAlerts } from "./dashboard-alerts";
+export { DashboardAlertCard } from "./dashboard-alert-card";
diff --git a/dashboard/src/components/dashboard-header.tsx b/dashboard/src/components/dashboard-header.tsx
new file mode 100644
index 0000000..a0c2c37
--- /dev/null
+++ b/dashboard/src/components/dashboard-header.tsx
@@ -0,0 +1,117 @@
+import {
+ ArrowDown01Icon,
+ PlusSignIcon,
+} from "@hugeicons/core-free-icons";
+import { DashboardSearch } from "@/components/dashboard-search";
+import { useBotStats } from "@/context/BotStatsContext";
+import { Icon } from "@/components/icon";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { NotificationBell } from "@/components/notification-bell";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { SidebarTrigger } from "@/components/ui/sidebar";
+import { displayDiscordName, useAuth } from "@/context/AuthContext";
+
+export function DashboardHeader() {
+ const { stats } = useBotStats();
+ const { user, logout } = useAuth();
+ const botName = stats?.bot.username ?? "SupportBot";
+ const accountName = user ? displayDiscordName(user) : botName;
+ const accountAvatar = user?.avatar ?? stats?.bot.avatar;
+ const inviteUrl = stats?.bot.inviteUrl;
+ const dateLabel = new Intl.DateTimeFormat("en-US", {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ }).format(new Date());
+
+ return (
+
+
+
+
+ {dateLabel}
+
+
+
+
+
+ {inviteUrl ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {accountName.slice(0, 2).toUpperCase()}
+
+
+
+ {accountName}
+
+
+
+
+
+ Account
+
+ {user ? (
+
+ {user.username}
+ {user.id ? ` · ${user.id}` : ""}
+
+ ) : null}
+ {
+ void logout();
+ }}
+ >
+ Sign out
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/dashboard-search.tsx b/dashboard/src/components/dashboard-search.tsx
new file mode 100644
index 0000000..e624d3b
--- /dev/null
+++ b/dashboard/src/components/dashboard-search.tsx
@@ -0,0 +1,174 @@
+import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
+import { createPortal } from "react-dom";
+import { useNavigate } from "react-router-dom";
+import { Search01Icon } from "@hugeicons/core-free-icons";
+import { Icon } from "@/components/icon";
+import { Input } from "@/components/ui/input";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import { cn } from "@/lib/utils";
+import {
+ groupSearchResults,
+ searchDashboard,
+ type SearchItem,
+} from "@/lib/search-index";
+
+export function DashboardSearch({ className }: { className?: string }) {
+ const navigate = useNavigate();
+ const [query, setQuery] = useState("");
+ const [open, setOpen] = useState(false);
+ const containerRef = useRef(null);
+ const panelRef = useRef(null);
+ const inputRef = useRef(null);
+ const [panelRect, setPanelRect] = useState<{
+ top: number;
+ left: number;
+ width: number;
+ } | null>(null);
+
+ const results = useMemo(() => searchDashboard(query, 14), [query]);
+ const groups = useMemo(() => groupSearchResults(results), [results]);
+ const showPanel = open && query.trim().length > 0;
+
+ const updatePanelRect = () => {
+ const el = inputRef.current;
+ if (!el) return;
+ const rect = el.getBoundingClientRect();
+ setPanelRect({
+ top: rect.bottom + 6,
+ left: rect.left,
+ width: rect.width,
+ });
+ };
+
+ useLayoutEffect(() => {
+ if (!showPanel) {
+ setPanelRect(null);
+ return;
+ }
+ updatePanelRect();
+ window.addEventListener("resize", updatePanelRect);
+ window.addEventListener("scroll", updatePanelRect, true);
+ return () => {
+ window.removeEventListener("resize", updatePanelRect);
+ window.removeEventListener("scroll", updatePanelRect, true);
+ };
+ }, [showPanel, query]);
+
+ useEffect(() => {
+ function onPointerDown(e: MouseEvent) {
+ const target = e.target as Node;
+ if (
+ containerRef.current?.contains(target) ||
+ panelRef.current?.contains(target)
+ ) {
+ return;
+ }
+ setOpen(false);
+ }
+ document.addEventListener("mousedown", onPointerDown);
+ return () => document.removeEventListener("mousedown", onPointerDown);
+ }, []);
+
+ function selectItem(item: SearchItem) {
+ navigate(item.href);
+ setQuery("");
+ setOpen(false);
+ inputRef.current?.blur();
+ }
+
+ return (
+
+
+
{
+ setQuery(e.target.value);
+ setOpen(true);
+ }}
+ onFocus={() => setOpen(true)}
+ onKeyDown={(e) => {
+ if (e.key === "Escape") {
+ setOpen(false);
+ setQuery("");
+ inputRef.current?.blur();
+ }
+ }}
+ placeholder="Search modules, configs…"
+ className="h-9 border-border bg-secondary/50 pl-9"
+ role="combobox"
+ aria-expanded={showPanel}
+ aria-controls="dashboard-search-results"
+ aria-autocomplete="list"
+ />
+
+ {showPanel && panelRect
+ ? createPortal(
+
+
+
+ {results.length === 0 ? (
+
+ No results for “{query.trim()}”
+
+ ) : (
+ groups.map((group) => (
+
+ {group.items.map((item) => (
+ selectItem(item)}
+ className="cursor-pointer gap-3 rounded-lg py-2.5 aria-selected:bg-accent"
+ >
+ {item.icon ? (
+
+
+
+ ) : null}
+
+
+ {item.title}
+
+ {item.subtitle ? (
+
+ {item.subtitle}
+
+ ) : null}
+
+
+ ))}
+
+ ))
+ )}
+
+
+
,
+ document.body,
+ )
+ : null}
+
+ );
+}
diff --git a/dashboard/src/components/home-redirect.tsx b/dashboard/src/components/home-redirect.tsx
new file mode 100644
index 0000000..fd03e55
--- /dev/null
+++ b/dashboard/src/components/home-redirect.tsx
@@ -0,0 +1,14 @@
+import { Navigate } from "react-router-dom";
+import { useAuth } from "@/context/AuthContext";
+import { firstAllowedPath, hasPermission } from "@/lib/permissions";
+import Overview from "@/pages/Overview";
+
+export function HomeRedirect() {
+ const { permissions } = useAuth();
+
+ if (hasPermission(permissions, "overview")) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/dashboard/src/components/icon.tsx b/dashboard/src/components/icon.tsx
new file mode 100644
index 0000000..35af6f8
--- /dev/null
+++ b/dashboard/src/components/icon.tsx
@@ -0,0 +1,26 @@
+import { HugeiconsIcon } from "@hugeicons/react";
+import type { IconSvgElement } from "@hugeicons/react";
+import { cn } from "@/lib/utils";
+
+interface IconProps {
+ icon: IconSvgElement;
+ className?: string;
+ size?: number;
+ strokeWidth?: number;
+}
+
+export function Icon({
+ icon,
+ className,
+ size = 18,
+ strokeWidth = 1.75,
+}: IconProps) {
+ return (
+
+ );
+}
diff --git a/dashboard/src/components/notification-bell.tsx b/dashboard/src/components/notification-bell.tsx
new file mode 100644
index 0000000..ada28fc
--- /dev/null
+++ b/dashboard/src/components/notification-bell.tsx
@@ -0,0 +1,168 @@
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import {
+ Cancel01Icon,
+ Notification01Icon,
+} from "@hugeicons/core-free-icons";
+import { useNotifications } from "@/context/NotificationsContext";
+import { Icon } from "@/components/icon";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import type { DashboardNotification } from "@/api/client";
+import { cn } from "@/lib/utils";
+
+function formatRelativeTime(ts: number) {
+ const sec = Math.floor((Date.now() - ts) / 1000);
+ if (sec < 60) return "Just now";
+ const min = Math.floor(sec / 60);
+ if (min < 60) return `${min}m ago`;
+ const hr = Math.floor(min / 60);
+ if (hr < 24) return `${hr}h ago`;
+ const day = Math.floor(hr / 24);
+ return `${day}d ago`;
+}
+
+function typeDotClass(type: DashboardNotification["type"]) {
+ switch (type) {
+ case "error":
+ return "bg-destructive";
+ case "update":
+ return "bg-amber-500";
+ case "bot":
+ return "bg-emerald-500";
+ default:
+ return "bg-primary";
+ }
+}
+
+export function NotificationBell() {
+ const navigate = useNavigate();
+ const { items, unreadCount, loading, markAllRead, dismiss, refresh } =
+ useNotifications();
+ const [open, setOpen] = useState(false);
+
+ const onOpenChange = (next: boolean) => {
+ setOpen(next);
+ if (next) {
+ void refresh();
+ if (unreadCount > 0) void markAllRead();
+ }
+ };
+
+ const onSelect = (n: DashboardNotification) => {
+ setOpen(false);
+ if (n.href) navigate(n.href);
+ };
+
+ return (
+
+
+
+
+ {unreadCount > 0 && (
+
+ {unreadCount > 9 ? "9+" : unreadCount}
+
+ )}
+
+
+
+
+
+ Notifications
+
+ {items.length > 0 && (
+ void markAllRead()}
+ >
+ Mark all read
+
+ )}
+
+
+
+ {loading && items.length === 0 ? (
+
+ Loading…
+
+ ) : items.length === 0 ? (
+
+ No notifications yet. Bot restarts and update checks will appear
+ here.
+
+ ) : (
+
+ {items.map((n) => (
+
+
+ onSelect(n)}
+ >
+
+ {n.title}
+
+
+ {n.message}
+
+
+ {formatRelativeTime(n.createdAt)}
+
+
+ {
+ e.stopPropagation();
+ void dismiss(n.id);
+ }}
+ >
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ setOpen(false)}>
+ Open settings (updates)
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/overview/active-module-card.tsx b/dashboard/src/components/overview/active-module-card.tsx
new file mode 100644
index 0000000..3c44b60
--- /dev/null
+++ b/dashboard/src/components/overview/active-module-card.tsx
@@ -0,0 +1,52 @@
+import { Link } from "react-router-dom";
+import type { IconSvgElement } from "@hugeicons/react";
+import { Icon } from "@/components/icon";
+import { cn } from "@/lib/utils";
+import type { ModuleStatus } from "@/components/overview/module-strip-card";
+
+const BADGE_CLASS: Record = {
+ todo: "bg-rose-500/15 text-rose-400",
+ progress: "bg-amber-500/15 text-amber-400",
+ done: "bg-emerald-500/15 text-emerald-400",
+};
+
+interface ActiveModuleCardProps {
+ title: string;
+ icon: IconSvgElement;
+ href: string;
+ badge?: string;
+ status?: ModuleStatus;
+}
+
+export function ActiveModuleCard({
+ title,
+ icon,
+ href,
+ badge = "Configured",
+ status = "done",
+}: ActiveModuleCardProps) {
+ return (
+
+
+
+
+
+
{title}
+
+ {badge}
+
+
+
+ );
+}
diff --git a/dashboard/src/components/overview/donut-chart.tsx b/dashboard/src/components/overview/donut-chart.tsx
new file mode 100644
index 0000000..8e620a7
--- /dev/null
+++ b/dashboard/src/components/overview/donut-chart.tsx
@@ -0,0 +1,62 @@
+import { cn } from "@/lib/utils";
+
+interface DonutChartProps {
+ percent: number;
+ size?: number;
+ className?: string;
+ trackClassName?: string;
+ strokeClassName?: string;
+}
+
+export function DonutChart({
+ percent,
+ size = 128,
+ className,
+ trackClassName = "stroke-white/20",
+ strokeClassName = "stroke-white",
+}: DonutChartProps) {
+ const r = 42;
+ const c = 2 * Math.PI * r;
+ const clamped = Math.min(100, Math.max(0, percent));
+ const offset = c - (clamped / 100) * c;
+
+ return (
+
+
+
+
+
+
+
+ {Math.round(clamped)}%
+
+
+ Complete
+
+
+
+ );
+}
diff --git a/dashboard/src/components/overview/module-strip-card.tsx b/dashboard/src/components/overview/module-strip-card.tsx
new file mode 100644
index 0000000..fd18f15
--- /dev/null
+++ b/dashboard/src/components/overview/module-strip-card.tsx
@@ -0,0 +1,102 @@
+import { Link } from "react-router-dom";
+import {
+ ArrowRight01Icon,
+ Link01Icon,
+ Settings02Icon,
+} from "@hugeicons/core-free-icons";
+import type { IconSvgElement } from "@hugeicons/react";
+import { Icon } from "@/components/icon";
+import { cn } from "@/lib/utils";
+
+export type ModuleStatus = "todo" | "progress" | "done";
+
+const statusStyles: Record<
+ ModuleStatus,
+ { dot: string; badge: string; label: string }
+> = {
+ todo: {
+ dot: "bg-rose-500",
+ badge: "bg-rose-500/15 text-rose-400",
+ label: "Needs setup",
+ },
+ progress: {
+ dot: "bg-amber-400",
+ badge: "bg-amber-500/15 text-amber-400",
+ label: "In progress",
+ },
+ done: {
+ dot: "bg-sky-400",
+ badge: "bg-sky-500/15 text-sky-400",
+ label: "Active",
+ },
+};
+
+interface ModuleStripCardProps {
+ title: string;
+ subtitle: string;
+ icon: IconSvgElement;
+ href: string;
+ status: ModuleStatus;
+ meta?: string;
+}
+
+export function ModuleStripCard({
+ title,
+ subtitle,
+ icon,
+ href,
+ status,
+ meta = "Open in dashboard",
+}: ModuleStripCardProps) {
+ const s = statusStyles[status];
+
+ return (
+
+
+
+ {subtitle}
+
+
+
+
{title}
+
+ {s.label}
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/overview/overview-aside.tsx b/dashboard/src/components/overview/overview-aside.tsx
new file mode 100644
index 0000000..de738e1
--- /dev/null
+++ b/dashboard/src/components/overview/overview-aside.tsx
@@ -0,0 +1,163 @@
+import { Link } from "react-router-dom";
+import {
+ ArrowRight01Icon,
+ Download04Icon,
+ Rocket01Icon,
+ Settings02Icon,
+} from "@hugeicons/core-free-icons";
+import type { BotStats } from "@/api/client";
+import { formatUptime } from "@/api/client";
+import { Icon } from "@/components/icon";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+
+interface OverviewAsideProps {
+ stats: BotStats;
+}
+
+export function OverviewAside({ stats }: OverviewAsideProps) {
+ const activity = [
+ {
+ id: "ping",
+ label: "Gateway latency",
+ detail: `${stats.bot.ping}ms`,
+ badge: stats.bot.ping < 200 ? "Good" : "High",
+ badgeClass:
+ stats.bot.ping < 200
+ ? "bg-emerald-500/15 text-emerald-400"
+ : "bg-amber-500/15 text-amber-400",
+ },
+ {
+ id: "uptime",
+ label: "Bot uptime",
+ detail: formatUptime(stats.bot.uptime),
+ badge: "Live",
+ badgeClass: "bg-primary/20 text-primary",
+ },
+ {
+ id: "ram",
+ label: "Memory usage",
+ detail: `${stats.hosting.ram_used} / ${stats.hosting.ram_total}`,
+ badge: `${stats.hosting.ram_percent}%`,
+ badgeClass: "bg-sky-500/15 text-sky-400",
+ },
+ ];
+
+ return (
+
+
+
+
+ Connected servers ({stats.servers})
+
+
+ +
+
+
+
+
+ {Array.from({ length: Math.min(6, stats.servers) }).map((_, i) => (
+
+
+
+ {String(i + 1)}
+
+
+ ))}
+ {stats.servers > 6 ? (
+
+ +{stats.servers - 6}
+
+ ) : null}
+
+
+ {stats.users.toLocaleString()} members across your guilds
+
+
+
+
+
+
+
+
+
+
+
+
Keep SupportBot up to date
+
+ Check for releases and install updates from settings.
+
+
+
+
+
+ Open settings
+
+
+
+
+
+
+
+
+ System activity
+
+ ⋯
+
+
+
+ {activity.map((item) => (
+
+
+
+
+ SB
+
+
+
+
{item.label}
+
+ {item.detail}
+
+
+
+ {item.badge}
+
+
+ ))}
+
+
+ All system details
+
+
+
+
+
+
+
+
+
+
+
+
+
v{stats.bot.version}
+
CPU {stats.hosting.cpu_load}
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/plugin-card.tsx b/dashboard/src/components/plugin-card.tsx
new file mode 100644
index 0000000..8a06e0f
--- /dev/null
+++ b/dashboard/src/components/plugin-card.tsx
@@ -0,0 +1,36 @@
+import { Link } from "react-router-dom";
+import type { IconSvgElement } from "@hugeicons/react";
+import { Icon } from "@/components/icon";
+import { cn } from "@/lib/utils";
+
+interface PluginCardProps {
+ title: string;
+ description: string;
+ icon: IconSvgElement;
+ href: string;
+}
+
+export function PluginCard({
+ title,
+ description,
+ icon,
+ href,
+}: PluginCardProps) {
+ return (
+
+
+
+
+ {title}
+
+ {description}
+
+
+ );
+}
diff --git a/dashboard/src/components/promo-banner.tsx b/dashboard/src/components/promo-banner.tsx
new file mode 100644
index 0000000..46b01bf
--- /dev/null
+++ b/dashboard/src/components/promo-banner.tsx
@@ -0,0 +1,39 @@
+import { Link } from "react-router-dom";
+import { SparklesIcon } from "@hugeicons/core-free-icons";
+import { Icon } from "@/components/icon";
+import { Button } from "@/components/ui/button";
+import { useBotStats } from "@/context/BotStatsContext";
+
+export function PromoBanner() {
+ const { stats } = useBotStats();
+ const version = stats?.bot.version ?? "26.1.1";
+
+ return (
+
+
+
+
+
+ SupportBot {version}
+
+
+ Manage tickets, configs & transcripts
+
+
+ Configure your bot from one place. Edit settings visually, review
+ ticket transcripts, and monitor live stats.
+
+
+ Open settings
+
+
+
+
+
+
+ );
+}
diff --git a/dashboard/src/components/require-permission.tsx b/dashboard/src/components/require-permission.tsx
new file mode 100644
index 0000000..42c1dc1
--- /dev/null
+++ b/dashboard/src/components/require-permission.tsx
@@ -0,0 +1,28 @@
+import { Navigate } from "react-router-dom";
+import { useAuth } from "@/context/AuthContext";
+import { firstAllowedPath, hasPermission } from "@/lib/permissions";
+
+export function RequirePermission({
+ permission,
+ children,
+}: {
+ permission: string;
+ children: React.ReactNode;
+}) {
+ const { loading, isAuthenticated, permissions } = useAuth();
+
+ if (loading) {
+ return (
+
+ Loading…
+
+ );
+ }
+
+ if (!isAuthenticated) return ;
+ if (!hasPermission(permissions, permission)) {
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/dashboard/src/components/settings/dashboard-branding-settings.tsx b/dashboard/src/components/settings/dashboard-branding-settings.tsx
new file mode 100644
index 0000000..e42f872
--- /dev/null
+++ b/dashboard/src/components/settings/dashboard-branding-settings.tsx
@@ -0,0 +1,230 @@
+import { useEffect, useRef, useState } from "react";
+import { ImageUpload01Icon } from "@hugeicons/core-free-icons";
+import { api } from "@/api/client";
+import { Icon } from "@/components/icon";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { useBranding } from "@/context/BrandingContext";
+
+function readFileAsDataUrl(file: File): Promise {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(String(reader.result));
+ reader.onerror = () => reject(new Error("Could not read file"));
+ reader.readAsDataURL(file);
+ });
+}
+
+interface DashboardBrandingSettingsProps {
+ canEdit: boolean;
+}
+
+export function DashboardBrandingSettings({ canEdit }: DashboardBrandingSettingsProps) {
+ const { branding, setBrandingLocal } = useBranding();
+ const fileRef = useRef(null);
+ const [title, setTitle] = useState(branding.title);
+ const [pageTitle, setPageTitle] = useState(branding.pageTitle);
+ const [saving, setSaving] = useState(false);
+ const [uploading, setUploading] = useState(false);
+ const [message, setMessage] = useState("");
+
+ useEffect(() => {
+ setTitle(branding.title);
+ setPageTitle(branding.pageTitle);
+ }, [branding.title, branding.pageTitle]);
+
+ const faviconPreview =
+ branding.hasFavicon && branding.faviconUrl
+ ? `${branding.faviconUrl}${branding.updatedAt ? `?v=${branding.updatedAt}` : ""}`
+ : null;
+
+ async function saveText() {
+ if (!canEdit) return;
+ setSaving(true);
+ setMessage("");
+ try {
+ const res = await api.updateBranding({ title, pageTitle });
+ if (res.data) setBrandingLocal(res.data);
+ setMessage(res.message || "Branding saved.");
+ } catch (err) {
+ setMessage(err instanceof Error ? err.message : "Save failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function onFaviconSelected(file: File | null) {
+ if (!canEdit || !file) return;
+ setUploading(true);
+ setMessage("");
+ try {
+ const dataUrl = await readFileAsDataUrl(file);
+ const res = await api.uploadBrandingFavicon(dataUrl);
+ if (res.data) setBrandingLocal(res.data);
+ setMessage(res.message || "Favicon uploaded.");
+ } catch (err) {
+ setMessage(err instanceof Error ? err.message : "Upload failed");
+ } finally {
+ setUploading(false);
+ if (fileRef.current) fileRef.current.value = "";
+ }
+ }
+
+ async function removeFavicon() {
+ if (!canEdit) return;
+ setUploading(true);
+ setMessage("");
+ try {
+ const res = await api.removeBrandingFavicon();
+ if (res.data) setBrandingLocal(res.data);
+ setMessage(res.message || "Favicon removed.");
+ } catch (err) {
+ setMessage(err instanceof Error ? err.message : "Remove failed");
+ } finally {
+ setUploading(false);
+ }
+ }
+
+ async function resetAll() {
+ if (!canEdit) return;
+ setSaving(true);
+ setMessage("");
+ try {
+ const res = await api.resetBranding();
+ if (res.data) setBrandingLocal(res.data);
+ setMessage(res.message || "Reset to defaults.");
+ } catch (err) {
+ setMessage(err instanceof Error ? err.message : "Reset failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ const textDirty =
+ title.trim() !== branding.title || pageTitle.trim() !== branding.pageTitle;
+
+ return (
+
+
+
Branding
+
+ Sidebar title, browser tab title, and favicon — saved on the server for
+ everyone using this dashboard.
+
+
+
+
+
+
+
Favicon
+
+ PNG, ICO, SVG, JPEG, or WebP — max 512 KB. Shown in the browser tab and
+ beside the sidebar title.
+
+
+
+ {faviconPreview ? (
+
+ ) : (
+
+ )}
+
+
void onFaviconSelected(e.target.files?.[0] ?? null)}
+ />
+ {canEdit ? (
+ <>
+
fileRef.current?.click()}
+ >
+ {uploading ? "Uploading…" : "Upload favicon"}
+
+ {branding.hasFavicon ? (
+
void removeFavicon()}
+ >
+ Remove
+
+ ) : null}
+ >
+ ) : (
+
View only
+ )}
+
+
+
+ {canEdit ? (
+
+ void saveText()}
+ >
+ {saving ? "Saving…" : "Save titles"}
+
+ void resetAll()}
+ >
+ Reset branding
+
+
+ ) : null}
+
+ {message ? (
+
{message}
+ ) : null}
+
+ );
+}
diff --git a/dashboard/src/components/setup/setup-wizard.tsx b/dashboard/src/components/setup/setup-wizard.tsx
new file mode 100644
index 0000000..f636e39
--- /dev/null
+++ b/dashboard/src/components/setup/setup-wizard.tsx
@@ -0,0 +1,638 @@
+import type { IconSvgElement } from "@hugeicons/react";
+import {
+ ArrowLeft01Icon,
+ ArrowRight01Icon,
+ BotIcon,
+ CheckmarkCircle02Icon,
+ Copy01Icon,
+ Key01Icon,
+ Link01Icon,
+ SecurityLockIcon,
+ UserMultiple02Icon,
+} from "@hugeicons/core-free-icons";
+import type { ReactNode } from "react";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import type { FieldValidationResult, SetupFormValues } from "@/lib/setup-types";
+import { cn } from "@/lib/utils";
+
+export const SETUP_STEP_IDS = [
+ "botToken",
+ "secretKey",
+ "oauth",
+ "redirectUri",
+ "owners",
+] as const;
+
+export type SetupStepId = (typeof SETUP_STEP_IDS)[number];
+
+export const SETUP_STEPS: {
+ id: SetupStepId;
+ title: string;
+ subtitle: string;
+ icon: IconSvgElement;
+}[] = [
+ {
+ id: "botToken",
+ title: "Bot token",
+ subtitle: "Connect your Discord bot",
+ icon: BotIcon,
+ },
+ {
+ id: "secretKey",
+ title: "Session secret",
+ subtitle: "Secure dashboard logins",
+ icon: SecurityLockIcon,
+ },
+ {
+ id: "oauth",
+ title: "Discord OAuth",
+ subtitle: "Sign in with Discord",
+ icon: Key01Icon,
+ },
+ {
+ id: "redirectUri",
+ title: "Redirect URL",
+ subtitle: "OAuth callback address",
+ icon: Link01Icon,
+ },
+ {
+ id: "owners",
+ title: "Dashboard owners",
+ subtitle: "Who can access this panel",
+ icon: UserMultiple02Icon,
+ },
+];
+
+function validationKeyForStep(id: SetupStepId): string {
+ if (id === "oauth") return "oauth";
+ if (id === "owners") return "owners";
+ return id;
+}
+
+export function SetupWizardShell({
+ brandingTitle,
+ faviconUrl,
+ faviconVersion,
+ hasFavicon,
+ children,
+}: {
+ brandingTitle: string;
+ faviconUrl?: string | null;
+ faviconVersion?: number;
+ hasFavicon: boolean;
+ children: ReactNode;
+}) {
+ return (
+
+
+
+ {hasFavicon && faviconUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ First-time setup
+
+
+ {brandingTitle}
+
+
+
+ {children}
+
+
+ );
+}
+
+export function SetupProgress({
+ currentIndex,
+ validation,
+}: {
+ currentIndex: number;
+ validation: Record;
+}) {
+ const progress = ((currentIndex + 1) / SETUP_STEPS.length) * 100;
+
+ return (
+
+
+
+
+ Step {currentIndex + 1} of {SETUP_STEPS.length}
+
+ {Math.round(progress)}%
+
+
+
+
+
+ {SETUP_STEPS.map((step, index) => {
+ const key = validationKeyForStep(step.id);
+ const verified = validation[key]?.ok;
+ const isCurrent = index === currentIndex;
+ const isPast = index < currentIndex;
+
+ return (
+
+
+ {verified ? (
+
+ ) : (
+
+ )}
+
+
+
+ {step.title}
+
+
{step.subtitle}
+
+
+ );
+ })}
+
+
+
+ Each step can be tested with Discord before you continue. You can move back
+ anytime to change a value.
+
+
+ );
+}
+
+export function SetupStepCard({
+ step,
+ children,
+ footer,
+}: {
+ step: (typeof SETUP_STEPS)[number];
+ children: ReactNode;
+ footer?: ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+
+
{step.title}
+
{step.subtitle}
+
+
+
+
{step.title}
+
{step.subtitle}
+
+
+
{children}
+ {footer ? (
+
{footer}
+ ) : null}
+
+ );
+}
+
+export function ValidationBanner({
+ result,
+}: {
+ result?: FieldValidationResult;
+}) {
+ if (!result) return null;
+
+ if (result.ok) {
+ return (
+
+
+ {result.bot ? (
+
+ Connected as @{result.bot.username}
+ {result.applicationId ? (
+
+ {" "}
+ · App ID {result.applicationId}
+
+ ) : null}
+
+ ) : null}
+ {result.users?.length ? (
+
+ {result.users.map((u) => (
+
+
+ {u.globalName || u.username}
+ {u.id}
+
+ ))}
+
+ ) : null}
+ {!result.bot && !result.users?.length ? (
+ {result.warning || "Verified successfully."}
+ ) : null}
+
+
+ );
+ }
+
+ return (
+
+ {result.error}
+
+ );
+}
+
+export function BotTokenStep({
+ values,
+ setValues,
+ validation,
+ validating,
+ onValidate,
+}: {
+ values: SetupFormValues;
+ setValues: (v: SetupFormValues) => void;
+ validation: Record;
+ validating: boolean;
+ onValidate: () => void;
+}) {
+ return (
+
+
+ Open the{" "}
+
+ Discord Developer Portal
+
+ , select your application, go to Bot , and reset or copy
+ the token.
+
+
+ Bot token
+ setValues({ ...values, botToken: e.target.value })}
+ placeholder="MTI…"
+ className="h-11 font-mono text-sm"
+ />
+
+
+
+
+ {validating ? "Checking with Discord…" : "Verify bot token"}
+
+
+
+ );
+}
+
+export function SecretKeyStep({
+ values,
+ setValues,
+ validation,
+ validating,
+ onValidate,
+ onGenerate,
+}: {
+ values: SetupFormValues;
+ setValues: (v: SetupFormValues) => void;
+ validation: Record;
+ validating: boolean;
+ onValidate: () => void;
+ onGenerate: () => void;
+}) {
+ return (
+
+
+ This random string signs your login cookies. Store it like a password — do
+ not commit it to public repos.
+
+
+ Session secret
+ setValues({ ...values, secretKey: e.target.value })}
+ className="h-11 font-mono text-sm"
+ />
+
+
+
+
+ Generate secure secret
+
+
+ {validating ? "Checking…" : "Validate"}
+
+
+
+ );
+}
+
+export function OAuthStep({
+ values,
+ setValues,
+ validation,
+ validating,
+ onValidate,
+}: {
+ values: SetupFormValues;
+ setValues: (v: SetupFormValues) => void;
+ validation: Record;
+ validating: boolean;
+ onValidate: () => void;
+}) {
+ return (
+
+
+ Use the same application as your bot. Under OAuth2, copy
+ the Client ID and Client Secret.
+
+
+
+
+
+ {validating ? "Checking with Discord…" : "Verify OAuth credentials"}
+
+
+
+ );
+}
+
+export function RedirectStep({
+ values,
+ setValues,
+ validation,
+}: {
+ values: SetupFormValues;
+ setValues: (v: SetupFormValues) => void;
+ validation: Record;
+}) {
+ async function copyUri() {
+ if (!values.redirectUri) return;
+ try {
+ await navigator.clipboard.writeText(values.redirectUri);
+ } catch {
+ /* ignore */
+ }
+ }
+
+ return (
+
+
+ Add this exact URL under OAuth2 → Redirects in the
+ Developer Portal. It must match character-for-character.
+
+
+
Redirect URI
+
+
+ setValues({ ...values, redirectUri: e.target.value })
+ }
+ className="h-11 flex-1 font-mono text-xs"
+ />
+ void copyUri()}
+ title="Copy URL"
+ >
+
+
+
+
+ {validation.redirectUri && !validation.redirectUri.ok ? (
+
+ ) : (
+
+
+ We pre-filled this from your current dashboard URL. If you access the
+ panel from a different domain later, update this and add the new URL in
+ Discord too.
+
+
+ )}
+
+ );
+}
+
+export function OwnersStep({
+ values,
+ setValues,
+ validation,
+ validating,
+ onValidate,
+}: {
+ values: SetupFormValues;
+ setValues: (v: SetupFormValues) => void;
+ validation: Record;
+ validating: boolean;
+ onValidate: () => void;
+}) {
+ return (
+
+
+ Enable Developer Mode in Discord (Settings → Advanced),
+ then right-click your profile and Copy User ID . Add one ID
+ per line.
+
+
+ Owner user IDs
+
+
+
+
+ {validating ? "Checking with Discord…" : "Verify owner IDs"}
+
+
+
+ );
+}
+
+export function SetupCompleteScreen({ onContinue }: { onContinue: () => void }) {
+ return (
+
+
+
+
+
+
Setup complete
+
+ Your configuration has been saved. Restart the server for changes to take
+ effect — stop the running process, then start it again (for example{" "}
+
+ npm start
+
+ ).
+
+
+
+ After restarting, sign in with Discord to open the dashboard.
+
+
+
+ Continue to sign in
+
+
+
+ );
+}
+
+export function SetupNav({
+ currentIndex,
+ validating,
+ saving,
+ onBack,
+ onNext,
+ onFinish,
+}: {
+ currentIndex: number;
+ validating: boolean;
+ saving: boolean;
+ onBack: () => void;
+ onNext: () => void;
+ onFinish: () => void;
+}) {
+ const isLast = currentIndex === SETUP_STEPS.length - 1;
+
+ return (
+
+
+
+ Back
+
+
+ {isLast ? (
+
+ {saving ? "Saving…" : "Finish setup"}
+
+ ) : (
+
+ Continue
+
+
+ )}
+
+
+ );
+}
diff --git a/dashboard/src/components/transcripts/transcript-appearance-editor.tsx b/dashboard/src/components/transcripts/transcript-appearance-editor.tsx
new file mode 100644
index 0000000..2f3934d
--- /dev/null
+++ b/dashboard/src/components/transcripts/transcript-appearance-editor.tsx
@@ -0,0 +1,240 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { FloppyDiskIcon } from "@hugeicons/core-free-icons";
+import { api } from "@/api/client";
+import { ConfigFieldInput } from "@/components/config/config-field";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import { getByPath, setByPath } from "@/lib/config-utils";
+import { transcriptTemplateSchema } from "@/lib/transcript-template-schema";
+import { normalizeGoogleFontValue } from "@/lib/google-fonts";
+import type { TranscriptTemplate } from "@/lib/transcript-template-types";
+import { useAuth } from "@/context/AuthContext";
+import { cn } from "@/lib/utils";
+
+const PREVIEW_DEBOUNCE_MS = 450;
+
+function canEditTemplate(role: string | undefined, hasTranscripts: boolean) {
+ return hasTranscripts && role !== "viewer";
+}
+
+export function TranscriptAppearanceEditor() {
+ const { user, hasPermission } = useAuth();
+ const readOnly = !canEditTemplate(user?.role, hasPermission("transcripts"));
+
+ const [data, setData] = useState(null);
+ const [original, setOriginal] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState("");
+ const [message, setMessage] = useState("");
+ const [previewHtml, setPreviewHtml] = useState("");
+ const [previewLoading, setPreviewLoading] = useState(false);
+ const debounceRef = useRef | null>(null);
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.getTranscriptTemplate();
+ const tpl = (res.data ?? null) as TranscriptTemplate | null;
+ if (tpl) {
+ const normalized = {
+ ...tpl,
+ Typography: {
+ ...tpl.Typography,
+ FontFamily: normalizeGoogleFontValue(tpl.Typography?.FontFamily),
+ },
+ };
+ setData(normalized);
+ setOriginal(structuredClone(normalized));
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load template");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const refreshPreview = useCallback(async (tpl: TranscriptTemplate) => {
+ setPreviewLoading(true);
+ try {
+ const html = await api.previewTranscriptTemplate(tpl);
+ setPreviewHtml(html);
+ } catch {
+ setPreviewHtml(
+ "Preview failed to load.
",
+ );
+ } finally {
+ setPreviewLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!data) return;
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ void refreshPreview(data);
+ }, PREVIEW_DEBOUNCE_MS);
+ return () => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ };
+ }, [data, refreshPreview]);
+
+ function patch(path: string, value: unknown) {
+ setData((prev) =>
+ prev ? (setByPath(prev as Record, path, value) as TranscriptTemplate) : prev,
+ );
+ }
+
+ async function handleSave() {
+ if (readOnly || !data) return;
+ setSaving(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.saveTranscriptTemplate(data);
+ setMessage(res.message || "Saved.");
+ setOriginal(structuredClone(data));
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Save failed");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ const dirty =
+ data && original && JSON.stringify(data) !== JSON.stringify(original);
+
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!data) {
+ return (
+
+ {error || "Failed to load template"}
+
+ );
+ }
+
+ return (
+
+
+
+ Customize how new ticket transcripts look. Existing saved transcripts are not
+ changed.
+
+
+ {dirty ? (
+ Unsaved changes
+ ) : null}
+ {!readOnly ? (
+
+
+ {saving ? "Saving…" : "Save appearance"}
+
+ ) : (
+ View only
+ )}
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+
+
+
+
+
+ Live preview
+
+ Sample ticket with your current settings
+ {previewLoading ? " · updating…" : ""}
+
+
+
+
+
+
+
+
+ );
+}
+
+function ScrollableEditor({
+ data,
+ readOnly,
+ onPatch,
+}: {
+ data: TranscriptTemplate;
+ readOnly: boolean;
+ onPatch: (path: string, value: unknown) => void;
+}) {
+ return (
+
+ {transcriptTemplateSchema.map((section) => (
+
+
+ {section.title}
+ {section.description ? (
+ {section.description}
+ ) : null}
+
+
+ {section.fields.map((field) => (
+
+ , field.path)}
+ readOnly={readOnly}
+ onChange={(v) => onPatch(field.path, v)}
+ />
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/dashboard/src/components/transcripts/transcript-settings-panel.tsx b/dashboard/src/components/transcripts/transcript-settings-panel.tsx
new file mode 100644
index 0000000..d820ccd
--- /dev/null
+++ b/dashboard/src/components/transcripts/transcript-settings-panel.tsx
@@ -0,0 +1,278 @@
+import { useCallback, useEffect, useState } from "react";
+import { FloppyDiskIcon, RefreshIcon } from "@hugeicons/core-free-icons";
+import { api } from "@/api/client";
+import { Icon } from "@/components/icon";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Skeleton } from "@/components/ui/skeleton";
+import { useAuth } from "@/context/AuthContext";
+import { cn } from "@/lib/utils";
+
+interface TranscriptSettingsState {
+ autoDelete: { enabled: boolean; afterDays: number };
+ publicAccess: { enabled: boolean };
+}
+
+function canManageTranscripts(role: string | undefined, hasTranscripts: boolean) {
+ return hasTranscripts && role !== "viewer";
+}
+
+export function TranscriptSettingsPanel() {
+ const { user, hasPermission } = useAuth();
+ const readOnly = !canManageTranscripts(user?.role, hasPermission("transcripts"));
+
+ const [settings, setSettings] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [purging, setPurging] = useState(false);
+ const [error, setError] = useState("");
+ const [message, setMessage] = useState("");
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.getTranscriptSettings();
+ const data = res.data;
+ if (data) {
+ setSettings({
+ autoDelete: {
+ enabled: Boolean(data.autoDelete?.enabled),
+ afterDays: data.autoDelete?.afterDays ?? 30,
+ },
+ publicAccess: {
+ enabled: Boolean(data.publicAccess?.enabled),
+ },
+ });
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load settings");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ async function handleSave() {
+ if (!settings || readOnly) return;
+ setSaving(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.updateTranscriptSettings(settings);
+ if (res.data) {
+ setSettings({
+ autoDelete: {
+ enabled: res.data.autoDelete.enabled,
+ afterDays: res.data.autoDelete.afterDays,
+ },
+ publicAccess: {
+ enabled: res.data.publicAccess.enabled,
+ },
+ });
+ }
+ setMessage(res.message ?? "Settings saved.");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to save settings");
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ async function handlePurge() {
+ if (readOnly) return;
+ setPurging(true);
+ setError("");
+ setMessage("");
+ try {
+ const res = await api.purgeTranscripts();
+ setMessage(
+ res.message ??
+ (res.data?.skipped
+ ? "Auto-delete is disabled."
+ : `Removed ${res.data?.deleted ?? 0} transcript(s).`),
+ );
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Purge failed");
+ } finally {
+ setPurging(false);
+ }
+ }
+
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ if (!settings) {
+ return (
+
+ Could not load transcript settings.
+
+ );
+ }
+
+ return (
+
+ {readOnly ? (
+
+
+ You have view-only access. An admin or editor can change these settings.
+
+
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {message ? (
+
+ {message}
+
+ ) : null}
+
+
+
+ Auto-delete
+
+ Remove transcript files automatically after they reach a certain age. The
+ server checks hourly and whenever you open the library.
+
+
+
+
+
+ setSettings((s) =>
+ s
+ ? {
+ ...s,
+ autoDelete: { ...s.autoDelete, enabled: e.target.checked },
+ }
+ : s,
+ )
+ }
+ />
+
+ Enable auto-delete
+
+ Deletes HTML files in Data/Transcripts based on last modified time.
+
+
+
+
+
+ Delete after (days)
+
+
+ setSettings((s) =>
+ s
+ ? {
+ ...s,
+ autoDelete: {
+ ...s.autoDelete,
+ afterDays: Math.min(
+ 3650,
+ Math.max(1, parseInt(e.target.value, 10) || 1),
+ ),
+ },
+ }
+ : s,
+ )
+ }
+ className="max-w-[8rem]"
+ />
+
+ void handlePurge()}
+ >
+
+ Run cleanup now
+
+
+
+
+
+
+ Public transcripts
+
+ When enabled, anyone with the link can view a transcript at{" "}
+
+ {typeof window !== "undefined"
+ ? `${window.location.origin}/transcripts/:id`
+ : "/transcripts/:id"}
+ {" "}
+ without signing in. Channel IDs are numeric but not secret—only enable if
+ you are comfortable sharing links.
+
+
+
+
+
+ setSettings((s) =>
+ s
+ ? {
+ ...s,
+ publicAccess: { enabled: e.target.checked },
+ }
+ : s,
+ )
+ }
+ />
+
+ Allow public transcript URLs
+
+ Dashboard preview and download still require login.
+
+
+
+
+
+
+ {!readOnly ? (
+
void handleSave()}>
+
+ {saving ? "Saving…" : "Save settings"}
+
+ ) : null}
+
+ );
+}
diff --git a/dashboard/src/components/ui/alert.tsx b/dashboard/src/components/ui/alert.tsx
new file mode 100644
index 0000000..5afd41d
--- /dev/null
+++ b/dashboard/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/dashboard/src/components/ui/avatar.tsx b/dashboard/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..991f56e
--- /dev/null
+++ b/dashboard/src/components/ui/avatar.tsx
@@ -0,0 +1,48 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/dashboard/src/components/ui/badge.tsx b/dashboard/src/components/ui/badge.tsx
new file mode 100644
index 0000000..e87d62b
--- /dev/null
+++ b/dashboard/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/dashboard/src/components/ui/button.tsx b/dashboard/src/components/ui/button.tsx
new file mode 100644
index 0000000..65d4fcd
--- /dev/null
+++ b/dashboard/src/components/ui/button.tsx
@@ -0,0 +1,57 @@
+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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default:
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2",
+ sm: "h-8 rounded-md px-3 text-xs",
+ lg: "h-10 rounded-md px-8",
+ icon: "h-9 w-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/dashboard/src/components/ui/card.tsx b/dashboard/src/components/ui/card.tsx
new file mode 100644
index 0000000..284f44a
--- /dev/null
+++ b/dashboard/src/components/ui/card.tsx
@@ -0,0 +1,76 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/dashboard/src/components/ui/checkbox.tsx b/dashboard/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..90b71e4
--- /dev/null
+++ b/dashboard/src/components/ui/checkbox.tsx
@@ -0,0 +1,28 @@
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { Check } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+))
+Checkbox.displayName = CheckboxPrimitive.Root.displayName
+
+export { Checkbox }
diff --git a/dashboard/src/components/ui/color-picker.tsx b/dashboard/src/components/ui/color-picker.tsx
new file mode 100644
index 0000000..7615e61
--- /dev/null
+++ b/dashboard/src/components/ui/color-picker.tsx
@@ -0,0 +1,113 @@
+import { forwardRef, useMemo, useState } from "react";
+import { HexColorPicker } from "react-colorful";
+import { cn } from "@/lib/utils";
+import { isValidHex, normalizeHex } from "@/lib/normalize-hex";
+import { useForwardedRef } from "@/hooks/use-forwarded-ref";
+import type { ButtonProps } from "@/components/ui/button";
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Input } from "@/components/ui/input";
+
+interface ColorPickerProps {
+ value: string;
+ onChange: (value: string) => void;
+ onBlur?: () => void;
+}
+
+const ColorPicker = forwardRef<
+ HTMLInputElement,
+ Omit & ColorPickerProps
+>(
+ (
+ { disabled, value, onChange, onBlur, name, className, size, ...props },
+ forwardedRef,
+ ) => {
+ const ref = useForwardedRef(forwardedRef);
+ const [open, setOpen] = useState(false);
+ const [draft, setDraft] = useState("");
+
+ const parsedValue = useMemo(
+ () => normalizeHex(value, "#ffffff"),
+ [value],
+ );
+
+ const displayHex = open && draft ? draft : parsedValue;
+
+ function commitHex(raw: string) {
+ if (isValidHex(raw)) {
+ onChange(normalizeHex(raw));
+ }
+ setDraft("");
+ }
+
+ return (
+ {
+ if (!next) commitHex(draft || parsedValue);
+ setOpen(next);
+ }}
+ >
+
+ setOpen(true)}
+ size={size ?? "icon"}
+ style={{ backgroundColor: parsedValue }}
+ variant="outline"
+ aria-label="Pick colour"
+ >
+ {parsedValue}
+
+
+
+ {
+ setDraft("");
+ onChange(normalizeHex(hex));
+ }}
+ />
+ {
+ const v = e.currentTarget.value;
+ setDraft(v);
+ if (isValidHex(v)) {
+ onChange(normalizeHex(v));
+ }
+ }}
+ onBlur={() => commitHex(draft || parsedValue)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ commitHex(draft || parsedValue);
+ setOpen(false);
+ }
+ }}
+ className="mt-3 border-border bg-secondary/30 font-mono text-sm uppercase"
+ placeholder="#000000"
+ />
+
+
+ );
+ },
+);
+ColorPicker.displayName = "ColorPicker";
+
+export { ColorPicker };
diff --git a/dashboard/src/components/ui/command.tsx b/dashboard/src/components/ui/command.tsx
new file mode 100644
index 0000000..2cecd91
--- /dev/null
+++ b/dashboard/src/components/ui/command.tsx
@@ -0,0 +1,153 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+const CommandDialog = ({ children, ...props }: DialogProps) => {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/dashboard/src/components/ui/dialog.tsx b/dashboard/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..9dbeaa0
--- /dev/null
+++ b/dashboard/src/components/ui/dialog.tsx
@@ -0,0 +1,120 @@
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Dialog = DialogPrimitive.Root
+
+const DialogTrigger = DialogPrimitive.Trigger
+
+const DialogPortal = DialogPrimitive.Portal
+
+const DialogClose = DialogPrimitive.Close
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+ {children}
+
+
+ Close
+
+
+
+))
+DialogContent.displayName = DialogPrimitive.Content.displayName
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogHeader.displayName = "DialogHeader"
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+DialogFooter.displayName = "DialogFooter"
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogTitle.displayName = DialogPrimitive.Title.displayName
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DialogDescription.displayName = DialogPrimitive.Description.displayName
+
+export {
+ Dialog,
+ DialogPortal,
+ DialogOverlay,
+ DialogTrigger,
+ DialogClose,
+ DialogContent,
+ DialogHeader,
+ DialogFooter,
+ DialogTitle,
+ DialogDescription,
+}
diff --git a/dashboard/src/components/ui/dropdown-menu.tsx b/dashboard/src/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..5a20503
--- /dev/null
+++ b/dashboard/src/components/ui/dropdown-menu.tsx
@@ -0,0 +1,201 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { Check, ChevronRight, Circle } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+ svg]:size-4 [&>svg]:shrink-0",
+ inset && "pl-8",
+ className
+ )}
+ {...props}
+ />
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/dashboard/src/components/ui/input.tsx b/dashboard/src/components/ui/input.tsx
new file mode 100644
index 0000000..69b64fb
--- /dev/null
+++ b/dashboard/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Input.displayName = "Input"
+
+export { Input }
diff --git a/dashboard/src/components/ui/label.tsx b/dashboard/src/components/ui/label.tsx
new file mode 100644
index 0000000..683faa7
--- /dev/null
+++ b/dashboard/src/components/ui/label.tsx
@@ -0,0 +1,24 @@
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+)
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/dashboard/src/components/ui/popover.tsx b/dashboard/src/components/ui/popover.tsx
new file mode 100644
index 0000000..70a28f6
--- /dev/null
+++ b/dashboard/src/components/ui/popover.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverAnchor = PopoverPrimitive.Anchor
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
diff --git a/dashboard/src/components/ui/scroll-area.tsx b/dashboard/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..393be03
--- /dev/null
+++ b/dashboard/src/components/ui/scroll-area.tsx
@@ -0,0 +1,48 @@
+"use client"
+
+import * as React from "react"
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
+
+import { cn } from "@/lib/utils"
+
+const ScrollArea = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+ {children}
+
+
+
+
+))
+ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
+
+const ScrollBar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = "vertical", ...props }, ref) => (
+
+
+
+))
+ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
+
+export { ScrollArea, ScrollBar }
diff --git a/dashboard/src/components/ui/select.tsx b/dashboard/src/components/ui/select.tsx
new file mode 100644
index 0000000..6e637f7
--- /dev/null
+++ b/dashboard/src/components/ui/select.tsx
@@ -0,0 +1,159 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/dashboard/src/components/ui/separator.tsx b/dashboard/src/components/ui/separator.tsx
new file mode 100644
index 0000000..6d7f122
--- /dev/null
+++ b/dashboard/src/components/ui/separator.tsx
@@ -0,0 +1,29 @@
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
+
+import { cn } from "@/lib/utils"
+
+const Separator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = "horizontal", decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/dashboard/src/components/ui/sheet.tsx b/dashboard/src/components/ui/sheet.tsx
new file mode 100644
index 0000000..02942db
--- /dev/null
+++ b/dashboard/src/components/ui/sheet.tsx
@@ -0,0 +1,138 @@
+import * as React from "react"
+import * as SheetPrimitive from "@radix-ui/react-dialog"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Sheet = SheetPrimitive.Root
+
+const SheetTrigger = SheetPrimitive.Trigger
+
+const SheetClose = SheetPrimitive.Close
+
+const SheetPortal = SheetPrimitive.Portal
+
+const SheetOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
+
+const sheetVariants = cva(
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
+ {
+ variants: {
+ side: {
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
+ bottom:
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
+ right:
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
+ },
+ },
+ defaultVariants: {
+ side: "right",
+ },
+ }
+)
+
+interface SheetContentProps
+ extends React.ComponentPropsWithoutRef,
+ VariantProps {}
+
+const SheetContent = React.forwardRef<
+ React.ElementRef,
+ SheetContentProps
+>(({ side = "right", className, children, ...props }, ref) => (
+
+
+
+
+
+ Close
+
+ {children}
+
+
+))
+SheetContent.displayName = SheetPrimitive.Content.displayName
+
+const SheetHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetHeader.displayName = "SheetHeader"
+
+const SheetFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+SheetFooter.displayName = "SheetFooter"
+
+const SheetTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetTitle.displayName = SheetPrimitive.Title.displayName
+
+const SheetDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+SheetDescription.displayName = SheetPrimitive.Description.displayName
+
+export {
+ Sheet,
+ SheetPortal,
+ SheetOverlay,
+ SheetTrigger,
+ SheetClose,
+ SheetContent,
+ SheetHeader,
+ SheetFooter,
+ SheetTitle,
+ SheetDescription,
+}
diff --git a/dashboard/src/components/ui/sidebar.tsx b/dashboard/src/components/ui/sidebar.tsx
new file mode 100644
index 0000000..ed6f66d
--- /dev/null
+++ b/dashboard/src/components/ui/sidebar.tsx
@@ -0,0 +1,771 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+import { PanelLeft } 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
+}
+
+const SidebarProvider = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ defaultOpen?: boolean
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ }
+>(
+ (
+ {
+ defaultOpen = true,
+ open: openProp,
+ onOpenChange: setOpenProp,
+ className,
+ style,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ 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}
+
+
+
+ )
+ }
+)
+SidebarProvider.displayName = "SidebarProvider"
+
+const Sidebar = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ side?: "left" | "right"
+ variant?: "sidebar" | "floating" | "inset"
+ collapsible?: "offcanvas" | "icon" | "none"
+ }
+>(
+ (
+ {
+ side = "left",
+ variant = "sidebar",
+ collapsible = "offcanvas",
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ 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 */}
+
+
+
+ )
+ }
+)
+Sidebar.displayName = "Sidebar"
+
+const SidebarTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, onClick, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+ {
+ onClick?.(event)
+ toggleSidebar()
+ }}
+ {...props}
+ >
+
+ Toggle Sidebar
+
+ )
+})
+SidebarTrigger.displayName = "SidebarTrigger"
+
+const SidebarRail = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button">
+>(({ className, ...props }, ref) => {
+ const { toggleSidebar } = useSidebar()
+
+ return (
+
+ )
+})
+SidebarRail.displayName = "SidebarRail"
+
+const SidebarInset = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"main">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInset.displayName = "SidebarInset"
+
+const SidebarInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarInput.displayName = "SidebarInput"
+
+const SidebarHeader = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarHeader.displayName = "SidebarHeader"
+
+const SidebarFooter = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarFooter.displayName = "SidebarFooter"
+
+const SidebarSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentProps
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarSeparator.displayName = "SidebarSeparator"
+
+const SidebarContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarContent.displayName = "SidebarContent"
+
+const SidebarGroup = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+SidebarGroup.displayName = "SidebarGroup"
+
+const SidebarGroupLabel = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ 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}
+ />
+ )
+})
+SidebarGroupLabel.displayName = "SidebarGroupLabel"
+
+const SidebarGroupAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & { asChild?: boolean }
+>(({ className, asChild = false, ...props }, ref) => {
+ 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 after:md:hidden",
+ "group-data-[collapsible=icon]:hidden",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarGroupAction.displayName = "SidebarGroupAction"
+
+const SidebarGroupContent = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarGroupContent.displayName = "SidebarGroupContent"
+
+const SidebarMenu = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenu.displayName = "SidebarMenu"
+
+const SidebarMenuItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuItem.displayName = "SidebarMenuItem"
+
+const sidebarMenuButtonVariants = cva(
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none 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",
+ },
+ }
+)
+
+const SidebarMenuButton = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ isActive?: boolean
+ tooltip?: string | React.ComponentProps
+ } & VariantProps
+>(
+ (
+ {
+ asChild = false,
+ isActive = false,
+ variant = "default",
+ size = "default",
+ tooltip,
+ className,
+ ...props
+ },
+ ref
+ ) => {
+ const Comp = asChild ? Slot : "button"
+ const { isMobile, state } = useSidebar()
+
+ const button = (
+
+ )
+
+ if (!tooltip) {
+ return button
+ }
+
+ if (typeof tooltip === "string") {
+ tooltip = {
+ children: tooltip,
+ }
+ }
+
+ return (
+
+ {button}
+
+
+ )
+ }
+)
+SidebarMenuButton.displayName = "SidebarMenuButton"
+
+const SidebarMenuAction = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps<"button"> & {
+ asChild?: boolean
+ showOnHover?: boolean
+ }
+>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
+ 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 after:md: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 &&
+ "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
+ className
+ )}
+ {...props}
+ />
+ )
+})
+SidebarMenuAction.displayName = "SidebarMenuAction"
+
+const SidebarMenuBadge = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuBadge.displayName = "SidebarMenuBadge"
+
+const SidebarMenuSkeleton = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ showIcon?: boolean
+ }
+>(({ className, showIcon = false, ...props }, ref) => {
+ // Random width between 50 to 90%.
+ const width = React.useMemo(() => {
+ return `${Math.floor(Math.random() * 40) + 50}%`
+ }, [])
+
+ return (
+
+ {showIcon && (
+
+ )}
+
+
+ )
+})
+SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
+
+const SidebarMenuSub = React.forwardRef<
+ HTMLUListElement,
+ React.ComponentProps<"ul">
+>(({ className, ...props }, ref) => (
+
+))
+SidebarMenuSub.displayName = "SidebarMenuSub"
+
+const SidebarMenuSubItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentProps<"li">
+>(({ ...props }, ref) => )
+SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
+
+const SidebarMenuSubButton = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentProps<"a"> & {
+ asChild?: boolean
+ size?: "sm" | "md"
+ isActive?: boolean
+ }
+>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+ span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "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}
+ />
+ )
+})
+SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
+
+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/dashboard/src/components/ui/skeleton.tsx b/dashboard/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..d7e45f7
--- /dev/null
+++ b/dashboard/src/components/ui/skeleton.tsx
@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+
+function Skeleton({
+ className,
+ ...props
+}: React.HTMLAttributes) {
+ return (
+
+ )
+}
+
+export { Skeleton }
diff --git a/dashboard/src/components/ui/sonner.tsx b/dashboard/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..2469cc8
--- /dev/null
+++ b/dashboard/src/components/ui/sonner.tsx
@@ -0,0 +1,25 @@
+import { Toaster as Sonner } from "sonner";
+
+type ToasterProps = React.ComponentProps;
+
+function Toaster({ ...props }: ToasterProps) {
+ return (
+
+ );
+}
+
+export { Toaster };
diff --git a/dashboard/src/components/ui/switch.tsx b/dashboard/src/components/ui/switch.tsx
new file mode 100644
index 0000000..455c23b
--- /dev/null
+++ b/dashboard/src/components/ui/switch.tsx
@@ -0,0 +1,27 @@
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/dashboard/src/components/ui/tabs.tsx b/dashboard/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..85d83be
--- /dev/null
+++ b/dashboard/src/components/ui/tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/dashboard/src/components/ui/textarea.tsx b/dashboard/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..e56b0af
--- /dev/null
+++ b/dashboard/src/components/ui/textarea.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef<
+ HTMLTextAreaElement,
+ React.ComponentProps<"textarea">
+>(({ className, ...props }, ref) => {
+ return (
+
+ )
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/dashboard/src/components/ui/tooltip.tsx b/dashboard/src/components/ui/tooltip.tsx
new file mode 100644
index 0000000..28e1918
--- /dev/null
+++ b/dashboard/src/components/ui/tooltip.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import * as React from "react"
+import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+
+import { cn } from "@/lib/utils"
+
+const TooltipProvider = TooltipPrimitive.Provider
+
+const Tooltip = TooltipPrimitive.Root
+
+const TooltipTrigger = TooltipPrimitive.Trigger
+
+const TooltipContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+TooltipContent.displayName = TooltipPrimitive.Content.displayName
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
diff --git a/dashboard/src/context/AuthContext.tsx b/dashboard/src/context/AuthContext.tsx
new file mode 100644
index 0000000..764f866
--- /dev/null
+++ b/dashboard/src/context/AuthContext.tsx
@@ -0,0 +1,124 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { api } from "../api/client";
+import {
+ canEditConfig,
+ canViewConfig,
+ displayDiscordName,
+ hasPermission,
+ type DashboardPermissions,
+ type DashboardUser,
+} from "../lib/permissions";
+
+interface AuthContextValue {
+ user: DashboardUser | null;
+ permissions: DashboardPermissions | null;
+ isAuthenticated: boolean;
+ loading: boolean;
+ loginWithDiscord: () => void;
+ logout: () => Promise;
+ refreshSession: () => Promise;
+ hasPermission: (key: string) => boolean;
+ canViewConfig: (file: string) => boolean;
+ canEditConfig: (file: string) => boolean;
+}
+
+const AuthContext = createContext(null);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ const refreshSession = useCallback(async () => {
+ try {
+ const res = await api.me();
+ if (res.data) {
+ setUser(res.data);
+ return true;
+ }
+ setUser(null);
+ return false;
+ } catch {
+ setUser(null);
+ return false;
+ }
+ }, []);
+
+ useEffect(() => {
+ refreshSession().finally(() => setLoading(false));
+ }, [refreshSession]);
+
+ const loginWithDiscord = useCallback(() => {
+ window.location.href = "/api/auth/discord";
+ }, []);
+
+ const logout = useCallback(async () => {
+ try {
+ await api.logout();
+ } finally {
+ setUser(null);
+ }
+ }, []);
+
+ const permissions = user?.permissions ?? null;
+
+ const checkPermission = useCallback(
+ (key: string) => hasPermission(permissions, key),
+ [permissions],
+ );
+
+ const checkViewConfig = useCallback(
+ (file: string) => canViewConfig(permissions, file),
+ [permissions],
+ );
+
+ const checkEditConfig = useCallback(
+ (file: string) => canEditConfig(permissions, file),
+ [permissions],
+ );
+
+ const value = useMemo(
+ () => ({
+ user,
+ permissions,
+ isAuthenticated: !!user,
+ loading,
+ loginWithDiscord,
+ logout,
+ refreshSession,
+ hasPermission: checkPermission,
+ canViewConfig: checkViewConfig,
+ canEditConfig: checkEditConfig,
+ }),
+ [
+ user,
+ permissions,
+ loading,
+ loginWithDiscord,
+ logout,
+ refreshSession,
+ checkPermission,
+ checkViewConfig,
+ checkEditConfig,
+ ],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useAuth() {
+ const ctx = useContext(AuthContext);
+ if (!ctx) throw new Error("useAuth must be used within AuthProvider");
+ return ctx;
+}
+
+export { displayDiscordName };
diff --git a/dashboard/src/context/BotStatsContext.tsx b/dashboard/src/context/BotStatsContext.tsx
new file mode 100644
index 0000000..3c903a5
--- /dev/null
+++ b/dashboard/src/context/BotStatsContext.tsx
@@ -0,0 +1,64 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { api, type BotStats } from "@/api/client";
+
+interface BotStatsContextValue {
+ stats: BotStats | null;
+ loading: boolean;
+ error: string;
+ refresh: () => Promise;
+}
+
+const BotStatsContext = createContext(null);
+
+export function BotStatsProvider({ children }: { children: ReactNode }) {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const refresh = useCallback(async () => {
+ try {
+ const res = await api.stats();
+ if (res.data) {
+ setStats(res.data);
+ setError("");
+ } else if (res.error) {
+ setError(res.error);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to load stats");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refresh();
+ const id = setInterval(refresh, 30_000);
+ return () => clearInterval(id);
+ }, [refresh]);
+
+ const value = useMemo(
+ () => ({ stats, loading, error, refresh }),
+ [stats, loading, error, refresh],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useBotStats() {
+ const ctx = useContext(BotStatsContext);
+ if (!ctx) {
+ throw new Error("useBotStats must be used within BotStatsProvider");
+ }
+ return ctx;
+}
diff --git a/dashboard/src/context/BrandingContext.tsx b/dashboard/src/context/BrandingContext.tsx
new file mode 100644
index 0000000..340443f
--- /dev/null
+++ b/dashboard/src/context/BrandingContext.tsx
@@ -0,0 +1,72 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { api } from "@/api/client";
+import { applyDashboardBranding } from "@/lib/apply-dashboard-branding";
+import { DEFAULT_BRANDING, type DashboardBranding } from "@/lib/branding-types";
+
+interface BrandingContextValue {
+ branding: DashboardBranding;
+ loading: boolean;
+ refreshBranding: () => Promise;
+ setBrandingLocal: (next: DashboardBranding) => void;
+}
+
+const BrandingContext = createContext(null);
+
+export function BrandingProvider({ children }: { children: ReactNode }) {
+ const [branding, setBranding] = useState(DEFAULT_BRANDING);
+ const [loading, setLoading] = useState(true);
+
+ const refreshBranding = useCallback(async () => {
+ try {
+ const res = await api.getBranding();
+ if (res.data) {
+ setBranding(res.data);
+ applyDashboardBranding(res.data);
+ }
+ } catch {
+ setBranding(DEFAULT_BRANDING);
+ applyDashboardBranding(DEFAULT_BRANDING);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void refreshBranding();
+ }, [refreshBranding]);
+
+ const setBrandingLocal = useCallback((next: DashboardBranding) => {
+ setBranding(next);
+ applyDashboardBranding(next);
+ }, []);
+
+ const value = useMemo(
+ () => ({
+ branding,
+ loading,
+ refreshBranding,
+ setBrandingLocal,
+ }),
+ [branding, loading, refreshBranding, setBrandingLocal],
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useBranding() {
+ const ctx = useContext(BrandingContext);
+ if (!ctx) {
+ throw new Error("useBranding must be used within BrandingProvider");
+ }
+ return ctx;
+}
diff --git a/dashboard/src/context/NotificationsContext.tsx b/dashboard/src/context/NotificationsContext.tsx
new file mode 100644
index 0000000..df3770b
--- /dev/null
+++ b/dashboard/src/context/NotificationsContext.tsx
@@ -0,0 +1,101 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import {
+ api,
+ type DashboardNotification,
+} from "@/api/client";
+
+interface NotificationsContextValue {
+ items: DashboardNotification[];
+ unreadCount: number;
+ loading: boolean;
+ refresh: () => Promise;
+ markAllRead: () => Promise;
+ dismiss: (id: string) => Promise;
+}
+
+const NotificationsContext = createContext(
+ null,
+);
+
+export function NotificationsProvider({ children }: { children: ReactNode }) {
+ const [items, setItems] = useState([]);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const [loading, setLoading] = useState(true);
+
+ const refresh = useCallback(async () => {
+ try {
+ const res = await api.getNotifications();
+ if (res.data) {
+ setItems(res.data.items);
+ setUnreadCount(res.data.unreadCount);
+ }
+ } catch {
+ /* ignore when logged out or API down */
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const markAllRead = useCallback(async () => {
+ try {
+ const res = await api.markNotificationsRead("all");
+ setItems((prev) => prev.map((n) => ({ ...n, read: true })));
+ setUnreadCount(res.data?.unreadCount ?? 0);
+ } catch {
+ /* ignore */
+ }
+ }, []);
+
+ const dismiss = useCallback(
+ async (id: string) => {
+ try {
+ const res = await api.dismissNotification(id);
+ setItems((prev) => prev.filter((n) => n.id !== id));
+ setUnreadCount(res.data?.unreadCount ?? 0);
+ } catch {
+ /* ignore */
+ }
+ },
+ [],
+ );
+
+ useEffect(() => {
+ refresh();
+ const id = setInterval(refresh, 30_000);
+ return () => clearInterval(id);
+ }, [refresh]);
+
+ const value = useMemo(
+ () => ({
+ items,
+ unreadCount,
+ loading,
+ refresh,
+ markAllRead,
+ dismiss,
+ }),
+ [items, unreadCount, loading, refresh, markAllRead, dismiss],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useNotifications() {
+ const ctx = useContext(NotificationsContext);
+ if (!ctx) {
+ throw new Error("useNotifications must be used within NotificationsProvider");
+ }
+ return ctx;
+}
diff --git a/dashboard/src/context/SetupContext.tsx b/dashboard/src/context/SetupContext.tsx
new file mode 100644
index 0000000..edd49cb
--- /dev/null
+++ b/dashboard/src/context/SetupContext.tsx
@@ -0,0 +1,61 @@
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type ReactNode,
+} from "react";
+import { api } from "@/api/client";
+import type { SetupStatus } from "@/lib/setup-types";
+
+interface SetupContextValue {
+ status: SetupStatus | null;
+ loading: boolean;
+ error: string;
+ complete: boolean;
+ refresh: () => Promise;
+}
+
+const SetupContext = createContext(null);
+
+export function SetupProvider({ children }: { children: ReactNode }) {
+ const [status, setStatus] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState("");
+
+ const refresh = useCallback(async () => {
+ setError("");
+ try {
+ const res = await api.getSetupStatus();
+ setStatus(res.data ?? null);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Could not load setup status");
+ setStatus(null);
+ }
+ }, []);
+
+ useEffect(() => {
+ refresh().finally(() => setLoading(false));
+ }, [refresh]);
+
+ const value = useMemo(
+ () => ({
+ status,
+ loading,
+ error,
+ complete: Boolean(status?.complete),
+ refresh,
+ }),
+ [status, loading, error, refresh],
+ );
+
+ return {children} ;
+}
+
+export function useSetup() {
+ const ctx = useContext(SetupContext);
+ if (!ctx) throw new Error("useSetup must be used within SetupProvider");
+ return ctx;
+}
diff --git a/dashboard/src/hooks/use-addon-configs.ts b/dashboard/src/hooks/use-addon-configs.ts
new file mode 100644
index 0000000..a7cab45
--- /dev/null
+++ b/dashboard/src/hooks/use-addon-configs.ts
@@ -0,0 +1,26 @@
+import { useCallback, useEffect, useState } from "react";
+import { useLocation } from "react-router-dom";
+import { api } from "@/api/client";
+
+export function useAddonConfigs() {
+ const location = useLocation();
+ const [files, setFiles] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const refresh = useCallback(async () => {
+ try {
+ const res = await api.listAddonConfigs();
+ setFiles(res.data ?? []);
+ } catch {
+ setFiles([]);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ refresh();
+ }, [refresh, location.pathname]);
+
+ return { files, loading, refresh };
+}
diff --git a/dashboard/src/hooks/use-forwarded-ref.ts b/dashboard/src/hooks/use-forwarded-ref.ts
new file mode 100644
index 0000000..6b7501e
--- /dev/null
+++ b/dashboard/src/hooks/use-forwarded-ref.ts
@@ -0,0 +1,16 @@
+import { useEffect, useRef, type ForwardedRef } from "react";
+
+export function useForwardedRef(forwardedRef: ForwardedRef) {
+ const innerRef = useRef(null);
+
+ useEffect(() => {
+ if (!forwardedRef) return;
+ if (typeof forwardedRef === "function") {
+ forwardedRef(innerRef.current);
+ } else {
+ forwardedRef.current = innerRef.current;
+ }
+ });
+
+ return innerRef;
+}
diff --git a/dashboard/src/hooks/use-mobile.tsx b/dashboard/src/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/dashboard/src/hooks/use-mobile.tsx
@@ -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/dashboard/src/index.css b/dashboard/src/index.css
new file mode 100644
index 0000000..3457769
--- /dev/null
+++ b/dashboard/src/index.css
@@ -0,0 +1,149 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 240 10% 4%;
+ --foreground: 0 0% 98%;
+ --card: 240 7% 11%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 8% 10%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 141 45% 49%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 240 6% 14%;
+ --secondary-foreground: 0 0% 90%;
+ --muted: 240 6% 14%;
+ --muted-foreground: 240 5% 55%;
+ --accent: 141 29% 18%;
+ --accent-foreground: 141 52% 78%;
+ --destructive: 0 72% 55%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 240 6% 18%;
+ --input: 240 6% 14%;
+ --ring: 141 45% 49%;
+ --radius: 0.75rem;
+ --chart-1: 141 45% 49%;
+ --chart-2: 199 89% 48%;
+ --chart-3: 38 92% 50%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 0 72% 55%;
+ --sidebar-background: 240 10% 5%;
+ --sidebar-foreground: 240 5% 90%;
+ --sidebar-primary: 141 45% 49%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 141 26% 14%;
+ --sidebar-accent-foreground: 141 48% 82%;
+ --sidebar-border: 240 6% 14%;
+ --sidebar-ring: 141 45% 49%;
+ --scrollbar-size: 6px;
+ --scrollbar-thumb: 240 4% 32%;
+ --scrollbar-thumb-hover: 240 4% 44%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background font-sans text-foreground antialiased;
+ }
+
+ html {
+ color-scheme: dark;
+ }
+
+ html,
+ body,
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: hsl(var(--scrollbar-thumb)) transparent;
+ }
+
+ *::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+ }
+
+ *::-webkit-scrollbar-button {
+ display: none;
+ width: 0;
+ height: 0;
+ }
+
+ *::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ *::-webkit-scrollbar-thumb {
+ background-color: hsl(var(--scrollbar-thumb));
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ background-clip: content-box;
+ }
+
+ *::-webkit-scrollbar-thumb:hover {
+ background-color: hsl(var(--scrollbar-thumb-hover));
+ }
+
+ *::-webkit-scrollbar-thumb:active {
+ background-color: hsl(var(--scrollbar-thumb-hover));
+ }
+
+ *::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+}
+
+@layer utilities {
+ .dashboard-glow {
+ background: hsl(var(--background));
+ }
+
+ .promo-gradient {
+ background: hsl(var(--card));
+ border: 1px solid hsl(var(--border));
+ }
+
+ .overview-card-glow {
+ box-shadow: none;
+ }
+
+ .color-picker-swatch.react-colorful {
+ width: 220px;
+ height: 140px;
+ }
+
+ .color-picker-swatch .react-colorful__saturation {
+ border-radius: calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0 0;
+ }
+
+ .color-picker-swatch .react-colorful__hue {
+ height: 12px;
+ border-radius: 0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);
+ margin-top: 10px;
+ }
+
+ .color-picker-swatch .react-colorful__pointer {
+ width: 16px;
+ height: 16px;
+ }
+
+ .unicode-emoji-panel-host emoji-picker.unicode-emoji-picker-el {
+ width: 100%;
+ height: min(50vh, 380px);
+ --background: hsl(var(--card));
+ --border-color: hsl(var(--border));
+ --button-active-background: hsl(var(--primary) / 0.25);
+ --button-hover-background: hsl(var(--accent));
+ --input-border-color: hsl(var(--border));
+ --input-font-color: hsl(var(--foreground));
+ --input-placeholder-color: hsl(var(--muted-foreground));
+ --outline-color: hsl(var(--ring));
+ --category-font-color: hsl(var(--muted-foreground));
+ --indicator-color: hsl(var(--primary));
+ }
+
+}
diff --git a/dashboard/src/lib/accent-theme.ts b/dashboard/src/lib/accent-theme.ts
new file mode 100644
index 0000000..2413307
--- /dev/null
+++ b/dashboard/src/lib/accent-theme.ts
@@ -0,0 +1,128 @@
+import { isValidHex, normalizeHex } from "@/lib/normalize-hex";
+
+export const ACCENT_STORAGE_KEY = "supportbot-dashboard-accent";
+export const DEFAULT_ACCENT_HEX = "#45b46c";
+
+const CSS_VARS = [
+ "--primary",
+ "--ring",
+ "--chart-1",
+ "--sidebar-primary",
+ "--sidebar-ring",
+ "--accent",
+ "--accent-foreground",
+ "--sidebar-accent",
+ "--sidebar-accent-foreground",
+] as const;
+
+export function hexToHsl(hex: string): { h: number; s: number; l: number } {
+ const normalized = normalizeHex(hex, DEFAULT_ACCENT_HEX);
+ const r = parseInt(normalized.slice(1, 3), 16) / 255;
+ const g = parseInt(normalized.slice(3, 5), 16) / 255;
+ const b = parseInt(normalized.slice(5, 7), 16) / 255;
+
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const l = (max + min) / 2;
+ let h = 0;
+ let s = 0;
+
+ if (max !== min) {
+ const d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch (max) {
+ case r:
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
+ break;
+ case g:
+ h = ((b - r) / d + 2) / 6;
+ break;
+ default:
+ h = ((r - g) / d + 4) / 6;
+ break;
+ }
+ }
+
+ return {
+ h: Math.round(h * 360),
+ s: Math.round(s * 100),
+ l: Math.round(l * 100),
+ };
+}
+
+function hsl(h: number, s: number, l: number): string {
+ return `${h} ${s}% ${l}%`;
+}
+
+/** Apply accent colour to dashboard CSS variables (replaces default purple). */
+export function applyAccentTheme(hex: string): string {
+ const normalized = normalizeHex(hex, DEFAULT_ACCENT_HEX);
+ const { h, s, l } = hexToHsl(normalized);
+ const root = document.documentElement;
+
+ const primaryS = Math.min(Math.max(s, 45), 85);
+ const primaryL = Math.min(Math.max(l, 42), 62);
+
+ root.style.setProperty("--primary", hsl(h, primaryS, primaryL));
+ root.style.setProperty("--ring", hsl(h, primaryS, primaryL));
+ root.style.setProperty("--chart-1", hsl(h, primaryS, primaryL));
+ root.style.setProperty("--sidebar-primary", hsl(h, primaryS, primaryL));
+ root.style.setProperty("--sidebar-ring", hsl(h, primaryS, primaryL));
+ root.style.setProperty("--primary-foreground", "0 0% 100%");
+ root.style.setProperty("--sidebar-primary-foreground", "0 0% 100%");
+
+ root.style.setProperty("--accent", hsl(h, Math.round(primaryS * 0.64), 18));
+ root.style.setProperty(
+ "--accent-foreground",
+ hsl(h, Math.min(Math.round(primaryS * 1.15), 90), 78),
+ );
+ root.style.setProperty("--sidebar-accent", hsl(h, Math.round(primaryS * 0.57), 14));
+ root.style.setProperty(
+ "--sidebar-accent-foreground",
+ hsl(h, Math.min(Math.round(primaryS * 1.07), 88), 82),
+ );
+
+ return normalized;
+}
+
+export function clearAccentTheme(): void {
+ for (const name of CSS_VARS) {
+ document.documentElement.style.removeProperty(name);
+ }
+ document.documentElement.style.removeProperty("--primary-foreground");
+ document.documentElement.style.removeProperty("--sidebar-primary-foreground");
+}
+
+export function getStoredAccentHex(): string | null {
+ try {
+ const raw = localStorage.getItem(ACCENT_STORAGE_KEY);
+ if (!raw || !isValidHex(raw)) return null;
+ return normalizeHex(raw);
+ } catch {
+ return null;
+ }
+}
+
+export function saveAccentTheme(hex: string): string {
+ const normalized = applyAccentTheme(hex);
+ try {
+ localStorage.setItem(ACCENT_STORAGE_KEY, normalized);
+ } catch {
+ /* ignore quota */
+ }
+ return normalized;
+}
+
+export function resetAccentTheme(): void {
+ try {
+ localStorage.removeItem(ACCENT_STORAGE_KEY);
+ } catch {
+ /* ignore */
+ }
+ clearAccentTheme();
+}
+
+export function initAccentTheme(): void {
+ const stored = getStoredAccentHex();
+ if (stored) applyAccentTheme(stored);
+}
diff --git a/dashboard/src/lib/addon-config.ts b/dashboard/src/lib/addon-config.ts
new file mode 100644
index 0000000..06178f3
--- /dev/null
+++ b/dashboard/src/lib/addon-config.ts
@@ -0,0 +1,25 @@
+export const ADDON_CONFIG_PREFIX = "addon:";
+
+export function addonConfigFileParam(filename: string): string {
+ return `${ADDON_CONFIG_PREFIX}${filename}`;
+}
+
+export function parseConfigFileParam(
+ file: string | null,
+): { type: "main" | "addon"; file: string } | null {
+ if (!file) return null;
+ if (file.startsWith(ADDON_CONFIG_PREFIX)) {
+ return {
+ type: "addon",
+ file: file.slice(ADDON_CONFIG_PREFIX.length),
+ };
+ }
+ return { type: "main", file };
+}
+
+export function addonConfigLabel(filename: string): string {
+ return filename
+ .replace(/\.(yml|yaml|json)$/i, "")
+ .replace(/[-_]/g, " ")
+ .replace(/\b\w/g, (c) => c.toUpperCase());
+}
diff --git a/dashboard/src/lib/apply-dashboard-branding.ts b/dashboard/src/lib/apply-dashboard-branding.ts
new file mode 100644
index 0000000..026da48
--- /dev/null
+++ b/dashboard/src/lib/apply-dashboard-branding.ts
@@ -0,0 +1,23 @@
+import type { DashboardBranding } from "@/lib/branding-types";
+
+const FAVICON_LINK_ID = "supportbot-dashboard-favicon";
+
+export function applyDashboardBranding(branding: DashboardBranding) {
+ if (branding.pageTitle) {
+ document.title = branding.pageTitle;
+ }
+
+ let link = document.getElementById(FAVICON_LINK_ID) as HTMLLinkElement | null;
+ if (branding.faviconUrl) {
+ const href = `${branding.faviconUrl}${branding.updatedAt ? `?v=${branding.updatedAt}` : ""}`;
+ if (!link) {
+ link = document.createElement("link");
+ link.id = FAVICON_LINK_ID;
+ link.rel = "icon";
+ document.head.appendChild(link);
+ }
+ link.href = href;
+ } else if (link) {
+ link.remove();
+ }
+}
diff --git a/dashboard/src/lib/branding-types.ts b/dashboard/src/lib/branding-types.ts
new file mode 100644
index 0000000..faba84c
--- /dev/null
+++ b/dashboard/src/lib/branding-types.ts
@@ -0,0 +1,15 @@
+export interface DashboardBranding {
+ title: string;
+ pageTitle: string;
+ hasFavicon: boolean;
+ faviconUrl: string | null;
+ updatedAt: number | null;
+}
+
+export const DEFAULT_BRANDING: DashboardBranding = {
+ title: "SupportBot",
+ pageTitle: "SupportBot Dashboard",
+ hasFavicon: false,
+ faviconUrl: null,
+ updatedAt: null,
+};
diff --git a/dashboard/src/lib/config-schemas/index.ts b/dashboard/src/lib/config-schemas/index.ts
new file mode 100644
index 0000000..dfbc6f7
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/index.ts
@@ -0,0 +1,29 @@
+import type { ConfigSchema } from "./types";
+import { messagesSchema } from "./messages";
+import { supportbotSchema } from "./supportbot";
+import { supportbotAiSchema } from "./supportbot-ai";
+import { ticketPanelSchema } from "./ticket-panel";
+
+export type ConfigFileName =
+ | "supportbot"
+ | "ticket-panel"
+ | "commands"
+ | "messages"
+ | "supportbot-ai";
+
+export const configSchemas: Record = {
+ supportbot: supportbotSchema,
+ "ticket-panel": ticketPanelSchema,
+ commands: "commands",
+ messages: messagesSchema,
+ "supportbot-ai": supportbotAiSchema,
+};
+
+export function getSchema(file: string): ConfigSchema | "commands" | null {
+ if (file in configSchemas) {
+ return configSchemas[file as ConfigFileName];
+ }
+ return null;
+}
+
+export * from "./types";
diff --git a/dashboard/src/lib/config-schemas/messages.ts b/dashboard/src/lib/config-schemas/messages.ts
new file mode 100644
index 0000000..fb23756
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/messages.ts
@@ -0,0 +1,41 @@
+import type { ConfigSchema } from "./types";
+
+export const messagesSchema: ConfigSchema = [
+ {
+ title: "Info command",
+ fields: [
+ { path: "Info.Title", label: "Title", type: "text" },
+ { path: "Info.Description", label: "Description", type: "textarea" },
+ { path: "Info.Button", label: "Button label", type: "text" },
+ { path: "Info.URL", label: "Button URL", type: "text" },
+ { path: "Info.Colour", label: "Colour", type: "color" },
+ ],
+ },
+ {
+ title: "Welcome message",
+ fields: [
+ { path: "Welcome.Enabled", label: "Enabled", type: "boolean" },
+ {
+ path: "Welcome.LayoutStyle",
+ label: "Layout style",
+ type: "select",
+ options: [
+ { value: "modern", label: "Modern (components)" },
+ { value: "classic", label: "Classic embed" },
+ ],
+ },
+ { path: "Welcome.Embed.Colour", label: "Embed colour", type: "color" },
+ { path: "Welcome.Embed.Title", label: "Embed title", type: "text" },
+ { path: "Welcome.Embed.Message", label: "Embed message", type: "textarea" },
+ ],
+ },
+ {
+ title: "Leave message",
+ fields: [
+ { path: "Leave.Enabled", label: "Enabled", type: "boolean" },
+ { path: "Leave.Embed.Colour", label: "Embed colour", type: "color" },
+ { path: "Leave.Embed.Title", label: "Embed title", type: "text" },
+ { path: "Leave.Embed.Message", label: "Embed message", type: "textarea" },
+ ],
+ },
+];
diff --git a/dashboard/src/lib/config-schemas/supportbot-ai.ts b/dashboard/src/lib/config-schemas/supportbot-ai.ts
new file mode 100644
index 0000000..9224e76
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/supportbot-ai.ts
@@ -0,0 +1,142 @@
+import type { ConfigSchema } from "./types";
+
+const providers = [
+ { value: "openai", label: "OpenAI" },
+ { value: "groq", label: "Groq" },
+ { value: "claude", label: "Claude" },
+];
+
+const messageTypes = [
+ { value: "text", label: "Plain text" },
+ { value: "embed", label: "Embed" },
+ { value: "components_v2", label: "Components v2" },
+];
+
+const memoryScopes = [
+ { value: "channel", label: "Per channel" },
+ { value: "guild", label: "Per server" },
+];
+
+export const supportbotAiSchema: ConfigSchema = [
+ {
+ title: "General",
+ fields: [
+ { path: "Enabled", label: "AI enabled", type: "boolean" },
+ {
+ path: "General.Provider",
+ label: "Provider",
+ type: "select",
+ options: providers,
+ },
+ { path: "General.Model", label: "Model", type: "text" },
+ {
+ path: "General.Model_API_Key",
+ label: "API key",
+ type: "secret",
+ placeholder: "MODEL_API_KEY",
+ },
+ { path: "General.Tokens", label: "Max tokens", type: "number" },
+ {
+ path: "General.Temperature",
+ label: "Temperature",
+ type: "number",
+ },
+ { path: "General.Name", label: "Display name", type: "text" },
+ {
+ path: "General.PastebinAPI_Key",
+ label: "Pastebin API key",
+ type: "secret",
+ placeholder: "PASTEBIN_API_KEY",
+ },
+ {
+ path: "General.PastebinAPI_URL",
+ label: "Pastebin API URL",
+ type: "text",
+ },
+ ],
+ },
+ {
+ title: "Style & channel",
+ fields: [
+ {
+ path: "Style.MessageType",
+ label: "Message type",
+ type: "select",
+ options: messageTypes,
+ },
+ {
+ path: "Features.SuggestTickets",
+ label: "Suggest tickets",
+ type: "boolean",
+ },
+ {
+ path: "Channels.AIChannel",
+ label: "AI channel ID",
+ type: "discordId",
+ },
+ { path: "Embed.Color", label: "Embed colour", type: "color" },
+ { path: "Embed.Footer", label: "Embed footer", type: "text" },
+ ],
+ },
+ {
+ title: "Memory",
+ fields: [
+ { path: "Memory.Enabled", label: "Memory enabled", type: "boolean" },
+ { path: "Memory.StoreFacts", label: "Store facts", type: "boolean" },
+ {
+ path: "Memory.StoreSummaries",
+ label: "Store summaries",
+ type: "boolean",
+ },
+ {
+ path: "Memory.MaxRecentMessages",
+ label: "Max recent messages",
+ type: "number",
+ },
+ {
+ path: "Memory.Scope",
+ label: "Memory scope",
+ type: "select",
+ options: memoryScopes,
+ },
+ ],
+ },
+ {
+ title: "Custom instructions",
+ fields: [
+ {
+ path: "CustomInstructions",
+ label: "Instruction lines",
+ type: "stringList",
+ description: "One instruction per line sent to the AI.",
+ },
+ ],
+ },
+ {
+ title: "Messages",
+ fields: [
+ {
+ path: "Messages.ErrorResponse",
+ label: "Error response",
+ type: "text",
+ },
+ { path: "Messages.AIError", label: "AI error", type: "text" },
+ { path: "Messages.AIDisabled", label: "AI disabled", type: "text" },
+ {
+ path: "Messages.MessageTooLong",
+ label: "Message too long",
+ type: "text",
+ },
+ {
+ path: "Messages.SuggestTicketText",
+ label: "Suggest ticket text",
+ type: "text",
+ },
+ {
+ path: "Messages.SuggestTicketReaction",
+ label: "Suggest ticket emoji",
+ type: "emoji",
+ },
+ ],
+ },
+];
diff --git a/dashboard/src/lib/config-schemas/supportbot.ts b/dashboard/src/lib/config-schemas/supportbot.ts
new file mode 100644
index 0000000..11ce427
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/supportbot.ts
@@ -0,0 +1,507 @@
+import type { ConfigSchema } from "./types";
+
+const activityTypes = [
+ { value: "Playing", label: "Playing" },
+ { value: "Watching", label: "Watching" },
+ { value: "Listening", label: "Listening" },
+ { value: "Competing", label: "Competing" },
+ { value: "Streaming", label: "Streaming" },
+];
+
+const suggestionModes = [
+ { value: "Channel", label: "Channel" },
+ { value: "Forum", label: "Forum" },
+];
+
+const ticketTypes = [
+ { value: "channels", label: "Channels" },
+ { value: "threads", label: "Threads" },
+];
+
+const ticketSubjects = [
+ { value: "description", label: "Channel description" },
+ { value: "embed", label: "Embed" },
+];
+
+const buttonStyles = [
+ { value: "1", label: "Blurple" },
+ { value: "2", label: "Grey" },
+ { value: "3", label: "Green" },
+ { value: "4", label: "Red" },
+];
+
+export const supportbotSchema: ConfigSchema = [
+ {
+ title: "General",
+ fields: [
+ { path: "General.Name", label: "Bot name", type: "text" },
+ {
+ path: "General.Token",
+ label: "Bot token",
+ type: "secret",
+ description: "Masked in the dashboard. Your real token is preserved on save.",
+ },
+ {
+ path: "General.GuildId",
+ label: "Server (guild) ID",
+ type: "discordId",
+ description:
+ "Right-click your Discord server icon → Copy Server ID. The bot should only stay in this server.",
+ placeholder: "Discord server snowflake ID",
+ },
+ {
+ path: "General.Addons.Enabled",
+ label: "Addons enabled",
+ type: "boolean",
+ },
+ ],
+ },
+ {
+ title: "Activity",
+ fields: [
+ { path: "Activity.Status", label: "Status text", type: "text" },
+ {
+ path: "Activity.Type",
+ label: "Activity type",
+ type: "select",
+ options: activityTypes,
+ },
+ {
+ path: "Activity.StreamingURL",
+ label: "Streaming URL",
+ type: "text",
+ description: "Required when type is Streaming.",
+ },
+ ],
+ },
+ {
+ title: "Embeds",
+ fields: [
+ { path: "Embed.Colours.General", label: "General colour", type: "color" },
+ { path: "Embed.Colours.Success", label: "Success colour", type: "color" },
+ { path: "Embed.Colours.Error", label: "Error colour", type: "color" },
+ { path: "Embed.Colours.Warn", label: "Warn colour", type: "color" },
+ { path: "Embed.Footer", label: "Footer text", type: "text" },
+ ],
+ },
+ {
+ title: "Roles",
+ fields: [
+ {
+ path: "Roles.AutoRole.Enabled",
+ label: "Auto role enabled",
+ type: "boolean",
+ },
+ {
+ path: "Roles.AutoRole.Role",
+ label: "Auto role",
+ type: "discordRole",
+ },
+ {
+ path: "Roles.StaffMember.Staff",
+ label: "Staff role",
+ type: "discordRole",
+ },
+ {
+ path: "Roles.StaffMember.Admin",
+ label: "Admin role",
+ type: "discordRole",
+ },
+ {
+ path: "Roles.StaffMember.Moderator",
+ label: "Moderator role",
+ type: "discordRole",
+ },
+ {
+ path: "Roles.Mod.AllowSupportStaff",
+ label: "Allow support staff /mod",
+ type: "boolean",
+ },
+ ],
+ },
+ {
+ title: "Welcome & leave",
+ fields: [
+ { path: "Welcome.Enabled", label: "Welcome enabled", type: "boolean" },
+ {
+ path: "Welcome.Channel",
+ label: "Welcome channel",
+ type: "discordChannel",
+ },
+ { path: "Leave.Enabled", label: "Leave enabled", type: "boolean" },
+ {
+ path: "Leave.Channel",
+ label: "Leave channel",
+ type: "discordChannel",
+ },
+ ],
+ },
+ {
+ title: "Suggestions",
+ fields: [
+ {
+ path: "Suggestions.Mode",
+ label: "Mode",
+ type: "select",
+ options: suggestionModes,
+ },
+ {
+ path: "Suggestions.Channel",
+ label: "Suggestions channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Suggestions.ForumChannel",
+ label: "Forum channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Suggestions.UpvoteEmoji",
+ label: "Upvote emoji",
+ type: "emoji",
+ },
+ {
+ path: "Suggestions.DownvoteEmoji",
+ label: "Downvote emoji",
+ type: "emoji",
+ },
+ { path: "Suggestions.OwnSuggestion", label: "Own suggestion votes", type: "boolean" },
+ { path: "Suggestions.ShowUsers", label: "Show users", type: "boolean" },
+ {
+ path: "Suggestions.UpvoteTitle",
+ label: "Upvote button title",
+ type: "text",
+ },
+ {
+ path: "Suggestions.DownvoteTitle",
+ label: "Downvote button title",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Threads.Enabled",
+ label: "Suggestion threads",
+ type: "boolean",
+ },
+ {
+ path: "Suggestions.Threads.Title",
+ label: "Thread title",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Threads.Reason",
+ label: "Thread reason",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Buttons.Upvote",
+ label: "Upvote button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "Suggestions.Buttons.Downvote",
+ label: "Downvote button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "Suggestions.Buttons.RemoveVote",
+ label: "Remove vote button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "Suggestions.Buttons.RemoveVote_Title",
+ label: "Remove vote title",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Tags.Created",
+ label: "Tag: created",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Tags.Considering",
+ label: "Tag: considering",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Tags.Solved",
+ label: "Tag: solved",
+ type: "text",
+ },
+ {
+ path: "Suggestions.Tags.Denied",
+ label: "Tag: denied",
+ type: "text",
+ },
+ ],
+ },
+ {
+ title: "Tickets",
+ fields: [
+ {
+ path: "Ticket.TicketType",
+ label: "Ticket type",
+ type: "select",
+ options: ticketTypes,
+ },
+ {
+ path: "Ticket.UserManagement.DMOnAdd",
+ label: "DM when added to ticket",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.UserManagement.DMOnRemove",
+ label: "DM when removed from ticket",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.UserManagement.DMTranscript",
+ label: "DM transcript on close",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.TicketHome",
+ label: "Ticket home channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Ticket.TicketChannelsCategory",
+ label: "Ticket category",
+ type: "discordCategory",
+ },
+ {
+ path: "Ticket.TicketChannelsCategory2",
+ label: "Overflow ticket category",
+ type: "discordCategory",
+ description: "Used when the first category is full.",
+ },
+ {
+ path: "Ticket.TicketArchiveCategory",
+ label: "Archive category",
+ type: "discordCategory",
+ },
+ { path: "Ticket.Channel", label: "Channel prefix", type: "text" },
+ {
+ path: "Ticket.TicketSubject",
+ label: "Ticket subject display",
+ type: "select",
+ options: ticketSubjects,
+ },
+ { path: "Ticket.Timeout", label: "Close timeout (minutes)", type: "number" },
+ {
+ path: "Ticket.TicketsPerUser",
+ label: "Tickets per user",
+ type: "number",
+ },
+ { path: "Ticket.TicketReason", label: "Require reason", type: "boolean" },
+ {
+ path: "Ticket.Close.StaffOnly",
+ label: "Close staff only",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.Invites.StaffOnly",
+ label: "Ticket invites staff only",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.ClaimTickets.Enabled",
+ label: "Claim tickets",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.ClaimTickets.Channel",
+ label: "Claim log channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Ticket.ClaimTickets.ClaimAcceptTime",
+ label: "Claim accept time (ms)",
+ type: "number",
+ },
+ {
+ path: "Ticket.ClaimTickets.ButtonTitle",
+ label: "Claim button title",
+ type: "text",
+ },
+ {
+ path: "Ticket.ClaimTickets.ButtonEmoji",
+ label: "Claim button emoji",
+ type: "emoji",
+ },
+ {
+ path: "Ticket.ClaimTickets.Button",
+ label: "Claim button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "Ticket.ReviewSystem.Enabled",
+ label: "Review system",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.ReviewSystem.Channel",
+ label: "Review channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Ticket.ReviewSystem.UseModal",
+ label: "Review uses modal",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.Log.TicketDataLog",
+ label: "Ticket data log channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Ticket.Log.TickeDataTitle",
+ label: "Ticket data log title",
+ type: "text",
+ },
+ {
+ path: "Ticket.Log.TicketBlacklistLog",
+ label: "Blacklist log channel",
+ type: "discordChannel",
+ },
+ {
+ path: "Ticket.AIMode.Enabled",
+ label: "AI mode in tickets",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.AIMode.ThreadName",
+ label: "AI thread name",
+ type: "text",
+ },
+ {
+ path: "Ticket.AIMode.WelcomeMessage",
+ label: "AI welcome message",
+ type: "textarea",
+ },
+ {
+ path: "Ticket.DepartmentSystem.Enabled",
+ label: "Departments enabled",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.DepartmentSystem.Placeholder",
+ label: "Department placeholder",
+ type: "text",
+ },
+ {
+ path: "Ticket.DepartmentSystem.DefaultDepartment",
+ label: "Default department key",
+ type: "text",
+ },
+ {
+ path: "Ticket.PrioritySystem.Enabled",
+ label: "Priority system",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.PrioritySystem.DefaultPriority",
+ label: "Default priority",
+ type: "text",
+ },
+ {
+ path: "Ticket.PrioritySystem.AllowUsersToChooseOnOpen",
+ label: "User picks priority on open",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.Questions.Enabled",
+ label: "Ticket questions",
+ type: "boolean",
+ },
+ {
+ path: "Ticket.Questions.List",
+ label: "Default questions",
+ type: "stringList",
+ },
+ ],
+ },
+ {
+ title: "Voice tickets",
+ fields: [
+ { path: "VoiceTickets.Name", label: "VC name format", type: "text" },
+ {
+ path: "VoiceTickets.Category",
+ label: "VC category",
+ type: "discordCategory",
+ },
+ ],
+ },
+ {
+ title: "Logging",
+ fields: [
+ {
+ path: "MessageDelete.Channel",
+ label: "Message delete log channel",
+ type: "discordChannel",
+ },
+ {
+ path: "MessageDelete.Colour",
+ label: "Message delete embed colour",
+ type: "color",
+ },
+ {
+ path: "MessageUpdate.Channel",
+ label: "Message update log channel",
+ type: "discordChannel",
+ },
+ {
+ path: "MessageUpdate.Colour",
+ label: "Message update embed colour",
+ type: "color",
+ },
+ {
+ path: "Translate.TranslateLog",
+ label: "Translate log channel",
+ type: "discordChannel",
+ },
+ ],
+ },
+ {
+ title: "Buttons & menus",
+ description: "Ticket panel and control button labels and styles.",
+ fields: [
+ { path: "Buttons.General.Delete", label: "Delete emoji", type: "emoji" },
+ {
+ path: "Buttons.General.DeleteStyle",
+ label: "Delete button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "Buttons.Voice.TicketDeleteText",
+ label: "Voice delete label",
+ type: "text",
+ },
+ {
+ path: "Buttons.Tickets.ClaimEmoji",
+ label: "Ticket claim emoji",
+ type: "emoji",
+ },
+ {
+ path: "Buttons.Tickets.ClaimStyle",
+ label: "Ticket claim style",
+ type: "select",
+ options: buttonStyles,
+ },
+ {
+ path: "SelectMenus.Tickets.PanelEmoji",
+ label: "Panel menu emoji",
+ type: "emoji",
+ },
+ {
+ path: "SelectMenus.Tickets.CloseEmoji",
+ label: "Close menu emoji",
+ type: "emoji",
+ },
+ ],
+ },
+];
diff --git a/dashboard/src/lib/config-schemas/ticket-panel.ts b/dashboard/src/lib/config-schemas/ticket-panel.ts
new file mode 100644
index 0000000..d73e31a
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/ticket-panel.ts
@@ -0,0 +1,109 @@
+import type { ConfigSchema } from "./types";
+
+const buttonStyles = [
+ { value: "1", label: "Blurple" },
+ { value: "2", label: "Grey" },
+ { value: "3", label: "Green" },
+ { value: "4", label: "Red" },
+];
+
+const layouts = [
+ { value: "1", label: "Style 1" },
+ { value: "2", label: "Style 2" },
+ { value: "3", label: "Style 3" },
+ { value: "4", label: "Style 4 (classic embed)" },
+];
+
+export const ticketPanelSchema: ConfigSchema = [
+ {
+ title: "Panel",
+ fields: [{ path: "Panel", label: "Panel enabled", type: "boolean" }],
+ },
+ {
+ title: "Embed style",
+ fields: [
+ { path: "Style.Title", label: "Title", type: "text" },
+ { path: "Style.Description", label: "Description", type: "textarea" },
+ { path: "Style.Image", label: "Image URL", type: "text" },
+ { path: "Style.Color", label: "Embed colour", type: "color" },
+ {
+ path: "Style.Layout",
+ label: "Layout",
+ type: "select",
+ options: layouts,
+ },
+ ],
+ },
+ {
+ title: "Create button",
+ fields: [
+ { path: "Button.Text", label: "Button text", type: "text" },
+ { path: "Button.Emoji", label: "Button emoji", type: "emoji" },
+ {
+ path: "Button.Color",
+ label: "Button style",
+ type: "select",
+ options: buttonStyles,
+ },
+ { path: "Button.CustomId", label: "Custom ID", type: "text" },
+ ],
+ },
+ {
+ title: "Department menu",
+ fields: [
+ { path: "DepartmentMenu.CustomId", label: "Custom ID", type: "text" },
+ { path: "DepartmentMenu.Placeholder", label: "Placeholder", type: "text" },
+ {
+ path: "DepartmentMenu.DefaultDescription",
+ label: "Default description",
+ type: "text",
+ },
+ {
+ path: "DepartmentMenu.DefaultEmoji",
+ label: "Default emoji",
+ type: "emoji",
+ },
+ ],
+ },
+ {
+ title: "Panel settings",
+ fields: [
+ { path: "Settings.Image", label: "Show image", type: "boolean" },
+ { path: "Settings.TopTitle", label: "Top title", type: "boolean" },
+ { path: "Settings.TopDivider", label: "Top divider", type: "boolean" },
+ {
+ path: "Settings.BottomTitle",
+ label: "Bottom title",
+ type: "boolean",
+ },
+ {
+ path: "Settings.BottomDivider",
+ label: "Bottom divider",
+ type: "boolean",
+ },
+ ],
+ },
+ {
+ title: "Messages",
+ description: "Bot messages when using the ticket panel command.",
+ fields: [
+ {
+ path: "Messages.NotConfigured",
+ label: "Not configured",
+ type: "textarea",
+ },
+ {
+ path: "Messages.InvalidChannel",
+ label: "Invalid channel",
+ type: "textarea",
+ },
+ { path: "Messages.PanelSent", label: "Panel sent", type: "text" },
+ {
+ path: "Messages.PanelSentWithDepartments",
+ label: "Panel sent (departments)",
+ type: "text",
+ },
+ { path: "Messages.SendError", label: "Send error", type: "text" },
+ ],
+ },
+];
diff --git a/dashboard/src/lib/config-schemas/types.ts b/dashboard/src/lib/config-schemas/types.ts
new file mode 100644
index 0000000..e196657
--- /dev/null
+++ b/dashboard/src/lib/config-schemas/types.ts
@@ -0,0 +1,32 @@
+export type FieldType =
+ | "text"
+ | "textarea"
+ | "boolean"
+ | "number"
+ | "color"
+ | "emoji"
+ | "select"
+ | "discordId"
+ | "discordChannel"
+ | "discordRole"
+ | "discordCategory"
+ | "secret"
+ | "stringList"
+ | "googleFont";
+
+export interface ConfigField {
+ path: string;
+ label: string;
+ type: FieldType;
+ description?: string;
+ options?: { value: string; label: string }[];
+ placeholder?: string;
+}
+
+export interface ConfigSection {
+ title: string;
+ description?: string;
+ fields: ConfigField[];
+}
+
+export type ConfigSchema = ConfigSection[];
diff --git a/dashboard/src/lib/config-utils.ts b/dashboard/src/lib/config-utils.ts
new file mode 100644
index 0000000..6bc2ffa
--- /dev/null
+++ b/dashboard/src/lib/config-utils.ts
@@ -0,0 +1,57 @@
+export function getByPath(obj: Record, path: string): unknown {
+ const keys = path.split(".");
+ let cur: unknown = obj;
+ for (const key of keys) {
+ if (cur == null || typeof cur !== "object") return undefined;
+ cur = (cur as Record)[key];
+ }
+ return cur;
+}
+
+export function setByPath(
+ obj: Record,
+ path: string,
+ value: unknown,
+): Record {
+ const keys = path.split(".");
+ const clone = structuredClone(obj);
+ let cur: Record = clone;
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (cur[key] == null || typeof cur[key] !== "object") {
+ cur[key] = {};
+ }
+ cur = cur[key] as Record;
+ }
+ cur[keys[keys.length - 1]] = value;
+ return clone;
+}
+
+export function flattenUpdates(
+ configFile: string,
+ obj: Record,
+ prefix = "",
+): Record {
+ const out: Record = {};
+ for (const [key, val] of Object.entries(obj)) {
+ const path = prefix ? `${prefix}.${key}` : key;
+ if (
+ val !== null &&
+ typeof val === "object" &&
+ !Array.isArray(val) &&
+ Object.keys(val as object).length > 0 &&
+ !isLeafObject(val as Record)
+ ) {
+ Object.assign(out, flattenUpdates(configFile, val as Record, path));
+ } else {
+ out[`${configFile}:${path}`] = val;
+ }
+ }
+ return out;
+}
+
+function isLeafObject(o: Record