diff --git a/orchestrator/components.json b/orchestrator/components.json new file mode 100644 index 0000000..c17c397 --- /dev/null +++ b/orchestrator/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/orchestrator/index.html b/orchestrator/index.html index 409f757..fe833ac 100644 --- a/orchestrator/index.html +++ b/orchestrator/index.html @@ -1,5 +1,5 @@ - + diff --git a/orchestrator/package-lock.json b/orchestrator/package-lock.json index 3140b91..14deea5 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/package-lock.json @@ -8,14 +8,28 @@ "name": "job-ops-orchestrator", "version": "1.0.0", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "better-sqlite3": "^11.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cors": "^2.8.5", "dotenv": "^17.2.3", "drizzle-orm": "^0.38.2", "express": "^4.18.2", + "lucide-react": "^0.561.0", + "next-themes": "^0.4.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": { + "@tailwindcss/postcss": "^4.1.18", "@types/better-sqlite3": "^7.6.8", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -23,16 +37,31 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.22", "concurrently": "^9.1.0", "drizzle-kit": "^0.30.1", + "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2", + "tailwindcss": "^4.1.18", "tsx": "^4.19.2", "typescript": "^5.7.2", "vite": "^6.0.3" } }, + "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==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1257,6 +1286,616 @@ "dev": true, "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==" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "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-dialog": "1.1.15", + "@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-alert-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==", + "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==", + "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-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==", + "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==", + "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.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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==", + "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-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==", + "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==", + "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==", + "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-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==", + "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==", + "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-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==", + "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-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==", + "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-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==", + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "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-primitive/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==", + "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-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@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-progress/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==", + "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-progress/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==", + "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==", + "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-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==", + "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-separator/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==", + "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-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==", + "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==", + "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-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==", + "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==", + "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==", + "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==", + "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-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==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1572,6 +2211,262 @@ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1751,7 +2646,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -1850,12 +2745,60 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "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==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "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" + } + ], + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "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/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2116,6 +3059,17 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "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==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2131,6 +3085,14 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2309,6 +3271,11 @@ "node": ">=8" } }, + "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==" + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -2515,6 +3482,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -2771,6 +3751,19 @@ "node": ">= 0.6" } }, + "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==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2888,6 +3881,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -2932,6 +3933,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3059,11 +4066,19 @@ "node": ">=16" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "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==", - "devOptional": true, "license": "MIT" }, "node_modules/jsesc": { @@ -3092,11 +4107,259 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "devOptional": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -3115,6 +4378,23 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.561.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz", + "integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3251,6 +4531,15 @@ "node": ">= 0.6" } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-abi": { "version": "3.85.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", @@ -3282,6 +4571,15 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "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", @@ -3378,7 +4676,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3388,6 +4685,12 @@ "node": "^10 || ^12 || >=14" } }, + "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==", + "dev": true + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -3495,7 +4798,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -3508,7 +4810,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -3528,6 +4829,51 @@ "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==", + "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==", + "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.10.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", @@ -3582,6 +4928,27 @@ "url": "https://opencollective.com/express" } }, + "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==", + "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/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3698,7 +5065,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -4009,6 +5375,15 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "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": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4111,6 +5486,41 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==" + }, + "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==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -4179,7 +5589,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/tsx": { @@ -4647,6 +6056,14 @@ "node": "*" } }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4721,6 +6138,47 @@ "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==", + "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==", + "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/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/orchestrator/package.json b/orchestrator/package.json index 50f6289..f8a2634 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -18,14 +18,28 @@ "pipeline:run": "tsx src/server/pipeline/run.ts" }, "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "better-sqlite3": "^11.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "cors": "^2.8.5", "dotenv": "^17.2.3", "drizzle-orm": "^0.38.2", "express": "^4.18.2", + "lucide-react": "^0.561.0", + "next-themes": "^0.4.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "tw-animate-css": "^1.4.0", "zod": "^3.23.8" }, "devDependencies": { + "@tailwindcss/postcss": "^4.1.18", "@types/better-sqlite3": "^7.6.8", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -33,13 +47,16 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.22", "concurrently": "^9.1.0", "drizzle-kit": "^0.30.1", + "postcss": "^8.5.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2", + "tailwindcss": "^4.1.18", "tsx": "^4.19.2", "typescript": "^5.7.2", "vite": "^6.0.3" } -} \ No newline at end of file +} diff --git a/orchestrator/postcss.config.cjs b/orchestrator/postcss.config.cjs new file mode 100644 index 0000000..de8ec71 --- /dev/null +++ b/orchestrator/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + "@tailwindcss/postcss": {}, + autoprefixer: {}, + }, +}; diff --git a/orchestrator/src/client/App.tsx b/orchestrator/src/client/App.tsx index aa56344..fbeeda4 100644 --- a/orchestrator/src/client/App.tsx +++ b/orchestrator/src/client/App.tsx @@ -2,13 +2,15 @@ * Main App component. */ -import React, { useState, useEffect, useCallback } from 'react'; -import type { Job, JobStatus } from '../shared/types'; -import { Header, Stats, JobList, ToastContainer, Toast, PipelineProgress } from './components'; -import * as api from './api'; +import React, { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; + +import { Toaster } from "@/components/ui/sonner"; +import type { Job, JobStatus } from "../shared/types"; +import { Header, JobList, PipelineProgress, Stats } from "./components"; +import * as api from "./api"; export const App: React.FC = () => { - // State const [jobs, setJobs] = useState([]); const [stats, setStats] = useState>({ discovered: 0, @@ -22,19 +24,7 @@ export const App: React.FC = () => { const [isPipelineRunning, setIsPipelineRunning] = useState(false); const [processingJobId, setProcessingJobId] = useState(null); const [isProcessingAll, setIsProcessingAll] = useState(false); - const [toasts, setToasts] = useState([]); - - // Toast helpers - const addToast = useCallback((message: string, type: Toast['type']) => { - const id = Math.random().toString(36).slice(2); - setToasts(prev => [...prev, { id, message, type }]); - }, []); - - const dismissToast = useCallback((id: string) => { - setToasts(prev => prev.filter(t => t.id !== id)); - }, []); - - // Load jobs + const loadJobs = useCallback(async () => { try { setIsLoading(true); @@ -42,14 +32,13 @@ export const App: React.FC = () => { setJobs(data.jobs); setStats(data.byStatus); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to load jobs'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to load jobs"; + toast.error(message); } finally { setIsLoading(false); } - }, [addToast]); - - // Check pipeline status + }, []); + const checkPipelineStatus = useCallback(async () => { try { const status = await api.getPipelineStatus(); @@ -58,121 +47,121 @@ export const App: React.FC = () => { // Ignore errors } }, []); - - // Initial load + useEffect(() => { loadJobs(); checkPipelineStatus(); - - // Poll for updates + const interval = setInterval(() => { loadJobs(); checkPipelineStatus(); }, 10000); - + return () => clearInterval(interval); }, [loadJobs, checkPipelineStatus]); - - // Run pipeline + const handleRunPipeline = async () => { try { setIsPipelineRunning(true); await api.runPipeline(); - addToast('Pipeline started! This may take a few minutes.', 'info'); - - // Poll more frequently while running + toast.message("Pipeline started", { description: "This may take a few minutes." }); + const pollInterval = setInterval(async () => { - const status = await api.getPipelineStatus(); - if (!status.isRunning) { - clearInterval(pollInterval); - setIsPipelineRunning(false); - loadJobs(); - addToast('Pipeline completed!', 'success'); + try { + const status = await api.getPipelineStatus(); + if (!status.isRunning) { + clearInterval(pollInterval); + setIsPipelineRunning(false); + await loadJobs(); + toast.success("Pipeline completed"); + } + } catch { + // Ignore errors } }, 5000); } catch (error) { setIsPipelineRunning(false); - const message = error instanceof Error ? error.message : 'Failed to start pipeline'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to start pipeline"; + toast.error(message); } }; - - // Process single job + const handleProcess = async (jobId: string) => { try { setProcessingJobId(jobId); await api.processJob(jobId); - addToast('Resume generated successfully!', 'success'); - loadJobs(); + toast.success("Resume generated successfully"); + await loadJobs(); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to process job'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to process job"; + toast.error(message); } finally { setProcessingJobId(null); } }; - - // Mark as applied + const handleApply = async (jobId: string) => { try { await api.markAsApplied(jobId); - addToast('Marked as applied! ✅', 'success'); - loadJobs(); + toast.success("Marked as applied"); + await loadJobs(); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to mark as applied'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to mark as applied"; + toast.error(message); } }; - - // Reject job + const handleReject = async (jobId: string) => { try { await api.rejectJob(jobId); - addToast('Job skipped', 'info'); - loadJobs(); + toast.message("Job skipped"); + await loadJobs(); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to reject job'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to reject job"; + toast.error(message); } }; - - // Clear database + const handleClearDatabase = async () => { try { const result = await api.clearDatabase(); - addToast(`Database cleared! Deleted ${result.jobsDeleted} jobs.`, 'success'); - loadJobs(); + toast.success("Database cleared", { description: `Deleted ${result.jobsDeleted} jobs.` }); + await loadJobs(); } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to clear database'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to clear database"; + toast.error(message); } }; - - // Process all discovered jobs + const handleProcessAll = async () => { try { setIsProcessingAll(true); const result = await api.processAllDiscovered(); - addToast(`Processing ${result.count} jobs in background...`, 'info'); - - // Poll for completion + toast.message("Processing jobs", { description: `Processing ${result.count} jobs in background...` }); + const pollInterval = setInterval(async () => { - await loadJobs(); - const currentStats = await api.getJobs(); - const stillDiscovered = currentStats.byStatus.discovered + currentStats.byStatus.processing; - if (stillDiscovered === 0) { - clearInterval(pollInterval); - setIsProcessingAll(false); - addToast('All jobs processed!', 'success'); + try { + const data = await api.getJobs(); + setJobs(data.jobs); + setStats(data.byStatus); + + const stillDiscovered = data.byStatus.discovered + data.byStatus.processing; + if (stillDiscovered === 0) { + clearInterval(pollInterval); + setIsProcessingAll(false); + toast.success("All jobs processed"); + } + } catch { + // Ignore errors } }, 3000); } catch (error) { setIsProcessingAll(false); - const message = error instanceof Error ? error.message : 'Failed to process jobs'; - addToast(message, 'error'); + const message = error instanceof Error ? error.message : "Failed to process jobs"; + toast.error(message); } }; - + return ( <>
{ isPipelineRunning={isPipelineRunning} isLoading={isLoading} /> - -
+ +
- - { isProcessingAll={isProcessingAll} />
- - + + ); }; + diff --git a/orchestrator/src/client/components/Header.tsx b/orchestrator/src/client/components/Header.tsx index b45ec1a..7fe16aa 100644 --- a/orchestrator/src/client/components/Header.tsx +++ b/orchestrator/src/client/components/Header.tsx @@ -2,8 +2,21 @@ * Header component with logo and pipeline trigger. */ -import React from 'react'; -import { RocketIcon, PlayIcon, RefreshIcon, TrashIcon } from './Icons'; +import React from "react"; +import { Loader2, Play, RefreshCcw, Rocket, Trash2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; interface HeaderProps { onRunPipeline: () => void; @@ -20,62 +33,68 @@ export const Header: React.FC = ({ isPipelineRunning, isLoading, }) => { - const handleClearDatabase = () => { - if (window.confirm('Are you sure you want to clear all jobs from the database? This cannot be undone.')) { - onClearDatabase(); - } - }; - return ( -
-
-
-
-
- -
- Job Ops +
+
+
+
+
- -
- - - - - +
+
Job Ops
+
Orchestrator
+ +
+ + + + + + + Clear all jobs? + + This deletes all jobs from the database. This action cannot be + undone. + + + + Cancel + + Clear database + + + + + + + + +
); diff --git a/orchestrator/src/client/components/Icons.tsx b/orchestrator/src/client/components/Icons.tsx deleted file mode 100644 index 043c857..0000000 --- a/orchestrator/src/client/components/Icons.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/** - * SVG Icons as React components. - */ - -import React from 'react'; - -interface IconProps { - className?: string; - size?: number; -} - -export const BriefcaseIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const MapPinIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const CalendarIcon: React.FC = ({ className, size = 16 }) => ( - - - - - - -); - -export const DollarIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const GraduationCapIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const ExternalLinkIcon: React.FC = ({ className, size = 16 }) => ( - - - - - -); - -export const FileTextIcon: React.FC = ({ className, size = 16 }) => ( - - - - - - - -); - -export const CheckCircleIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const XCircleIcon: React.FC = ({ className, size = 16 }) => ( - - - - - -); - -export const RefreshIcon: React.FC = ({ className, size = 16 }) => ( - - - - - -); - -export const PlayIcon: React.FC = ({ className, size = 16 }) => ( - - - -); - -export const DownloadIcon: React.FC = ({ className, size = 16 }) => ( - - - - - -); - -export const XIcon: React.FC = ({ className, size = 16 }) => ( - - - - -); - -export const RocketIcon: React.FC = ({ className, size = 16 }) => ( - - - - - - -); - -export const TrashIcon: React.FC = ({ className, size = 16 }) => ( - - - - - - -); diff --git a/orchestrator/src/client/components/JobCard.tsx b/orchestrator/src/client/components/JobCard.tsx index 3c6e70d..185136d 100644 --- a/orchestrator/src/client/components/JobCard.tsx +++ b/orchestrator/src/client/components/JobCard.tsx @@ -2,21 +2,25 @@ * Individual job card component. */ -import React from 'react'; -import type { Job } from '../../shared/types'; -import { StatusBadge } from './StatusBadge'; -import { ScoreIndicator } from './ScoreIndicator'; +import React from "react"; import { - MapPinIcon, - CalendarIcon, - DollarIcon, - GraduationCapIcon, - ExternalLinkIcon, - DownloadIcon, - CheckCircleIcon, - XCircleIcon, - RefreshIcon, -} from './Icons'; + Calendar, + CheckCircle2, + DollarSign, + Download, + ExternalLink, + GraduationCap, + Loader2, + MapPin, + RefreshCcw, + XCircle, +} from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import type { Job } from "../../shared/types"; +import { ScoreIndicator } from "./ScoreIndicator"; +import { StatusBadge } from "./StatusBadge"; interface JobCardProps { job: Job; @@ -26,6 +30,21 @@ interface JobCardProps { isProcessing: boolean; } +const formatDate = (dateStr: string | null) => { + if (!dateStr) return null; + try { + return new Date(dateStr).toLocaleDateString("en-GB", { + day: "numeric", + month: "short", + year: "numeric", + }); + } catch { + return dateStr; + } +}; + +const safeFilenamePart = (value: string) => value.replace(/[^a-z0-9]/gi, "_"); + export const JobCard: React.FC = ({ job, onApply, @@ -33,155 +52,133 @@ export const JobCard: React.FC = ({ onProcess, isProcessing, }) => { - const formatDate = (dateStr: string | null) => { - if (!dateStr) return null; - try { - return new Date(dateStr).toLocaleDateString('en-GB', { - day: 'numeric', - month: 'short', - year: 'numeric', - }); - } catch { - return dateStr; - } - }; - const hasPdf = !!job.pdfPath; - const canApply = job.status === 'ready'; - const canProcess = job.status === 'discovered'; - const canReject = ['discovered', 'ready'].includes(job.status); - + const canApply = job.status === "ready"; + const canProcess = job.status === "discovered"; + const canReject = ["discovered", "ready"].includes(job.status); + + const jobLink = job.applicationLink || job.jobUrl; + const pdfHref = `/pdfs/resume_${job.id}.pdf`; + const deadline = formatDate(job.deadline); + return ( -
-
-
-

{job.title}

-

{job.employer}

+ + +
+
+ {job.title} +
{job.employer}
+
+ +
+ + +
-
- - + +
+ {job.location && ( + + + {job.location} + + )} + {deadline && ( + + + {deadline} + + )} + {job.salary && ( + + + {job.salary} + + )} + {job.degreeRequired && ( + + + {job.degreeRequired} + + )}
-
- -
- {job.location && ( - - - {job.location} - - )} - {job.deadline && ( - - - {job.deadline} - - )} - {job.salary && ( - - - {job.salary} - - )} - {job.degreeRequired && ( - - - {job.degreeRequired} - - )} -
- - {job.suitabilityReason && ( -

- "{job.suitabilityReason}" -

+
+ + {(job.suitabilityReason || canApply || canReject || canProcess || hasPdf) && ( + + {job.suitabilityReason && ( +

+ "{job.suitabilityReason}" +

+ )} +
)} - -
- {/* View job posting */} - - - View Job - - - {/* View PDF in browser */} - {hasPdf && ( - - - View PDF + + + + {hasPdf && ( - - - Download - + )} - - {/* Process job */} + + {hasPdf && ( + + )} + {canProcess && ( - + )} - - {/* Reject */} + {canReject && ( - + )} - - {/* Mark as applied */} + {canApply && ( - + )} -
-
+ + ); }; + diff --git a/orchestrator/src/client/components/JobList.tsx b/orchestrator/src/client/components/JobList.tsx index 03aa198..9e48c97 100644 --- a/orchestrator/src/client/components/JobList.tsx +++ b/orchestrator/src/client/components/JobList.tsx @@ -2,10 +2,14 @@ * Job list with filtering tabs. */ -import React, { useState } from 'react'; -import type { Job, JobStatus } from '../../shared/types'; -import { JobCard } from './JobCard'; -import { RefreshIcon } from './Icons'; +import React, { useMemo, useState } from "react"; +import { Loader2, RefreshCcw } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { Job, JobStatus } from "../../shared/types"; +import { JobCard } from "./JobCard"; interface JobListProps { jobs: Job[]; @@ -17,15 +21,22 @@ interface JobListProps { isProcessingAll: boolean; } -type FilterTab = 'ready' | 'discovered' | 'applied' | 'all'; +type FilterTab = "ready" | "discovered" | "applied" | "all"; const tabs: Array<{ id: FilterTab; label: string; statuses: JobStatus[] }> = [ - { id: 'ready', label: '✨ Ready to Apply', statuses: ['ready'] }, - { id: 'discovered', label: '🔍 Discovered', statuses: ['discovered', 'processing'] }, - { id: 'applied', label: '✅ Applied', statuses: ['applied'] }, - { id: 'all', label: '📋 All Jobs', statuses: [] }, + { id: "ready", label: "Ready", statuses: ["ready"] }, + { id: "discovered", label: "Discovered", statuses: ["discovered", "processing"] }, + { id: "applied", label: "Applied", statuses: ["applied"] }, + { id: "all", label: "All Jobs", statuses: [] }, ]; +const emptyStateCopy: Record = { + ready: "Run the pipeline to discover and process new jobs.", + discovered: "All discovered jobs have been processed.", + applied: "You haven't applied to any jobs yet.", + all: "No jobs in the system yet. Run the pipeline to get started!", +}; + export const JobList: React.FC = ({ jobs, onApply, @@ -35,86 +46,104 @@ export const JobList: React.FC = ({ processingJobId, isProcessingAll, }) => { - const [activeTab, setActiveTab] = useState('ready'); - - const filteredJobs = React.useMemo(() => { - const tab = tabs.find(t => t.id === activeTab); - if (!tab || tab.statuses.length === 0) { - return jobs; + const [activeTab, setActiveTab] = useState("ready"); + + const counts = useMemo(() => { + const byTab: Record = { + ready: 0, + discovered: 0, + applied: 0, + all: jobs.length, + }; + + for (const job of jobs) { + if (job.status === "ready") byTab.ready += 1; + if (job.status === "applied") byTab.applied += 1; + if (job.status === "discovered" || job.status === "processing") byTab.discovered += 1; } - return jobs.filter(job => tab.statuses.includes(job.status)); - }, [jobs, activeTab]); - - const discoveredCount = jobs.filter(j => j.status === 'discovered').length; - + + return byTab; + }, [jobs]); + + const jobsForTab = useMemo(() => { + const map = new Map(); + + for (const tab of tabs) { + if (tab.statuses.length === 0) { + map.set(tab.id, jobs); + } else { + map.set(tab.id, jobs.filter((job) => tab.statuses.includes(job.status))); + } + } + + return map; + }, [jobs]); + return ( -
-
-
- {tabs.map(tab => { - const count = tab.statuses.length === 0 - ? jobs.length - : jobs.filter(j => tab.statuses.includes(j.status)).length; - - return ( - - ); - })} -
- - {activeTab === 'discovered' && discoveredCount > 0 && ( - + )}
- - {filteredJobs.length === 0 ? ( -
-
📭
-

No jobs found

-

- {activeTab === 'ready' && 'Run the pipeline to discover and process new jobs.'} - {activeTab === 'discovered' && 'All discovered jobs have been processed.'} - {activeTab === 'applied' && "You haven't applied to any jobs yet."} - {activeTab === 'all' && 'No jobs in the system yet. Run the pipeline to get started!'} -

-
- ) : ( -
- {filteredJobs.map(job => ( - - ))} -
- )} -
+ + {tabs.map((tab) => { + const filteredJobs = jobsForTab.get(tab.id) ?? []; + + return ( + + {filteredJobs.length === 0 ? ( + + +
No jobs found
+

{emptyStateCopy[tab.id]}

+
+
+ ) : ( +
+ {filteredJobs.map((job) => ( + + ))} +
+ )} +
+ ); + })} + ); }; + diff --git a/orchestrator/src/client/components/PipelineProgress.tsx b/orchestrator/src/client/components/PipelineProgress.tsx index 2097f3d..a0a3b26 100644 --- a/orchestrator/src/client/components/PipelineProgress.tsx +++ b/orchestrator/src/client/components/PipelineProgress.tsx @@ -2,10 +2,17 @@ * Live pipeline progress display component. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from "react"; +import { Loader2 } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; interface PipelineProgress { - step: 'idle' | 'crawling' | 'importing' | 'scoring' | 'processing' | 'completed' | 'failed'; + step: "idle" | "crawling" | "importing" | "scoring" | "processing" | "completed" | "failed"; message: string; detail?: string; crawlingListPagesProcessed: number; @@ -14,7 +21,7 @@ interface PipelineProgress { crawlingJobPagesEnqueued: number; crawlingJobPagesSkipped: number; crawlingJobPagesProcessed: number; - crawlingPhase?: 'list' | 'job'; + crawlingPhase?: "list" | "job"; crawlingCurrentUrl?: string; jobsDiscovered: number; jobsScored: number; @@ -34,26 +41,28 @@ interface PipelineProgressProps { isRunning: boolean; } -const stepLabels: Record = { - idle: 'Ready', - crawling: 'Crawling Jobs', - importing: 'Importing', - scoring: 'Scoring Jobs', - processing: 'Generating Resumes', - completed: 'Complete', - failed: 'Failed', +const stepLabels: Record = { + idle: "Ready", + crawling: "Crawling", + importing: "Importing", + scoring: "Scoring", + processing: "Processing", + completed: "Complete", + failed: "Failed", }; -const stepColors: Record = { - idle: 'var(--color-muted)', - crawling: 'var(--color-info)', - importing: 'var(--color-info)', - scoring: 'var(--color-warning)', - processing: 'var(--color-primary-500)', - completed: 'var(--color-success)', - failed: 'var(--color-error)', +const stepBadgeClasses: Record = { + idle: "bg-muted text-muted-foreground border-border", + crawling: "bg-sky-500/10 text-sky-400 border-sky-500/20", + importing: "bg-sky-500/10 text-sky-400 border-sky-500/20", + scoring: "bg-amber-500/10 text-amber-400 border-amber-500/20", + processing: "bg-primary/10 text-primary border-primary/20", + completed: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", + failed: "bg-destructive/10 text-destructive border-destructive/20", }; +const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); + export const PipelineProgress: React.FC = ({ isRunning }) => { const [progress, setProgress] = useState(null); const [isConnected, setIsConnected] = useState(false); @@ -61,29 +70,28 @@ export const PipelineProgress: React.FC = ({ isRunning }) useEffect(() => { if (!isRunning) { setProgress(null); + setIsConnected(false); return; } - // Connect to SSE endpoint - const eventSource = new EventSource('/api/pipeline/progress'); - + const eventSource = new EventSource("/api/pipeline/progress"); + eventSource.onopen = () => { setIsConnected(true); }; - + eventSource.onmessage = (event) => { try { - const data = JSON.parse(event.data); - setProgress(data); + setProgress(JSON.parse(event.data)); } catch { // Ignore parse errors } }; - + eventSource.onerror = () => { setIsConnected(false); }; - + return () => { eventSource.close(); setIsConnected(false); @@ -94,203 +102,137 @@ export const PipelineProgress: React.FC = ({ isRunning }) return null; } - const step = progress?.step || 'idle'; - const isActive = progress && step !== 'idle' && step !== 'completed' && step !== 'failed'; + const step = progress?.step ?? "idle"; + const isActive = step !== "idle" && step !== "completed" && step !== "failed"; - // Calculate overall progress percentage - let percentage = 0; - if (progress) { - switch (step) { - case 'crawling': + const percentage = useMemo(() => { + if (!progress) return 0; + + switch (progress.step) { + case "crawling": { if (progress.crawlingListPagesTotal > 0) { - percentage = (progress.crawlingListPagesProcessed / progress.crawlingListPagesTotal) * 15; - } else if (progress.crawlingListPagesProcessed > 0) { - percentage = 8; - } else { - percentage = 5; + return clamp((progress.crawlingListPagesProcessed / progress.crawlingListPagesTotal) * 15, 0, 15); } - break; - case 'importing': - percentage = 20; - break; - case 'scoring': + if (progress.crawlingListPagesProcessed > 0) return 8; + return 5; + } + case "importing": + return 20; + case "scoring": { if (progress.jobsScored > 0) { - percentage = 20 + (progress.jobsScored / Math.max(progress.jobsDiscovered, 1)) * 30; - } else { - percentage = 25; + return clamp(20 + (progress.jobsScored / Math.max(progress.jobsDiscovered, 1)) * 30, 20, 50); } - break; - case 'processing': + return 25; + } + case "processing": { if (progress.totalToProcess > 0) { - percentage = 50 + (progress.jobsProcessed / progress.totalToProcess) * 50; - } else { - percentage = 55; + return clamp(50 + (progress.jobsProcessed / progress.totalToProcess) * 50, 50, 100); } - break; - case 'completed': - percentage = 100; - break; - case 'failed': - percentage = 100; - break; + return 55; + } + case "completed": + case "failed": + return 100; + case "idle": + default: + return 0; } - } + }, [progress]); + + const showStats = !!progress && ["crawling", "scoring", "processing", "completed"].includes(step); return ( -
- {/* Header */} -
-
- {isActive && ( -
- )} - - {stepLabels[step]} - + + +
+
+ Pipeline + + {stepLabels[step]} + + + {isConnected ? "Live" : "Connecting…"} + +
+ +
+ {isActive && } + {Math.round(percentage)}% +
- - {Math.round(percentage)}% - -
- - {/* Progress bar */} -
-
-
- - {/* Message */} + + + + {progress && ( -
-

- {progress.message} -

- {progress.detail && ( -

- {progress.detail} -

- )} -
- )} - - {/* Stats */} - {progress && (step === 'crawling' || step === 'scoring' || step === 'processing' || step === 'completed') && ( -
- {step === 'crawling' && ( + +
+

{progress.message}

+ {progress.detail &&

{progress.detail}

} +
+ + {showStats && ( <> -
- Sources: - - {progress.crawlingListPagesProcessed} - {progress.crawlingListPagesTotal > 0 ? `/${progress.crawlingListPagesTotal}` : ''} - + +
+ {step === "crawling" ? ( + <> +
+
Sources
+
+ {progress.crawlingListPagesProcessed} + {progress.crawlingListPagesTotal > 0 ? `/${progress.crawlingListPagesTotal}` : ""} +
+
+
+
Pages
+
+ {progress.crawlingJobPagesProcessed}/{Math.max(progress.crawlingJobPagesEnqueued, 0)} +
+
+
+
Enqueued
+
{progress.crawlingJobPagesEnqueued}
+
+
+
Skipped
+
{progress.crawlingJobPagesSkipped}
+
+ + ) : ( + <> +
+
Discovered
+
{progress.jobsDiscovered}
+
+
+
Scored
+
{progress.jobsScored}
+
+
+
Processed
+
+ {progress.totalToProcess > 0 ? `${progress.jobsProcessed}/${progress.totalToProcess}` : progress.jobsProcessed} +
+
+
+
To process
+
{progress.totalToProcess}
+
+ + )}
-
- Pages: - - {progress.crawlingJobPagesProcessed}/{Math.max(progress.crawlingJobPagesEnqueued, 0)} - -
-
- Enqueued: - - {progress.crawlingJobPagesEnqueued} - -
- {progress.crawlingJobPagesSkipped > 0 && ( -
- Skipped: - - {progress.crawlingJobPagesSkipped} - -
- )} - {progress.crawlingJobCardsFound > 0 && ( -
- Cards: - - {progress.crawlingJobCardsFound} - -
- )} )} - {step !== 'crawling' && ( -
- Discovered: - - {progress.jobsDiscovered} - + + {step === "failed" && progress.error && ( +
+ {progress.error}
)} - {progress.jobsScored > 0 && ( -
- Scored: - - {progress.jobsScored} - -
- )} - {progress.totalToProcess > 0 && ( -
- Processed: - - {progress.jobsProcessed}/{progress.totalToProcess} - -
- )} -
+ )} - - {/* Error state */} - {step === 'failed' && progress?.error && ( -
- {progress.error} -
- )} -
+ ); }; + diff --git a/orchestrator/src/client/components/ScoreIndicator.tsx b/orchestrator/src/client/components/ScoreIndicator.tsx index 0d7ab54..2138841 100644 --- a/orchestrator/src/client/components/ScoreIndicator.tsx +++ b/orchestrator/src/client/components/ScoreIndicator.tsx @@ -2,7 +2,9 @@ * Suitability score display component. */ -import React from 'react'; +import React from "react"; + +import { Progress } from "@/components/ui/progress"; interface ScoreIndicatorProps { score: number | null; @@ -10,28 +12,14 @@ interface ScoreIndicatorProps { export const ScoreIndicator: React.FC = ({ score }) => { if (score === null) { - return ( - - Not scored - - ); + return Not scored; } - - const getScoreClass = () => { - if (score >= 70) return 'score-high'; - if (score >= 40) return 'score-medium'; - return 'score-low'; - }; - + return ( -
-
-
-
- {score} +
+ + {score}
); }; + diff --git a/orchestrator/src/client/components/Stats.tsx b/orchestrator/src/client/components/Stats.tsx index 39e2230..abd0349 100644 --- a/orchestrator/src/client/components/Stats.tsx +++ b/orchestrator/src/client/components/Stats.tsx @@ -2,8 +2,18 @@ * Stats dashboard showing job counts by status. */ -import React from 'react'; -import type { JobStatus } from '../../shared/types'; +import React from "react"; +import { + CheckCircle2, + Clock, + Loader2, + Search, + Sparkles, + XCircle, +} from "lucide-react"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import type { JobStatus } from "../../shared/types"; interface StatsProps { stats: Record; @@ -12,39 +22,56 @@ interface StatsProps { const statConfig: Array<{ key: JobStatus; label: string; - emoji: string; + Icon: React.ComponentType<{ className?: string }>; }> = [ - { key: 'discovered', label: 'Discovered', emoji: '🔍' }, - { key: 'processing', label: 'Processing', emoji: '⚙️' }, - { key: 'ready', label: 'Ready', emoji: '✨' }, - { key: 'applied', label: 'Applied', emoji: '✅' }, - { key: 'rejected', label: 'Rejected', emoji: '❌' }, - { key: 'expired', label: 'Expired', emoji: '⏰' }, + { key: "discovered", label: "Discovered", Icon: Search }, + { key: "processing", label: "Processing", Icon: Loader2 }, + { key: "ready", label: "Ready", Icon: Sparkles }, + { key: "applied", label: "Applied", Icon: CheckCircle2 }, + { key: "rejected", label: "Rejected", Icon: XCircle }, + { key: "expired", label: "Expired", Icon: Clock }, ]; export const Stats: React.FC = ({ stats }) => { const total = Object.values(stats).reduce((a, b) => a + b, 0); - + return ( -
-
-

Overview

- - {total} total jobs - -
- -
- {statConfig.map(({ key, label, emoji }) => ( -
-
{stats[key] || 0}
-
- {emoji} - {label} -
-
- ))} -
-
+ + + Overview +
{total} total jobs
+
+ + +
+ {statConfig.map(({ key, label, Icon }) => ( + + +
+
+ +
+
+
+ {stats[key] || 0} +
+
+ {label} +
+
+
+
+
+ ))} +
+
+
); }; + diff --git a/orchestrator/src/client/components/StatusBadge.tsx b/orchestrator/src/client/components/StatusBadge.tsx index c7fe138..b2158ac 100644 --- a/orchestrator/src/client/components/StatusBadge.tsx +++ b/orchestrator/src/client/components/StatusBadge.tsx @@ -2,27 +2,46 @@ * Status badge component. */ -import React from 'react'; -import type { JobStatus } from '../../shared/types'; +import React from "react"; +import { Loader2 } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; +import type { JobStatus } from "../../shared/types"; interface StatusBadgeProps { status: JobStatus; } const statusLabels: Record = { - discovered: 'Discovered', - processing: 'Processing', - ready: 'Ready', - applied: 'Applied', - rejected: 'Rejected', - expired: 'Expired', + discovered: "Discovered", + processing: "Processing", + ready: "Ready", + applied: "Applied", + rejected: "Rejected", + expired: "Expired", +}; + +const statusStyles: Record< + JobStatus, + { variant: "default" | "secondary" | "destructive" | "outline"; className?: string } +> = { + discovered: { variant: "secondary" }, + processing: { variant: "secondary" }, + ready: { variant: "default" }, + applied: { variant: "outline", className: "text-emerald-400 border-emerald-500/30" }, + rejected: { variant: "destructive" }, + expired: { variant: "outline", className: "text-muted-foreground" }, }; export const StatusBadge: React.FC = ({ status }) => { + const { variant, className } = statusStyles[status]; + return ( - - {status === 'processing' && } + + {status === "processing" && } {statusLabels[status]} - + ); }; + diff --git a/orchestrator/src/client/components/Toast.tsx b/orchestrator/src/client/components/Toast.tsx deleted file mode 100644 index ae520ac..0000000 --- a/orchestrator/src/client/components/Toast.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Toast notification component. - */ - -import React, { useEffect } from 'react'; - -export interface Toast { - id: string; - message: string; - type: 'success' | 'error' | 'info'; -} - -interface ToastContainerProps { - toasts: Toast[]; - onDismiss: (id: string) => void; -} - -export const ToastContainer: React.FC = ({ toasts, onDismiss }) => { - return ( -
- {toasts.map(toast => ( - - ))} -
- ); -}; - -const ToastItem: React.FC<{ toast: Toast; onDismiss: (id: string) => void }> = ({ - toast, - onDismiss, -}) => { - useEffect(() => { - const timer = setTimeout(() => { - onDismiss(toast.id); - }, 5000); - return () => clearTimeout(timer); - }, [toast.id, onDismiss]); - - return ( -
- {toast.message} -
- ); -}; diff --git a/orchestrator/src/client/components/index.ts b/orchestrator/src/client/components/index.ts index 3246784..b8adf8b 100644 --- a/orchestrator/src/client/components/index.ts +++ b/orchestrator/src/client/components/index.ts @@ -4,6 +4,4 @@ export { StatusBadge } from './StatusBadge'; export { ScoreIndicator } from './ScoreIndicator'; export { JobCard } from './JobCard'; export { JobList } from './JobList'; -export { ToastContainer, type Toast } from './Toast'; export { PipelineProgress } from './PipelineProgress'; -export * from './Icons'; diff --git a/orchestrator/src/client/main.tsx b/orchestrator/src/client/main.tsx index f8136ef..ac226f6 100644 --- a/orchestrator/src/client/main.tsx +++ b/orchestrator/src/client/main.tsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './App'; -import './styles/index.css'; +import '../index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/orchestrator/src/client/styles/index.css b/orchestrator/src/client/styles/index.css deleted file mode 100644 index 3d4ab42..0000000 --- a/orchestrator/src/client/styles/index.css +++ /dev/null @@ -1,680 +0,0 @@ -/* =================================================================== - Job Ops Orchestrator - Design System - A modern, dark-mode first design with glassmorphism and gradients - =================================================================== */ - -/* CSS Custom Properties (Design Tokens) */ -:root { - /* Colors */ - --color-background: #0a0a0f; - --color-surface: #12121a; - --color-surface-elevated: #1a1a25; - --color-surface-glass: rgba(26, 26, 37, 0.7); - - --color-border: rgba(255, 255, 255, 0.08); - --color-border-light: rgba(255, 255, 255, 0.12); - - --color-text-primary: #f5f5f7; - --color-text-secondary: #a1a1aa; - --color-text-muted: #71717a; - - /* Accent colors */ - --color-primary: #6366f1; - --color-primary-light: #818cf8; - --color-primary-dark: #4f46e5; - --color-primary-glow: rgba(99, 102, 241, 0.3); - - --color-success: #10b981; - --color-success-light: #34d399; - --color-success-glow: rgba(16, 185, 129, 0.2); - - --color-warning: #f59e0b; - --color-warning-light: #fbbf24; - - --color-danger: #ef4444; - --color-danger-light: #f87171; - --color-danger-glow: rgba(239, 68, 68, 0.2); - - --color-info: #3b82f6; - --color-info-light: #60a5fa; - - /* Gradients */ - --gradient-primary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); - --gradient-success: linear-gradient(135deg, #10b981 0%, #34d399 100%); - --gradient-mesh: radial-gradient(at 40% 20%, hsla(250, 80%, 60%, 0.1) 0px, transparent 50%), - radial-gradient(at 80% 0%, hsla(280, 80%, 50%, 0.1) 0px, transparent 50%), - radial-gradient(at 0% 50%, hsla(220, 100%, 60%, 0.05) 0px, transparent 50%); - - /* Typography */ - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'SF Mono', Monaco, 'Cascadia Code', monospace; - - /* Spacing */ - --space-1: 0.25rem; - --space-2: 0.5rem; - --space-3: 0.75rem; - --space-4: 1rem; - --space-5: 1.25rem; - --space-6: 1.5rem; - --space-8: 2rem; - --space-10: 2.5rem; - --space-12: 3rem; - - /* Border Radius */ - --radius-sm: 0.375rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; - --radius-2xl: 1.5rem; - --radius-full: 9999px; - - /* Shadows */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.4); - --shadow-glow: 0 0 20px var(--color-primary-glow); - - /* Transitions */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); -} - -/* Reset & Base */ -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font-family: var(--font-sans); - background-color: var(--color-background); - color: var(--color-text-primary); - line-height: 1.6; - min-height: 100vh; -} - -#root { - min-height: 100vh; - background-image: var(--gradient-mesh); - background-attachment: fixed; -} - -/* Typography */ -h1, h2, h3, h4, h5, h6 { - font-weight: 600; - line-height: 1.3; - letter-spacing: -0.02em; -} - -h1 { font-size: 2rem; } -h2 { font-size: 1.5rem; } -h3 { font-size: 1.25rem; } -h4 { font-size: 1.125rem; } - -a { - color: var(--color-primary-light); - text-decoration: none; - transition: color var(--transition-fast); -} - -a:hover { - color: var(--color-primary); -} - -/* Utility Classes */ -.container { - max-width: 1400px; - margin: 0 auto; - padding: 0 var(--space-6); -} - -.flex { display: flex; } -.flex-col { flex-direction: column; } -.items-center { align-items: center; } -.justify-between { justify-content: space-between; } -.gap-2 { gap: var(--space-2); } -.gap-3 { gap: var(--space-3); } -.gap-4 { gap: var(--space-4); } -.gap-6 { gap: var(--space-6); } - -/* Glass Card */ -.card { - background: var(--color-surface-glass); - backdrop-filter: blur(12px); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--space-6); - transition: all var(--transition-normal); -} - -.card:hover { - border-color: var(--color-border-light); - box-shadow: var(--shadow-lg); -} - -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--space-4); -} - -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-5); - font-family: var(--font-sans); - font-size: 0.875rem; - font-weight: 500; - border: none; - border-radius: var(--radius-lg); - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-primary { - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-sm), 0 0 20px var(--color-primary-glow); -} - -.btn-primary:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: var(--shadow-md), 0 0 30px var(--color-primary-glow); -} - -.btn-success { - background: var(--gradient-success); - color: white; -} - -.btn-success:hover:not(:disabled) { - transform: translateY(-1px); - box-shadow: var(--shadow-md), 0 0 20px var(--color-success-glow); -} - -.btn-danger { - background: var(--color-danger); - color: white; -} - -.btn-danger:hover:not(:disabled) { - background: var(--color-danger-light); -} - -.btn-ghost { - background: transparent; - color: var(--color-text-secondary); - border: 1px solid var(--color-border); -} - -.btn-ghost:hover:not(:disabled) { - background: var(--color-surface); - color: var(--color-text-primary); - border-color: var(--color-border-light); -} - -.btn-icon { - padding: var(--space-2); - border-radius: var(--radius-md); -} - -/* Status Badges */ -.badge { - display: inline-flex; - align-items: center; - gap: var(--space-1); - padding: var(--space-1) var(--space-3); - font-size: 0.75rem; - font-weight: 500; - border-radius: var(--radius-full); - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.badge-discovered { - background: rgba(59, 130, 246, 0.15); - color: var(--color-info-light); - border: 1px solid rgba(59, 130, 246, 0.3); -} - -.badge-processing { - background: rgba(245, 158, 11, 0.15); - color: var(--color-warning-light); - border: 1px solid rgba(245, 158, 11, 0.3); -} - -.badge-ready { - background: rgba(16, 185, 129, 0.15); - color: var(--color-success-light); - border: 1px solid rgba(16, 185, 129, 0.3); -} - -.badge-applied { - background: rgba(99, 102, 241, 0.15); - color: var(--color-primary-light); - border: 1px solid rgba(99, 102, 241, 0.3); -} - -.badge-rejected { - background: rgba(239, 68, 68, 0.15); - color: var(--color-danger-light); - border: 1px solid rgba(239, 68, 68, 0.3); -} - -.badge-expired { - background: rgba(113, 113, 122, 0.15); - color: var(--color-text-muted); - border: 1px solid rgba(113, 113, 122, 0.3); -} - -/* Score indicator */ -.score { - display: flex; - align-items: center; - gap: var(--space-2); - font-weight: 600; - font-size: 0.875rem; -} - -.score-bar { - width: 60px; - height: 6px; - background: var(--color-surface); - border-radius: var(--radius-full); - overflow: hidden; -} - -.score-bar-fill { - height: 100%; - border-radius: var(--radius-full); - transition: width var(--transition-slow); -} - -.score-high .score-bar-fill { background: var(--color-success); } -.score-medium .score-bar-fill { background: var(--color-warning); } -.score-low .score-bar-fill { background: var(--color-danger); } - -/* Stats Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: var(--space-4); -} - -.stat-card { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: var(--space-4); - text-align: center; -} - -.stat-value { - font-size: 2rem; - font-weight: 700; - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.stat-label { - font-size: 0.75rem; - color: var(--color-text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-top: var(--space-1); -} - -/* Job List */ -.job-list { - display: flex; - flex-direction: column; - gap: var(--space-4); -} - -.job-card { - background: var(--color-surface-glass); - backdrop-filter: blur(12px); - border: 1px solid var(--color-border); - border-radius: var(--radius-xl); - padding: var(--space-5); - transition: all var(--transition-normal); -} - -.job-card:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-glow); - transform: translateY(-2px); -} - -.job-card-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-4); - margin-bottom: var(--space-3); -} - -.job-title { - font-size: 1.125rem; - font-weight: 600; - color: var(--color-text-primary); - margin-bottom: var(--space-1); -} - -.job-employer { - font-size: 0.875rem; - color: var(--color-text-secondary); -} - -.job-meta { - display: flex; - flex-wrap: wrap; - gap: var(--space-4); - margin-top: var(--space-3); - padding-top: var(--space-3); - border-top: 1px solid var(--color-border); -} - -.job-meta-item { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.8125rem; - color: var(--color-text-muted); -} - -.job-meta-item svg { - width: 14px; - height: 14px; - opacity: 0.7; -} - -.job-actions { - display: flex; - gap: var(--space-2); - margin-top: var(--space-4); -} - -/* Tabs */ -.tabs { - display: flex; - gap: var(--space-1); - background: var(--color-surface); - padding: var(--space-1); - border-radius: var(--radius-lg); - margin-bottom: var(--space-6); -} - -.tab { - flex: 1; - padding: var(--space-3) var(--space-4); - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text-muted); - background: transparent; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: all var(--transition-fast); -} - -.tab:hover { - color: var(--color-text-secondary); -} - -.tab.active { - background: var(--gradient-primary); - color: white; -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: var(--space-12); - color: var(--color-text-muted); -} - -.empty-state-icon { - font-size: 3rem; - margin-bottom: var(--space-4); - opacity: 0.5; -} - -.empty-state-title { - font-size: 1.125rem; - font-weight: 600; - color: var(--color-text-secondary); - margin-bottom: var(--space-2); -} - -/* Loading Spinner */ -.spinner { - width: 20px; - height: 20px; - border: 2px solid var(--color-border); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Pulse animation for processing jobs */ -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -.pulse { - animation: pulse 2s ease-in-out infinite; -} - -/* Header */ -.header { - padding: var(--space-6) 0; - border-bottom: 1px solid var(--color-border); - margin-bottom: var(--space-8); -} - -.header-content { - display: flex; - align-items: center; - justify-content: space-between; -} - -.logo { - display: flex; - align-items: center; - gap: var(--space-3); -} - -.logo-icon { - width: 40px; - height: 40px; - border-radius: var(--radius-lg); - background: var(--gradient-primary); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.25rem; -} - -.logo-text { - font-size: 1.25rem; - font-weight: 700; -} - -/* Modal */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; - opacity: 0; - animation: fadeIn var(--transition-fast) forwards; -} - -@keyframes fadeIn { - to { opacity: 1; } -} - -.modal { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-2xl); - max-width: 600px; - width: 90%; - max-height: 80vh; - overflow-y: auto; - animation: slideUp var(--transition-normal) forwards; -} - -@keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-5) var(--space-6); - border-bottom: 1px solid var(--color-border); -} - -.modal-title { - font-size: 1.125rem; - font-weight: 600; -} - -.modal-body { - padding: var(--space-6); -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-3); - padding: var(--space-4) var(--space-6); - border-top: 1px solid var(--color-border); - background: var(--color-surface-elevated); - border-radius: 0 0 var(--radius-2xl) var(--radius-2xl); -} - -/* Toast */ -.toast-container { - position: fixed; - bottom: var(--space-6); - right: var(--space-6); - z-index: 200; - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.toast { - background: var(--color-surface-elevated); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - padding: var(--space-4) var(--space-5); - box-shadow: var(--shadow-lg); - min-width: 280px; - animation: slideIn var(--transition-normal) forwards; -} - -@keyframes slideIn { - from { - opacity: 0; - transform: translateX(100%); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.toast-success { border-left: 3px solid var(--color-success); } -.toast-error { border-left: 3px solid var(--color-danger); } -.toast-info { border-left: 3px solid var(--color-info); } - -/* Scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--color-surface); -} - -::-webkit-scrollbar-thumb { - background: var(--color-border-light); - border-radius: var(--radius-full); -} - -::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); -} - -/* Responsive */ -@media (max-width: 768px) { - .container { - padding: 0 var(--space-4); - } - - h1 { font-size: 1.5rem; } - - .header-content { - flex-direction: column; - gap: var(--space-4); - align-items: flex-start; - } - - .stats-grid { - grid-template-columns: repeat(3, 1fr); - } - - .job-card-header { - flex-direction: column; - } - - .job-actions { - flex-direction: column; - } - - .job-actions .btn { - width: 100%; - } -} diff --git a/orchestrator/src/components/ui/alert-dialog.tsx b/orchestrator/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/orchestrator/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/orchestrator/src/components/ui/badge.tsx b/orchestrator/src/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/orchestrator/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/orchestrator/src/components/ui/button.tsx b/orchestrator/src/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/orchestrator/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/orchestrator/src/components/ui/card.tsx b/orchestrator/src/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/orchestrator/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/orchestrator/src/components/ui/progress.tsx b/orchestrator/src/components/ui/progress.tsx new file mode 100644 index 0000000..4fc3b47 --- /dev/null +++ b/orchestrator/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/orchestrator/src/components/ui/separator.tsx b/orchestrator/src/components/ui/separator.tsx new file mode 100644 index 0000000..6d7f122 --- /dev/null +++ b/orchestrator/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/orchestrator/src/components/ui/sonner.tsx b/orchestrator/src/components/ui/sonner.tsx new file mode 100644 index 0000000..41c699c --- /dev/null +++ b/orchestrator/src/components/ui/sonner.tsx @@ -0,0 +1,26 @@ +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + + ) +} + +export { Toaster } diff --git a/orchestrator/src/components/ui/tabs.tsx b/orchestrator/src/components/ui/tabs.tsx new file mode 100644 index 0000000..85d83be --- /dev/null +++ b/orchestrator/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/orchestrator/src/index.css b/orchestrator/src/index.css new file mode 100644 index 0000000..c864fe0 --- /dev/null +++ b/orchestrator/src/index.css @@ -0,0 +1,130 @@ +@import "tailwindcss"; + +@import "tw-animate-css"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +@tailwind utilities; + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); + --radius-3xl: calc(var(--radius) + 12px); + --radius-4xl: calc(var(--radius) + 16px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", + Roboto, Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; + @apply bg-background text-foreground antialiased; + } +} diff --git a/orchestrator/src/lib/utils.ts b/orchestrator/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/orchestrator/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/orchestrator/tailwind.config.ts b/orchestrator/tailwind.config.ts new file mode 100644 index 0000000..a2766a1 --- /dev/null +++ b/orchestrator/tailwind.config.ts @@ -0,0 +1,11 @@ +import type { Config } from "tailwindcss"; + +export default { + darkMode: "class", + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config; +