From 16c1ed38cd14cdd3beafc4df045c00aff9679478 Mon Sep 17 00:00:00 2001 From: Harsh63870 Date: Wed, 10 Jun 2026 16:27:49 +0000 Subject: [PATCH] frontend typescript --- frontend/index.html | 6 +- frontend/package-lock.json | 1534 ++++++++++++++++- frontend/package.json | 21 +- frontend/public/favicon.svg | 12 +- frontend/src/App.css | 184 -- frontend/src/App.jsx | 189 -- frontend/src/App.tsx | 55 + frontend/src/components/layout/AppShell.tsx | 27 + .../src/components/layout/CommandPalette.tsx | 130 ++ frontend/src/components/layout/RightPanel.tsx | 153 ++ frontend/src/components/layout/Sidebar.tsx | 90 + frontend/src/components/layout/Topbar.tsx | 45 + frontend/src/components/shared/CopyButton.tsx | 44 + frontend/src/components/shared/DiffViewer.tsx | 35 + frontend/src/components/shared/EmptyState.tsx | 30 + .../src/components/shared/OutputBlock.tsx | 44 + .../src/components/shared/SectionHeader.tsx | 21 + frontend/src/components/shared/StatCard.tsx | 44 + frontend/src/components/three/CommitGraph.tsx | 113 ++ frontend/src/components/three/Particles.tsx | 53 + .../src/components/three/SceneBackground.tsx | 56 + frontend/src/components/ui/badge.tsx | 26 + frontend/src/components/ui/button.tsx | 62 + frontend/src/components/ui/card.tsx | 33 + frontend/src/components/ui/kbd.tsx | 16 + frontend/src/components/ui/skeleton.tsx | 15 + frontend/src/components/ui/switch.tsx | 37 + frontend/src/hooks/useAnimatedCounter.ts | 30 + frontend/src/hooks/useCopy.ts | 28 + frontend/src/hooks/useGit.ts | 80 + frontend/src/index.css | 278 +-- frontend/src/lib/api.ts | 30 + frontend/src/lib/diff.ts | 68 + frontend/src/lib/motion.ts | 62 + frontend/src/lib/utils.ts | 17 + frontend/src/main.jsx | 10 - frontend/src/main.tsx | 22 + frontend/src/store/useAppStore.ts | 70 + frontend/src/views/CommitView.tsx | 113 ++ frontend/src/views/DashboardView.tsx | 149 ++ frontend/src/views/GitAnalysisView.tsx | 143 ++ frontend/src/views/PRView.tsx | 84 + frontend/src/views/SettingsView.tsx | 91 + frontend/src/vite-env.d.ts | 1 + frontend/tsconfig.json | 24 + frontend/vite.config.js | 18 +- 46 files changed, 3888 insertions(+), 505 deletions(-) delete mode 100644 frontend/src/App.css delete mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/layout/AppShell.tsx create mode 100644 frontend/src/components/layout/CommandPalette.tsx create mode 100644 frontend/src/components/layout/RightPanel.tsx create mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/layout/Topbar.tsx create mode 100644 frontend/src/components/shared/CopyButton.tsx create mode 100644 frontend/src/components/shared/DiffViewer.tsx create mode 100644 frontend/src/components/shared/EmptyState.tsx create mode 100644 frontend/src/components/shared/OutputBlock.tsx create mode 100644 frontend/src/components/shared/SectionHeader.tsx create mode 100644 frontend/src/components/shared/StatCard.tsx create mode 100644 frontend/src/components/three/CommitGraph.tsx create mode 100644 frontend/src/components/three/Particles.tsx create mode 100644 frontend/src/components/three/SceneBackground.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/button.tsx create mode 100644 frontend/src/components/ui/card.tsx create mode 100644 frontend/src/components/ui/kbd.tsx create mode 100644 frontend/src/components/ui/skeleton.tsx create mode 100644 frontend/src/components/ui/switch.tsx create mode 100644 frontend/src/hooks/useAnimatedCounter.ts create mode 100644 frontend/src/hooks/useCopy.ts create mode 100644 frontend/src/hooks/useGit.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/diff.ts create mode 100644 frontend/src/lib/motion.ts create mode 100644 frontend/src/lib/utils.ts delete mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/store/useAppStore.ts create mode 100644 frontend/src/views/CommitView.tsx create mode 100644 frontend/src/views/DashboardView.tsx create mode 100644 frontend/src/views/GitAnalysisView.tsx create mode 100644 frontend/src/views/PRView.tsx create mode 100644 frontend/src/views/SettingsView.tsx create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json diff --git a/frontend/index.html b/frontend/index.html index f94d687..c226044 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,10 +4,12 @@ - frontend + + + DevLog — AI Git Copilot
- + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 49bae76..bfc76bd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,18 +8,37 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "@tanstack/react-query": "^5.101.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.40.0", + "lucide-react": "^1.17.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "three": "^0.184.0", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^25.9.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", "vite": "^8.0.12" } }, @@ -202,6 +221,14 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -247,6 +274,11 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==" + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -397,6 +429,22 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz", + "integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -503,6 +551,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -530,6 +594,405 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "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.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.16.tgz", + "integrity": "sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.12", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.9", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.11", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.5", + "@radix-ui/react-slot": "1.2.5", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.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-dismissable-layer": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.12.tgz", + "integrity": "sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.5", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.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-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "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.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.9.tgz", + "integrity": "sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.5", + "@radix-ui/react-use-callback-ref": "1.1.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-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.11.tgz", + "integrity": "sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.5", + "@radix-ui/react-use-layout-effect": "1.1.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-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.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-primitive": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.5.tgz", + "integrity": "sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==", + "dependencies": { + "@radix-ui/react-slot": "1.2.5" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.5.tgz", + "integrity": "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-three/drei": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", + "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz", + "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.27.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": ">=19 <19.3", + "react-dom": ">=19 <19.3", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", @@ -778,6 +1241,292 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "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.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -788,6 +1537,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -806,11 +1560,24 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "dev": true, + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==" + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", - "dev": true, "dependencies": { "csstype": "^3.2.2" } @@ -819,11 +1586,58 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==" + }, + "node_modules/@types/three": { + "version": "0.184.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz", + "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": ">=0.5.17", + "fflate": "~0.8.2", + "meshoptimizer": "~1.1.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==" + }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", @@ -886,6 +1700,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "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/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -895,6 +1720,25 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/baseline-browser-mapping": { "version": "2.10.32", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", @@ -907,6 +1751,14 @@ "node": ">=6.0.0" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -952,6 +1804,41 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/camera-controls": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", + "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "engines": { + "node": ">=22.0.0", + "npm": ">=10.5.1" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -972,17 +1859,67 @@ } ] }, + "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/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/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -995,8 +1932,7 @@ "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/debug": { "version": "4.4.3", @@ -1021,6 +1957,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1030,12 +1974,35 @@ "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/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + }, "node_modules/electron-to-chromium": { "version": "1.5.362", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", "integrity": "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==", "dev": true }, + "node_modules/enhanced-resolve": { + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.23.0.tgz", + "integrity": "sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1264,6 +2231,11 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -1311,6 +2283,32 @@ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, + "node_modules/framer-motion": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.40.0.tgz", + "integrity": "sha512-uaBd3qC1v3KQqBEjwTUd183K6PbS+j0yR9w9VmEOLWA/tnUcSn8Xa3uck7t4dgpDoUss8xQTcj8W2L07lrnLFg==", + "dependencies": { + "motion-dom": "^12.40.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1334,6 +2332,14 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1358,6 +2364,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==" + }, + "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/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1373,6 +2390,30 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", + "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1382,6 +2423,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1412,11 +2458,35 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -1488,6 +2558,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -1761,6 +2839,45 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", + "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.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/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==" + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -1776,6 +2893,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/motion-dom": { + "version": "12.40.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.40.0.tgz", + "integrity": "sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1875,7 +3005,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -1926,6 +3055,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1935,6 +3069,15 @@ "node": ">= 0.8.0" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1963,6 +3106,94 @@ "react": "^19.2.6" } }, + "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-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/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rolldown": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", @@ -2014,7 +3245,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2026,11 +3256,19 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } }, + "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-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2040,6 +3278,99 @@ "node": ">=0.10.0" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==" + }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.1", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", + "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==" + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -2056,12 +3387,72 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "optional": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } }, "node_modules/type-check": { "version": "0.4.0", @@ -2075,6 +3466,25 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2114,6 +3524,63 @@ "punycode": "^2.1.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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "engines": { + "node": ">= 4" + } + }, "node_modules/vite": { "version": "8.0.14", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", @@ -2191,11 +3658,20 @@ } } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2253,6 +3729,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/frontend/package.json b/frontend/package.json index e4d03ce..dbb1bd6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,18 +10,37 @@ "preview": "vite preview" }, "dependencies": { + "@fontsource-variable/inter": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.6.1", + "@tanstack/react-query": "^5.101.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.40.0", + "lucide-react": "^1.17.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "three": "^0.184.0", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "^4.3.0", + "@types/node": "^25.9.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "tailwindcss": "^4.3.0", + "typescript": "^6.0.3", "vite": "^8.0.12" } } diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..c96913c 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1,11 @@ - \ No newline at end of file + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx deleted file mode 100644 index ed77fe1..0000000 --- a/frontend/src/App.jsx +++ /dev/null @@ -1,189 +0,0 @@ -import { useState } from "react"; - -function App() { - const [diff, setDiff] = useState(""); - const [commitMsg, setCommitMsg] = useState(""); - const [prDesc, setPrDesc] = useState(""); - const [loading, setLoading] = useState(false); - - const copyText = async (text) => { - await navigator.clipboard.writeText(text); - alert("Copied!"); - }; - - const fetchDiff = async () => { - setLoading(true); - - try { - const res = await fetch("http://localhost:8000/git-diff"); - const data = await res.json(); - setDiff(data.diff); - } catch (error) { - console.error(error); - } - - setLoading(false); - }; - - const generateCommit = async () => { - setLoading(true); - - try { - const res = await fetch("http://localhost:8000/generate-commit"); - const data = await res.json(); - setCommitMsg(data.message); - } catch (error) { - console.error(error); - } - - setLoading(false); - }; - - const generatePR = async () => { - setLoading(true); - - try { - const res = await fetch("http://localhost:8000/generate-pr"); - const data = await res.json(); - setPrDesc(data.description); - } catch (error) { - console.error(error); - } - - setLoading(false); - }; - - const cardStyle = { - border: "1px solid #121212", - borderRadius: "12px", - padding: "20px", - marginBottom: "20px", - backgroundColor: "#161b22", -color: "white", -minHeight: "120px", - }; - - const buttonStyle = { - padding: "10px 16px", - marginRight: "10px", - marginBottom: "10px", - cursor: "pointer", - borderRadius: "12px", - border: "none", - backgroundColor: "#140a0a", - color: "white", - }; - - return ( -
-

DevLog

- -

- AI-style Git Commit + PR Generator -

- -
- - - - - -
- - {loading && ( -

- Processing... -

- )} - -
-

Git Diff

- -
-          {diff}
-        
-
- -
-
-

Commit Message

- - -
- -
-          {commitMsg}
-        
-
- -
-
-

PR Description

- - -
- -
-          {prDesc}
-        
-
-
- ); -} - -export default App; \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..1f19c41 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,55 @@ +import { AnimatePresence, MotionConfig, motion } from "framer-motion"; +import { Toaster } from "sonner"; +import { AppShell } from "@/components/layout/AppShell"; +import { DashboardView } from "@/views/DashboardView"; +import { GitAnalysisView } from "@/views/GitAnalysisView"; +import { CommitView } from "@/views/CommitView"; +import { PRView } from "@/views/PRView"; +import { SettingsView } from "@/views/SettingsView"; +import { useAppStore, type ViewId } from "@/store/useAppStore"; +import { pageVariants } from "@/lib/motion"; + +const VIEWS: Record React.JSX.Element> = { + dashboard: DashboardView, + analysis: GitAnalysisView, + commit: CommitView, + pr: PRView, + settings: SettingsView, +}; + +export default function App() { + const view = useAppStore((s) => s.view); + const reducedMotion = useAppStore((s) => s.reducedMotion); + const ActiveView = VIEWS[view]; + + return ( + + + + + + + + + + + ); +} diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..c0fb569 --- /dev/null +++ b/frontend/src/components/layout/AppShell.tsx @@ -0,0 +1,27 @@ +import { lazy, Suspense, type ReactNode } from "react"; +import { Sidebar } from "./Sidebar"; +import { Topbar } from "./Topbar"; +import { RightPanel } from "./RightPanel"; +import { CommandPalette } from "./CommandPalette"; + +// Code-split: three.js + R3F load in their own chunk after first paint. +const SceneBackground = lazy(() => + import("@/components/three/SceneBackground").then((m) => ({ default: m.SceneBackground })), +); + +export function AppShell({ children }: { children: ReactNode }) { + return ( +
+ }> + + + +
+ +
{children}
+
+ + +
+ ); +} diff --git a/frontend/src/components/layout/CommandPalette.tsx b/frontend/src/components/layout/CommandPalette.tsx new file mode 100644 index 0000000..e35e99f --- /dev/null +++ b/frontend/src/components/layout/CommandPalette.tsx @@ -0,0 +1,130 @@ +import { useEffect } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Command } from "cmdk"; +import { + GitBranch, + GitCommitHorizontal, + GitPullRequest, + LayoutDashboard, + ScanLine, + Settings, + Sparkles, +} from "lucide-react"; +import { useAppStore, type ViewId } from "@/store/useAppStore"; +import { useScanDiff, useGenerateCommit, useGeneratePR } from "@/hooks/useGit"; +import { Kbd } from "@/components/ui/kbd"; + +export function CommandPalette() { + const open = useAppStore((s) => s.commandOpen); + const setOpen = useAppStore((s) => s.setCommandOpen); + const setView = useAppStore((s) => s.setView); + + const scan = useScanDiff(); + const commit = useGenerateCommit(); + const pr = useGeneratePR(); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen(!open); + } + if (!open && (e.metaKey || e.ctrlKey) && /^[1-5]$/.test(e.key)) { + const views: ViewId[] = ["dashboard", "analysis", "commit", "pr", "settings"]; + e.preventDefault(); + setView(views[Number(e.key) - 1]); + } + if (e.key === "Escape") setOpen(false); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [open, setOpen, setView]); + + const run = (fn: () => void) => { + setOpen(false); + fn(); + }; + + const navigate = (view: ViewId) => run(() => setView(view)); + + return ( + + {open && ( + setOpen(false)} + > + e.stopPropagation()} + > + +
+ + + ESC +
+ + + No results found. + + + + run(() => { setView("analysis"); scan.mutate(); })} /> + run(() => { setView("commit"); commit.mutate(); })} /> + run(() => { setView("pr"); pr.mutate(); })} /> + + + + navigate("dashboard")} /> + navigate("analysis")} /> + navigate("commit")} /> + navigate("pr")} /> + navigate("settings")} /> + + +
+
+
+ )} +
+ ); +} + +function PaletteItem({ + icon: Icon, + label, + shortcut, + onSelect, +}: { + icon: typeof Sparkles; + label: string; + shortcut?: string; + onSelect: () => void; +}) { + return ( + + + {label} + {shortcut && {shortcut}} + + ); +} diff --git a/frontend/src/components/layout/RightPanel.tsx b/frontend/src/components/layout/RightPanel.tsx new file mode 100644 index 0000000..b1e824a --- /dev/null +++ b/frontend/src/components/layout/RightPanel.tsx @@ -0,0 +1,153 @@ +import { AnimatePresence, motion } from "framer-motion"; +import { + Activity, + Copy, + GitBranch, + GitCommitHorizontal, + GitPullRequest, + ScanLine, + Sparkles, + Zap, +} from "lucide-react"; +import { useAppStore, type ActivityKind } from "@/store/useAppStore"; +import { useScanDiff, useGenerateCommit, useGeneratePR } from "@/hooks/useGit"; +import { useCopy } from "@/hooks/useCopy"; +import { Badge } from "@/components/ui/badge"; +import { timeAgo, cn } from "@/lib/utils"; +import { slideInRight } from "@/lib/motion"; + +const KIND_META: Record = { + scan: { icon: ScanLine, className: "text-accent-cyan bg-accent-cyan/10 border-accent-cyan/20" }, + commit: { icon: GitCommitHorizontal, className: "text-brand-300 bg-brand-500/10 border-brand-500/20" }, + pr: { icon: GitPullRequest, className: "text-accent-emerald bg-accent-emerald/10 border-accent-emerald/20" }, + copy: { icon: Copy, className: "text-accent-amber bg-accent-amber/10 border-accent-amber/20" }, + system: { icon: Zap, className: "text-text-tertiary bg-surface-2 border-edge" }, +}; + +function QuickAction({ + icon: Icon, + label, + onClick, + loading, +}: { + icon: typeof Activity; + label: string; + onClick: () => void; + loading?: boolean; +}) { + return ( + + ); +} + +export function RightPanel() { + const activity = useAppStore((s) => s.activity); + const outputs = useAppStore((s) => s.outputs); + const setView = useAppStore((s) => s.setView); + + const scan = useScanDiff(); + const commit = useGenerateCommit(); + const pr = useGeneratePR(); + const { copy } = useCopy("Output copied"); + + return ( + + ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..7a34499 --- /dev/null +++ b/frontend/src/components/layout/Sidebar.tsx @@ -0,0 +1,90 @@ +import { motion } from "framer-motion"; +import { + GitBranch, + GitCommitHorizontal, + GitPullRequest, + LayoutDashboard, + Settings, + Terminal, +} from "lucide-react"; +import { useAppStore, type ViewId } from "@/store/useAppStore"; +import { Kbd } from "@/components/ui/kbd"; +import { cn } from "@/lib/utils"; + +const NAV_ITEMS: { id: ViewId; label: string; icon: typeof LayoutDashboard; shortcut: string }[] = [ + { id: "dashboard", label: "Dashboard", icon: LayoutDashboard, shortcut: "1" }, + { id: "analysis", label: "Git Analysis", icon: GitBranch, shortcut: "2" }, + { id: "commit", label: "Commit Generator", icon: GitCommitHorizontal, shortcut: "3" }, + { id: "pr", label: "PR Generator", icon: GitPullRequest, shortcut: "4" }, + { id: "settings", label: "Settings", icon: Settings, shortcut: "5" }, +]; + +export function Sidebar() { + const view = useAppStore((s) => s.view); + const setView = useAppStore((s) => s.setView); + + return ( + + ); +} diff --git a/frontend/src/components/layout/Topbar.tsx b/frontend/src/components/layout/Topbar.tsx new file mode 100644 index 0000000..4cce0bf --- /dev/null +++ b/frontend/src/components/layout/Topbar.tsx @@ -0,0 +1,45 @@ +import { Search, Sparkles } from "lucide-react"; +import { useAppStore } from "@/store/useAppStore"; +import { Kbd } from "@/components/ui/kbd"; +import { Badge } from "@/components/ui/badge"; + +const VIEW_TITLES: Record = { + dashboard: "Dashboard", + analysis: "Git Analysis", + commit: "Commit Generator", + pr: "PR Generator", + settings: "Settings", +}; + +export function Topbar() { + const view = useAppStore((s) => s.view); + const setCommandOpen = useAppStore((s) => s.setCommandOpen); + + return ( +
+
+ DevLog + / + {VIEW_TITLES[view]} +
+ +
+ + + + AI Ready + +
+
+ ); +} diff --git a/frontend/src/components/shared/CopyButton.tsx b/frontend/src/components/shared/CopyButton.tsx new file mode 100644 index 0000000..92ba8eb --- /dev/null +++ b/frontend/src/components/shared/CopyButton.tsx @@ -0,0 +1,44 @@ +import { AnimatePresence, motion } from "framer-motion"; +import { Check, Copy } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useCopy } from "@/hooks/useCopy"; + +interface CopyButtonProps { + text: string; + label?: string; + size?: "sm" | "md"; +} + +export function CopyButton({ text, label = "Copied to clipboard", size = "sm" }: CopyButtonProps) { + const { copied, copy } = useCopy(label); + + return ( + + ); +} diff --git a/frontend/src/components/shared/DiffViewer.tsx b/frontend/src/components/shared/DiffViewer.tsx new file mode 100644 index 0000000..07063c1 --- /dev/null +++ b/frontend/src/components/shared/DiffViewer.tsx @@ -0,0 +1,35 @@ +import { memo } from "react"; +import { classifyDiffLines, type DiffLineKind } from "@/lib/diff"; +import { cn } from "@/lib/utils"; + +const lineStyles: Record = { + add: "bg-accent-emerald/8 text-accent-emerald border-l-2 border-accent-emerald/50", + del: "bg-accent-rose/8 text-accent-rose border-l-2 border-accent-rose/50", + hunk: "bg-accent-cyan/8 text-accent-cyan font-medium", + meta: "text-text-tertiary", + context: "text-text-secondary", +}; + +interface DiffViewerProps { + raw: string; + maxHeight?: string; +} + +export const DiffViewer = memo(function DiffViewer({ raw, maxHeight = "28rem" }: DiffViewerProps) { + const lines = classifyDiffLines(raw); + + return ( +
+
+ {lines.map((line, i) => ( +
+ {line.text || " "} +
+ ))} +
+
+ ); +}); diff --git a/frontend/src/components/shared/EmptyState.tsx b/frontend/src/components/shared/EmptyState.tsx new file mode 100644 index 0000000..f55fc0b --- /dev/null +++ b/frontend/src/components/shared/EmptyState.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from "react"; +import { motion } from "framer-motion"; +import type { LucideIcon } from "lucide-react"; + +interface EmptyStateProps { + icon: LucideIcon; + title: string; + description: string; + action?: ReactNode; +} + +export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) { + return ( + +
+
+
+ +
+
+
{title}
+

{description}

+ {action &&
{action}
} + + ); +} diff --git a/frontend/src/components/shared/OutputBlock.tsx b/frontend/src/components/shared/OutputBlock.tsx new file mode 100644 index 0000000..a0b417f --- /dev/null +++ b/frontend/src/components/shared/OutputBlock.tsx @@ -0,0 +1,44 @@ +import { motion } from "framer-motion"; +import { Sparkles } from "lucide-react"; +import { CopyButton } from "@/components/shared/CopyButton"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; + +interface OutputBlockProps { + content: string; + label: string; + timeTakenSec?: number; + mono?: boolean; +} + +/** Displays a generated AI output with copy action + meta. */ +export function OutputBlock({ content, label, timeTakenSec, mono = true }: OutputBlockProps) { + return ( + +
+
+ + {label} + {timeTakenSec !== undefined && ( + + {timeTakenSec}s + + )} +
+ +
+
+        {content}
+      
+
+ ); +} diff --git a/frontend/src/components/shared/SectionHeader.tsx b/frontend/src/components/shared/SectionHeader.tsx new file mode 100644 index 0000000..5c6ccd1 --- /dev/null +++ b/frontend/src/components/shared/SectionHeader.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from "react"; +import { motion } from "framer-motion"; +import { fadeUp } from "@/lib/motion"; + +interface SectionHeaderProps { + title: string; + description?: string; + actions?: ReactNode; +} + +export function SectionHeader({ title, description, actions }: SectionHeaderProps) { + return ( + +
+

{title}

+ {description &&

{description}

} +
+ {actions &&
{actions}
} +
+ ); +} diff --git a/frontend/src/components/shared/StatCard.tsx b/frontend/src/components/shared/StatCard.tsx new file mode 100644 index 0000000..a11bcc4 --- /dev/null +++ b/frontend/src/components/shared/StatCard.tsx @@ -0,0 +1,44 @@ +import { motion } from "framer-motion"; +import type { LucideIcon } from "lucide-react"; +import { Card } from "@/components/ui/card"; +import { useAnimatedCounter } from "@/hooks/useAnimatedCounter"; +import { fadeUp, hoverLift } from "@/lib/motion"; +import { cn } from "@/lib/utils"; + +interface StatCardProps { + icon: LucideIcon; + label: string; + value: number; + tone?: "brand" | "cyan" | "emerald" | "rose"; + suffix?: string; +} + +const toneStyles = { + brand: "text-brand-300 bg-brand-500/10 border-brand-500/20", + cyan: "text-accent-cyan bg-accent-cyan/10 border-accent-cyan/20", + emerald: "text-accent-emerald bg-accent-emerald/10 border-accent-emerald/20", + rose: "text-accent-rose bg-accent-rose/10 border-accent-rose/20", +}; + +export function StatCard({ icon: Icon, label, value, tone = "brand", suffix }: StatCardProps) { + const animated = useAnimatedCounter(value); + + return ( + + +
+
+ +
+
+
+ {animated.toLocaleString()} + {suffix && {suffix}} +
+
{label}
+
+
+
+
+ ); +} diff --git a/frontend/src/components/three/CommitGraph.tsx b/frontend/src/components/three/CommitGraph.tsx new file mode 100644 index 0000000..64aeb54 --- /dev/null +++ b/frontend/src/components/three/CommitGraph.tsx @@ -0,0 +1,113 @@ +import { useEffect, useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; + +interface GraphNode { + position: THREE.Vector3; + scale: number; + branch: number; +} + +const BRANCH_COLORS = ["#8b5cf6", "#22d3ee", "#34d399"]; + +/** + * Procedural "git commit graph": three branch lanes of nodes, + * connected by lines, with merge links between lanes. + * Nodes are instanced (1 draw call) + lines are a single LineSegments. + */ +export function CommitGraph() { + const instancedRef = useRef(null); + const groupRef = useRef(null); + + const { nodes, linePositions, lineColors } = useMemo(() => { + const nodes: GraphNode[] = []; + const lanes = [-2.2, 0, 2.2]; + const perLane = 9; + + for (let lane = 0; lane < lanes.length; lane++) { + for (let i = 0; i < perLane; i++) { + const x = -8 + i * 2 + (lane === 1 ? 1 : 0); + const y = lanes[lane] + Math.sin(i * 1.7 + lane * 2) * 0.45; + const z = -1 + Math.sin(i * 0.9 + lane) * 0.8; + nodes.push({ + position: new THREE.Vector3(x, y, z), + scale: 0.06 + Math.random() * 0.05, + branch: lane, + }); + } + } + + const segments: [THREE.Vector3, THREE.Vector3, number][] = []; + // connect consecutive commits within each lane + for (let lane = 0; lane < lanes.length; lane++) { + const laneNodes = nodes.filter((n) => n.branch === lane); + for (let i = 0; i < laneNodes.length - 1; i++) { + segments.push([laneNodes[i].position, laneNodes[i + 1].position, lane]); + } + } + // merge/branch links between lanes + const merges: [number, number][] = [ + [2, 12], [6, 14], [11, 22], [16, 24], [4, 20], [8, 26], + ]; + for (const [a, b] of merges) { + if (nodes[a] && nodes[b]) segments.push([nodes[a].position, nodes[b].position, nodes[b].branch]); + } + + const linePositions = new Float32Array(segments.length * 6); + const lineColors = new Float32Array(segments.length * 6); + segments.forEach(([from, to, branch], i) => { + linePositions.set([from.x, from.y, from.z, to.x, to.y, to.z], i * 6); + const c = new THREE.Color(BRANCH_COLORS[branch]); + lineColors.set([c.r, c.g, c.b, c.r, c.g, c.b], i * 6); + }); + + return { nodes, linePositions, lineColors }; + }, []); + + // place instances once after mount + useEffect(() => { + const mesh = instancedRef.current; + if (!mesh) return; + const dummy = new THREE.Object3D(); + const color = new THREE.Color(); + nodes.forEach((node, i) => { + dummy.position.copy(node.position); + dummy.scale.setScalar(node.scale); + dummy.updateMatrix(); + mesh.setMatrixAt(i, dummy.matrix); + mesh.setColorAt(i, color.set(BRANCH_COLORS[node.branch])); + }); + mesh.instanceMatrix.needsUpdate = true; + if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true; + }, [nodes]); + + useFrame((state) => { + const group = groupRef.current; + if (!group) return; + const t = state.clock.elapsedTime; + group.rotation.z = Math.sin(t * 0.08) * 0.04; + group.position.y = Math.sin(t * 0.15) * 0.25; + }); + + return ( + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/components/three/Particles.tsx b/frontend/src/components/three/Particles.tsx new file mode 100644 index 0000000..b25ebf5 --- /dev/null +++ b/frontend/src/components/three/Particles.tsx @@ -0,0 +1,53 @@ +import { useMemo, useRef } from "react"; +import { useFrame } from "@react-three/fiber"; +import * as THREE from "three"; + +const COUNT = 350; + +/** Single-draw-call floating particle field. */ +export function Particles() { + const pointsRef = useRef(null); + + const { positions, speeds } = useMemo(() => { + const positions = new Float32Array(COUNT * 3); + const speeds = new Float32Array(COUNT); + for (let i = 0; i < COUNT; i++) { + positions[i * 3] = (Math.random() - 0.5) * 24; + positions[i * 3 + 1] = (Math.random() - 0.5) * 14; + positions[i * 3 + 2] = (Math.random() - 0.5) * 10 - 2; + speeds[i] = 0.2 + Math.random() * 0.6; + } + return { positions, speeds }; + }, []); + + useFrame((state) => { + const points = pointsRef.current; + if (!points) return; + const t = state.clock.elapsedTime; + const arr = points.geometry.attributes.position.array as Float32Array; + for (let i = 0; i < COUNT; i++) { + // gentle vertical drift, wraps around + arr[i * 3 + 1] += speeds[i] * 0.003; + if (arr[i * 3 + 1] > 7) arr[i * 3 + 1] = -7; + arr[i * 3] += Math.sin(t * 0.3 + i) * 0.0008; + } + points.geometry.attributes.position.needsUpdate = true; + }); + + return ( + + + + + + + ); +} diff --git a/frontend/src/components/three/SceneBackground.tsx b/frontend/src/components/three/SceneBackground.tsx new file mode 100644 index 0000000..8bfdc1b --- /dev/null +++ b/frontend/src/components/three/SceneBackground.tsx @@ -0,0 +1,56 @@ +import { Suspense } from "react"; +import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import { Particles } from "./Particles"; +import { CommitGraph } from "./CommitGraph"; +import { useAppStore } from "@/store/useAppStore"; + +/** Smoothly moves the camera toward the pointer — parallax. */ +function ParallaxRig() { + const { camera, pointer } = useThree(); + + useFrame((_, delta) => { + camera.position.x = THREE.MathUtils.damp(camera.position.x, pointer.x * 0.6, 2, delta); + camera.position.y = THREE.MathUtils.damp(camera.position.y, pointer.y * 0.35, 2, delta); + camera.lookAt(0, 0, -2); + }); + + return null; +} + +/** + * Fixed, GPU-efficient 3D backdrop: + * capped DPR, no shadows, additive blending, ~3 draw calls total. + */ +export function SceneBackground() { + const show3D = useAppStore((s) => s.show3D); + + if (!show3D) { + return ( +
+ ); + } + + return ( +
+ {/* Gradient wash under the canvas */} +
+
+ + + + + + + + + {/* Vignette to keep content readable */} +
+
+ ); +} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..bf58cd3 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,26 @@ +import type { HTMLAttributes } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium tracking-wide border", + { + variants: { + tone: { + brand: "bg-brand-500/12 text-brand-300 border-brand-500/25", + cyan: "bg-accent-cyan/10 text-accent-cyan border-accent-cyan/25", + emerald: "bg-accent-emerald/10 text-accent-emerald border-accent-emerald/25", + amber: "bg-accent-amber/10 text-accent-amber border-accent-amber/25", + rose: "bg-accent-rose/10 text-accent-rose border-accent-rose/25", + neutral: "bg-surface-2/80 text-text-secondary border-edge", + }, + }, + defaultVariants: { tone: "neutral" }, + }, +); + +export interface BadgeProps extends HTMLAttributes, VariantProps {} + +export function Badge({ className, tone, ...props }: BadgeProps) { + return ; +} diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..d141702 --- /dev/null +++ b/frontend/src/components/ui/button.tsx @@ -0,0 +1,62 @@ +import { forwardRef, type ButtonHTMLAttributes } from "react"; +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 font-medium", + "transition-all duration-200 outline-none select-none cursor-pointer", + "focus-visible:ring-2 focus-visible:ring-brand-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-0", + "disabled:pointer-events-none disabled:opacity-45", + "[&_svg]:pointer-events-none [&_svg]:shrink-0", + ], + { + variants: { + variant: { + primary: [ + "bg-brand-600 text-white border border-brand-500/50", + "shadow-glow-brand hover:bg-brand-500 hover:shadow-[0_0_32px_oklch(0.65_0.21_295_/_50%)]", + "active:scale-[0.98]", + ], + secondary: [ + "glass text-text-primary", + "hover:bg-surface-2/80 hover:border-edge-strong active:scale-[0.98]", + ], + ghost: [ + "text-text-secondary hover:text-text-primary hover:bg-surface-2/60", + ], + outline: [ + "border border-edge-strong text-text-primary bg-transparent", + "hover:bg-surface-2/60 hover:border-brand-500/40", + ], + danger: [ + "bg-accent-rose/15 text-accent-rose border border-accent-rose/30", + "hover:bg-accent-rose/25", + ], + }, + size: { + sm: "h-8 px-3 text-xs rounded-lg [&_svg]:size-3.5", + md: "h-9.5 px-4 text-sm rounded-xl [&_svg]:size-4", + lg: "h-11 px-6 text-sm rounded-xl [&_svg]:size-4.5", + icon: "size-9 rounded-xl [&_svg]:size-4", + }, + }, + defaultVariants: { + variant: "secondary", + size: "md", + }, + }, +); + +export interface ButtonProps + extends ButtonHTMLAttributes, + VariantProps {} + +export const Button = forwardRef( + ({ className, variant, size, ...props }, ref) => ( + + ); +} diff --git a/frontend/src/hooks/useAnimatedCounter.ts b/frontend/src/hooks/useAnimatedCounter.ts new file mode 100644 index 0000000..ae7393c --- /dev/null +++ b/frontend/src/hooks/useAnimatedCounter.ts @@ -0,0 +1,30 @@ +import { useEffect, useRef, useState } from "react"; + +/** Smoothly animates a number toward its target using rAF + easing. */ +export function useAnimatedCounter(target: number, durationMs = 800): number { + const [value, setValue] = useState(0); + const fromRef = useRef(0); + const frameRef = useRef(0); + + useEffect(() => { + const from = fromRef.current; + const start = performance.now(); + + const tick = (now: number) => { + const t = Math.min((now - start) / durationMs, 1); + const eased = 1 - Math.pow(1 - t, 4); + const next = Math.round(from + (target - from) * eased); + setValue(next); + if (t < 1) { + frameRef.current = requestAnimationFrame(tick); + } else { + fromRef.current = target; + } + }; + + frameRef.current = requestAnimationFrame(tick); + return () => cancelAnimationFrame(frameRef.current); + }, [target, durationMs]); + + return value; +} diff --git a/frontend/src/hooks/useCopy.ts b/frontend/src/hooks/useCopy.ts new file mode 100644 index 0000000..b259ef1 --- /dev/null +++ b/frontend/src/hooks/useCopy.ts @@ -0,0 +1,28 @@ +import { useCallback, useRef, useState } from "react"; +import { toast } from "sonner"; +import { useAppStore } from "@/store/useAppStore"; + +export function useCopy(label = "Copied to clipboard") { + const [copied, setCopied] = useState(false); + const timeoutRef = useRef>(undefined); + const logActivity = useAppStore((s) => s.logActivity); + + const copy = useCallback( + async (text: string) => { + if (!text) return; + try { + await navigator.clipboard.writeText(text); + setCopied(true); + toast.success(label); + logActivity("copy", label); + clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Clipboard unavailable"); + } + }, + [label, logActivity], + ); + + return { copied, copy }; +} diff --git a/frontend/src/hooks/useGit.ts b/frontend/src/hooks/useGit.ts new file mode 100644 index 0000000..c12c1c0 --- /dev/null +++ b/frontend/src/hooks/useGit.ts @@ -0,0 +1,80 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { api } from "@/lib/api"; +import { parseDiff } from "@/lib/diff"; +import { useAppStore } from "@/store/useAppStore"; + +export function useGitDiff() { + return useQuery({ + queryKey: ["git-diff"], + queryFn: api.getDiff, + select: (data) => parseDiff(data.diff ?? ""), + enabled: false, + retry: 1, + }); +} + +export function useScanDiff() { + const queryClient = useQueryClient(); + const logActivity = useAppStore((s) => s.logActivity); + + return useMutation({ + mutationFn: api.getDiff, + onSuccess: (data) => { + queryClient.setQueryData(["git-diff"], data); + const stats = parseDiff(data.diff ?? ""); + logActivity( + "scan", + stats.files.length > 0 + ? `Scanned diff — ${stats.files.length} file${stats.files.length === 1 ? "" : "s"} changed` + : "Scanned diff — no staged changes", + ); + if (stats.files.length === 0) { + toast.info("No staged changes", { + description: "Run `git add` to stage changes first.", + }); + } + }, + onError: (err: Error) => { + toast.error("Failed to scan diff", { description: err.message }); + }, + }); +} + +export function useGenerateCommit() { + const logActivity = useAppStore((s) => s.logActivity); + const addOutput = useAppStore((s) => s.addOutput); + + return useMutation({ + mutationFn: api.generateCommit, + onSuccess: (data) => { + logActivity("commit", "Generated commit message"); + addOutput({ kind: "commit", content: data.message, timeTakenSec: data.time_taken_sec }); + toast.success("Commit message ready", { + description: data.time_taken_sec ? `Generated in ${data.time_taken_sec}s` : undefined, + }); + }, + onError: (err: Error) => { + toast.error("Commit generation failed", { description: err.message }); + }, + }); +} + +export function useGeneratePR() { + const logActivity = useAppStore((s) => s.logActivity); + const addOutput = useAppStore((s) => s.addOutput); + + return useMutation({ + mutationFn: api.generatePR, + onSuccess: (data) => { + logActivity("pr", "Generated PR description"); + addOutput({ kind: "pr", content: data.description, timeTakenSec: data.time_taken_sec }); + toast.success("PR description ready", { + description: data.time_taken_sec ? `Generated in ${data.time_taken_sec}s` : undefined, + }); + }, + onError: (err: Error) => { + toast.error("PR generation failed", { description: err.message }); + }, + }); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index a248f94..ed81386 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,115 +1,195 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; +@import "@fontsource-variable/inter"; +@import "@fontsource-variable/jetbrains-mono"; +@import "tailwindcss"; + +/* ============================================ + DevLog Design Tokens — dark-first system + ============================================ */ +@theme { + /* Typography */ + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono Variable", ui-monospace, "SF Mono", monospace; + + /* Surface scale (near-black, blue-tinted) */ + --color-surface-0: oklch(0.13 0.012 260); + --color-surface-1: oklch(0.16 0.014 260); + --color-surface-2: oklch(0.19 0.016 260); + --color-surface-3: oklch(0.23 0.018 260); + + /* Text scale */ + --color-text-primary: oklch(0.95 0.01 260); + --color-text-secondary: oklch(0.72 0.015 260); + --color-text-tertiary: oklch(0.55 0.02 260); + + /* Brand — electric violet → cyan spectrum */ + --color-brand-300: oklch(0.82 0.12 295); + --color-brand-400: oklch(0.74 0.16 295); + --color-brand-500: oklch(0.65 0.21 295); + --color-brand-600: oklch(0.56 0.23 295); + + --color-accent-cyan: oklch(0.81 0.13 220); + --color-accent-emerald: oklch(0.78 0.16 160); + --color-accent-amber: oklch(0.82 0.14 80); + --color-accent-rose: oklch(0.7 0.19 15); + + /* Borders */ + --color-edge: oklch(1 0 0 / 8%); + --color-edge-strong: oklch(1 0 0 / 14%); + + /* Shadows / glows */ + --shadow-glow-brand: 0 0 24px oklch(0.65 0.21 295 / 35%); + --shadow-glow-cyan: 0 0 24px oklch(0.81 0.13 220 / 30%); + --shadow-card: 0 1px 0 oklch(1 0 0 / 6%) inset, 0 8px 32px oklch(0 0 0 / 45%); + + /* Motion */ + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - @media (max-width: 1024px) { - font-size: 16px; + /* Radii */ + --radius-xl2: 1.25rem; + + --animate-shimmer: shimmer 2s linear infinite; + --animate-pulse-glow: pulse-glow 3s ease-in-out infinite; + --animate-grid-flow: grid-flow 20s linear infinite; + + @keyframes shimmer { + from { + background-position: 200% 0; + } + to { + background-position: -200% 0; + } } -} -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + @keyframes pulse-glow { + 0%, + 100% { + opacity: 0.6; + } + 50% { + opacity: 1; + } } - #social .button-icon { - filter: invert(1) brightness(2); + @keyframes grid-flow { + from { + background-position: 0 0; + } + to { + background-position: 0 48px; + } } } +/* ============================================ + Base + ============================================ */ +html { + color-scheme: dark; +} + body { - margin: 0; -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } + @apply bg-surface-0 text-text-primary font-sans antialiased; + text-rendering: optimizeLegibility; + overflow: hidden; } -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } + +::selection { + background: oklch(0.65 0.21 295 / 35%); } -p { - margin: 0; + +/* Scrollbars */ +* { + scrollbar-width: thin; + scrollbar-color: oklch(1 0 0 / 12%) transparent; +} +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} +*::-webkit-scrollbar-thumb { + background: oklch(1 0 0 / 12%); + border-radius: 9999px; +} +*::-webkit-scrollbar-track { + background: transparent; } -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); +/* ============================================ + Utilities — glass, gradients, surfaces + ============================================ */ +@utility glass { + background: oklch(0.17 0.014 260 / 62%); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + border: 1px solid var(--color-edge); } -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); +@utility glass-strong { + background: oklch(0.15 0.014 260 / 80%); + backdrop-filter: blur(24px) saturate(160%); + -webkit-backdrop-filter: blur(24px) saturate(160%); + border: 1px solid var(--color-edge-strong); +} + +@utility text-gradient { + background: linear-gradient( + 100deg, + var(--color-text-primary) 0%, + var(--color-brand-300) 55%, + var(--color-accent-cyan) 100% + ); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +@utility edge-gradient { + position: relative; +} +.edge-gradient::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + oklch(0.65 0.21 295 / 50%), + oklch(1 0 0 / 6%) 40%, + oklch(0.81 0.13 220 / 40%) + ); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +@utility shimmer-text { + background: linear-gradient( + 110deg, + var(--color-text-tertiary) 35%, + var(--color-text-primary) 50%, + var(--color-text-tertiary) 65% + ); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + animation: var(--animate-shimmer); +} + +@utility bg-grid { + background-image: + linear-gradient(oklch(1 0 0 / 3%) 1px, transparent 1px), + linear-gradient(90deg, oklch(1 0 0 / 3%) 1px, transparent 1px); + background-size: 48px 48px; +} + +@utility no-scrollbar { + scrollbar-width: none; +} +.no-scrollbar::-webkit-scrollbar { + display: none; } -body { - margin: 0; - background: #0d1117; -} \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..3be13f9 --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,30 @@ +const BASE_URL = "http://localhost:8000"; + +async function request(path: string): Promise { + const res = await fetch(`${BASE_URL}${path}`); + if (!res.ok) { + throw new Error(`API error ${res.status}: ${res.statusText}`); + } + return res.json() as Promise; +} + +export interface DiffResponse { + diff: string; +} + +export interface CommitResponse { + message: string; + time_taken_sec?: number; +} + +export interface PRResponse { + description: string; + time_taken_sec?: number; +} + +export const api = { + getDiff: () => request("/git-diff"), + generateCommit: () => request("/generate-commit"), + generatePR: () => request("/generate-pr"), + warmup: () => request<{ status: string }>("/warmup"), +}; diff --git a/frontend/src/lib/diff.ts b/frontend/src/lib/diff.ts new file mode 100644 index 0000000..15c9110 --- /dev/null +++ b/frontend/src/lib/diff.ts @@ -0,0 +1,68 @@ +export interface DiffFile { + path: string; + additions: number; + deletions: number; + status: "modified" | "added" | "deleted"; +} + +export interface DiffStats { + files: DiffFile[]; + additions: number; + deletions: number; + raw: string; +} + +/** Parse a raw `git diff` string into per-file stats. */ +export function parseDiff(raw: string): DiffStats { + const files: DiffFile[] = []; + let current: DiffFile | null = null; + + for (const line of raw.split("\n")) { + if (line.startsWith("diff --git")) { + if (current) files.push(current); + const match = line.match(/ b\/(.+)$/); + current = { + path: match?.[1] ?? "unknown", + additions: 0, + deletions: 0, + status: "modified", + }; + } else if (current) { + if (line.startsWith("new file mode")) current.status = "added"; + else if (line.startsWith("deleted file mode")) current.status = "deleted"; + else if (line.startsWith("+") && !line.startsWith("+++")) current.additions++; + else if (line.startsWith("-") && !line.startsWith("---")) current.deletions++; + } + } + if (current) files.push(current); + + return { + files, + additions: files.reduce((sum, f) => sum + f.additions, 0), + deletions: files.reduce((sum, f) => sum + f.deletions, 0), + raw, + }; +} + +export type DiffLineKind = "add" | "del" | "hunk" | "meta" | "context"; + +export interface DiffLine { + kind: DiffLineKind; + text: string; +} + +/** Classify each diff line for syntax-aware rendering. */ +export function classifyDiffLines(raw: string): DiffLine[] { + return raw + .split("\n") + .filter((l) => l.length > 0) + .map((text) => { + let kind: DiffLineKind = "context"; + if (text.startsWith("+++") || text.startsWith("---") || text.startsWith("diff ") || text.startsWith("index ")) { + kind = "meta"; + } else if (text.startsWith("@@")) kind = "hunk"; + else if (text.startsWith("+")) kind = "add"; + else if (text.startsWith("-")) kind = "del"; + return { kind, text }; + }); +} diff --git a/frontend/src/lib/motion.ts b/frontend/src/lib/motion.ts new file mode 100644 index 0000000..21e8976 --- /dev/null +++ b/frontend/src/lib/motion.ts @@ -0,0 +1,62 @@ +import type { Transition, Variants } from "framer-motion"; + +/** Shared spring used for interactive elements. */ +export const spring: Transition = { + type: "spring", + stiffness: 380, + damping: 30, +}; + +/** Soft spring for larger surfaces (panels, pages). */ +export const softSpring: Transition = { + type: "spring", + stiffness: 220, + damping: 28, +}; + +/** Page-level transition variants. */ +export const pageVariants: Variants = { + initial: { opacity: 0, y: 12, filter: "blur(4px)" }, + enter: { + opacity: 1, + y: 0, + filter: "blur(0px)", + transition: { ...softSpring, staggerChildren: 0.06 }, + }, + exit: { + opacity: 0, + y: -8, + filter: "blur(4px)", + transition: { duration: 0.18, ease: "easeIn" }, + }, +}; + +/** Staggered container — children animate in sequence. */ +export const staggerContainer: Variants = { + initial: {}, + enter: { transition: { staggerChildren: 0.07, delayChildren: 0.05 } }, +}; + +/** Card / item entry. */ +export const fadeUp: Variants = { + initial: { opacity: 0, y: 16, scale: 0.98 }, + enter: { opacity: 1, y: 0, scale: 1, transition: softSpring }, +}; + +/** Slide in from the right (right panel items). */ +export const slideInRight: Variants = { + initial: { opacity: 0, x: 16 }, + enter: { opacity: 1, x: 0, transition: spring }, +}; + +/** Scale pop for badges/pills. */ +export const pop: Variants = { + initial: { opacity: 0, scale: 0.8 }, + enter: { opacity: 1, scale: 1, transition: spring }, +}; + +/** Hover lift for interactive cards. */ +export const hoverLift = { + whileHover: { y: -3, transition: spring }, + whileTap: { scale: 0.99 }, +}; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 0000000..70926c9 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,17 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function timeAgo(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 5) return "just now"; + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx deleted file mode 100644 index b9a1a6d..0000000 --- a/frontend/src/main.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.jsx' - -createRoot(document.getElementById('root')).render( - - - , -) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..c5121d9 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,22 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +}); + +createRoot(document.getElementById("root")!).render( + + + + + , +); diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts new file mode 100644 index 0000000..e3ae89a --- /dev/null +++ b/frontend/src/store/useAppStore.ts @@ -0,0 +1,70 @@ +import { create } from "zustand"; + +export type ViewId = "dashboard" | "analysis" | "commit" | "pr" | "settings"; + +export type ActivityKind = "scan" | "commit" | "pr" | "copy" | "system"; + +export interface ActivityItem { + id: string; + kind: ActivityKind; + label: string; + timestamp: number; +} + +export interface GeneratedOutput { + id: string; + kind: "commit" | "pr"; + content: string; + timestamp: number; + timeTakenSec?: number; +} + +interface AppState { + view: ViewId; + setView: (view: ViewId) => void; + + commandOpen: boolean; + setCommandOpen: (open: boolean) => void; + + activity: ActivityItem[]; + logActivity: (kind: ActivityKind, label: string) => void; + + outputs: GeneratedOutput[]; + addOutput: (output: Omit) => void; + + /* Settings */ + reducedMotion: boolean; + setReducedMotion: (v: boolean) => void; + show3D: boolean; + setShow3D: (v: boolean) => void; +} + +let idCounter = 0; +const uid = () => `${Date.now()}-${idCounter++}`; + +export const useAppStore = create((set) => ({ + view: "dashboard", + setView: (view) => set({ view }), + + commandOpen: false, + setCommandOpen: (commandOpen) => set({ commandOpen }), + + activity: [ + { id: uid(), kind: "system", label: "DevLog session started", timestamp: Date.now() }, + ], + logActivity: (kind, label) => + set((state) => ({ + activity: [{ id: uid(), kind, label, timestamp: Date.now() }, ...state.activity].slice(0, 50), + })), + + outputs: [], + addOutput: (output) => + set((state) => ({ + outputs: [{ ...output, id: uid(), timestamp: Date.now() }, ...state.outputs].slice(0, 20), + })), + + reducedMotion: false, + setReducedMotion: (reducedMotion) => set({ reducedMotion }), + show3D: true, + setShow3D: (show3D) => set({ show3D }), +})); diff --git a/frontend/src/views/CommitView.tsx b/frontend/src/views/CommitView.tsx new file mode 100644 index 0000000..ad16d67 --- /dev/null +++ b/frontend/src/views/CommitView.tsx @@ -0,0 +1,113 @@ +import { motion } from "framer-motion"; +import { GitCommitHorizontal, RefreshCw, Sparkles, Wand2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Badge } from "@/components/ui/badge"; +import { SectionHeader } from "@/components/shared/SectionHeader"; +import { EmptyState } from "@/components/shared/EmptyState"; +import { OutputBlock } from "@/components/shared/OutputBlock"; +import { useGenerateCommit } from "@/hooks/useGit"; +import { useAppStore } from "@/store/useAppStore"; +import { fadeUp, staggerContainer } from "@/lib/motion"; + +export function CommitView() { + const commit = useGenerateCommit(); + const outputs = useAppStore((s) => s.outputs); + const commitOutputs = outputs.filter((o) => o.kind === "commit"); + const latest = commitOutputs[0]; + + return ( + + commit.mutate()} disabled={commit.isPending}> + {commit.isPending ? ( + <> + Generating… + + ) : ( + <> + Generate commit + + )} + + } + /> + + {commit.isPending && ( + + +
+ + Reading your diff and writing a commit message… +
+
+ + +
+
+
+ )} + + {!commit.isPending && !latest && ( + + + commit.mutate()}> + Generate now + + } + /> + + + )} + + {!commit.isPending && latest && ( + + + +
+
+ Apply it +
+ + git commit -m "{latest.content.replaceAll('"', '\\"')}" + +
+
+ )} + + {commitOutputs.length > 1 && ( + + + +
+ History {commitOutputs.length - 1} +
+ {commitOutputs.slice(1, 6).map((output) => ( +
+ {output.content} +
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/frontend/src/views/DashboardView.tsx b/frontend/src/views/DashboardView.tsx new file mode 100644 index 0000000..21241bc --- /dev/null +++ b/frontend/src/views/DashboardView.tsx @@ -0,0 +1,149 @@ +import { motion } from "framer-motion"; +import { + ArrowRight, + FileDiff, + GitCommitHorizontal, + GitPullRequest, + Minus, + Plus, + ScanLine, + Sparkles, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { StatCard } from "@/components/shared/StatCard"; +import { useGitDiff, useScanDiff, useGenerateCommit, useGeneratePR } from "@/hooks/useGit"; +import { useAppStore } from "@/store/useAppStore"; +import { fadeUp, staggerContainer, hoverLift } from "@/lib/motion"; + +export function DashboardView() { + const setView = useAppStore((s) => s.setView); + const outputs = useAppStore((s) => s.outputs); + const { data: stats } = useGitDiff(); + const scan = useScanDiff(); + const commit = useGenerateCommit(); + const pr = useGeneratePR(); + + return ( + + {/* Hero */} + + +
+
+
+ + Local AI · Ollama powered + +

+ Ship better commits, written by AI. +

+

+ DevLog reads your staged changes and writes conventional commit messages and + pull-request descriptions — entirely on your machine. +

+
+ + +
+
+ + + + {/* Repository status */} + +

+ Repository status +

+ 0 ? "emerald" : "neutral"}> + {stats + ? stats.files.length > 0 + ? "Staged changes detected" + : "Working tree clean" + : "Not scanned yet"} + +
+ +
+ + + + +
+ + {/* Workflow panels */} +
+ + setView("commit")}> + +
+ +
+
Commit Generator
+

+ Conventional-commits formatted messages, generated from your staged diff in seconds. +

+ +
+
+
+ + + setView("pr")}> + +
+ +
+
PR Generator
+

+ Structured pull-request descriptions with summary, change list, and stats. +

+ +
+
+
+
+ + ); +} diff --git a/frontend/src/views/GitAnalysisView.tsx b/frontend/src/views/GitAnalysisView.tsx new file mode 100644 index 0000000..0ab8a66 --- /dev/null +++ b/frontend/src/views/GitAnalysisView.tsx @@ -0,0 +1,143 @@ +import { motion } from "framer-motion"; +import { FileDiff, FilePlus2, FileX2, GitBranch, Minus, Plus, RefreshCw, ScanLine } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SectionHeader } from "@/components/shared/SectionHeader"; +import { EmptyState } from "@/components/shared/EmptyState"; +import { DiffViewer } from "@/components/shared/DiffViewer"; +import { StatCard } from "@/components/shared/StatCard"; +import { useGitDiff, useScanDiff } from "@/hooks/useGit"; +import { fadeUp, staggerContainer } from "@/lib/motion"; +import { cn } from "@/lib/utils"; +import type { DiffFile } from "@/lib/diff"; + +const FILE_STATUS_META: Record = { + modified: { icon: FileDiff, tone: "text-accent-amber" }, + added: { icon: FilePlus2, tone: "text-accent-emerald" }, + deleted: { icon: FileX2, tone: "text-accent-rose" }, +}; + +export function GitAnalysisView() { + const { data: stats } = useGitDiff(); + const scan = useScanDiff(); + const hasData = !!stats && stats.raw.trim().length > 0; + + return ( + + scan.mutate()} disabled={scan.isPending}> + {scan.isPending ? ( + <> + Scanning… + + ) : ( + <> + {hasData ? "Re-scan" : "Scan staged diff"} + + )} + + } + /> + + {scan.isPending && ( + +
+ + + +
+ +
+ )} + + {!scan.isPending && !hasData && ( + + + scan.mutate()}> + Scan now + + } + /> + + + )} + + {!scan.isPending && hasData && stats && ( + <> +
+ + + +
+ +
+ {/* Changed files list */} + + + +
+ Changed files + {stats.files.length} staged +
+
+ + {stats.files.map((file, i) => { + const meta = FILE_STATUS_META[file.status]; + return ( + + + + {file.path} + + + +{file.additions}{" "} + −{file.deletions} + + + ); + })} + +
+
+ + {/* Raw diff */} + + + + Diff preview + + git diff --cached + + + + + + + +
+ + )} +
+ ); +} diff --git a/frontend/src/views/PRView.tsx b/frontend/src/views/PRView.tsx new file mode 100644 index 0000000..ab7a47e --- /dev/null +++ b/frontend/src/views/PRView.tsx @@ -0,0 +1,84 @@ +import { motion } from "framer-motion"; +import { GitPullRequest, RefreshCw, Sparkles, Wand2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { SectionHeader } from "@/components/shared/SectionHeader"; +import { EmptyState } from "@/components/shared/EmptyState"; +import { OutputBlock } from "@/components/shared/OutputBlock"; +import { useGeneratePR } from "@/hooks/useGit"; +import { useAppStore } from "@/store/useAppStore"; +import { fadeUp, staggerContainer } from "@/lib/motion"; + +export function PRView() { + const pr = useGeneratePR(); + const outputs = useAppStore((s) => s.outputs); + const latest = outputs.find((o) => o.kind === "pr"); + + return ( + + pr.mutate()} disabled={pr.isPending}> + {pr.isPending ? ( + <> + Generating… + + ) : ( + <> + Generate PR + + )} + + } + /> + + {pr.isPending && ( + + +
+ + Analyzing changes and drafting your PR description… +
+
+ + + + +
+
+
+ )} + + {!pr.isPending && !latest && ( + + + pr.mutate()}> + Generate now + + } + /> + + + )} + + {!pr.isPending && latest && ( + + + + )} +
+ ); +} diff --git a/frontend/src/views/SettingsView.tsx b/frontend/src/views/SettingsView.tsx new file mode 100644 index 0000000..cf811b6 --- /dev/null +++ b/frontend/src/views/SettingsView.tsx @@ -0,0 +1,91 @@ +import { motion } from "framer-motion"; +import { Box, Cpu, Gauge, Server } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { SectionHeader } from "@/components/shared/SectionHeader"; +import { useAppStore } from "@/store/useAppStore"; +import { fadeUp, staggerContainer } from "@/lib/motion"; + +function SettingRow({ + icon: Icon, + title, + description, + control, +}: { + icon: typeof Cpu; + title: string; + description: string; + control: React.ReactNode; +}) { + return ( +
+
+
+ +
+
+
{title}
+

{description}

+
+
+
{control}
+
+ ); +} + +export function SettingsView() { + const show3D = useAppStore((s) => s.show3D); + const setShow3D = useAppStore((s) => s.setShow3D); + const reducedMotion = useAppStore((s) => s.reducedMotion); + const setReducedMotion = useAppStore((s) => s.setReducedMotion); + + return ( + + + + + + + } + /> + } + /> + + + + + + + + mistral} + /> + localhost:8000} + /> + + + + + ); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..b100e12 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowJs": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..b50e35c 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,17 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});