From 5bbf75a65b77781ecee221385792d39d0d348823 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 14 Aug 2026 10:49:44 +0700 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20finish=20shadcn=E2=86=92c?= =?UTF-8?q?ustom=20primitive=20migration=20(green=20build)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove tw-animate-css import + dead src/components/ui shadcn tree - Convert 7 orphaned components (moderation, analysis, guild-selector, voice/activity-timeline, shared/empty+error) to new primitives - Add missing moderation/view.tsx; analysis uses SearchPanel directly - globals.css now uses new signal-driven ops-console tokens - tsc --noEmit clean, next build green (11 routes), local smoke 200 --- services/frontend/package.json | 18 +- services/frontend/pnpm-lock.yaml | 3209 +---------------- .../src/app/(dashboard)/analysis/page.tsx | 2 +- .../src/app/(dashboard)/dashboard/page.tsx | 20 +- .../src/app/(dashboard)/dashboard/view.tsx | 354 +- .../frontend/src/app/(dashboard)/layout.tsx | 75 +- .../src/app/(dashboard)/media/view.tsx | 159 +- .../src/app/(dashboard)/messages/page.tsx | 11 +- .../src/app/(dashboard)/messages/view.tsx | 345 +- .../src/app/(dashboard)/moderation/page.tsx | 29 +- .../src/app/(dashboard)/moderation/view.tsx | 19 + .../src/app/(dashboard)/recordings/page.tsx | 6 +- .../src/app/(dashboard)/recordings/view.tsx | 351 +- .../src/app/(dashboard)/voice/page.tsx | 24 +- .../src/app/(dashboard)/voice/view.tsx | 283 +- services/frontend/src/app/globals.css | 469 ++- services/frontend/src/app/layout.tsx | 21 +- .../src/components/analysis/search-panel.tsx | 161 +- .../src/components/charts/area-activity.tsx | 83 + .../src/components/charts/radial-gauge.tsx | 89 + .../src/components/charts/session-ribbon.tsx | 56 + .../src/components/charts/sparkline.tsx | 67 + .../src/components/charts/waveform.tsx | 76 + .../components/dashboard/activity-chart.tsx | 148 +- .../components/dashboard/channels-section.tsx | 242 +- .../dashboard/hourly-activity-chart.tsx | 122 +- .../components/dashboard/moderation-donut.tsx | 137 +- .../dashboard/reactions-section.tsx | 172 +- .../src/components/dashboard/stat-card.tsx | 101 - .../dashboard/top-channels-chart.tsx | 99 +- .../components/dashboard/users-section.tsx | 307 +- .../src/components/layout/app-sidebar.tsx | 84 - .../frontend/src/components/layout/spine.tsx | 99 + .../src/components/layout/status-bar.tsx | 66 + .../src/components/layout/sub-nav.tsx | 49 - .../src/components/layout/theme-toggle.tsx | 58 +- .../src/components/media/mini-player.tsx | 111 +- .../src/components/media/music-player.tsx | 178 - .../components/messages/ai-analysis-panel.tsx | 85 +- .../components/messages/ai-status-badge.tsx | 50 +- .../components/messages/attachments-grid.tsx | 84 +- .../src/components/messages/lightbox.tsx | 174 +- .../src/components/messages/message-card.tsx | 175 - .../messages/message-detail-view.tsx | 178 +- .../components/messages/message-detail.tsx | 76 - .../src/components/messages/message-entry.tsx | 86 + .../src/components/messages/message-list.tsx | 68 +- .../components/messages/search-overlay.tsx | 148 +- .../moderation/moderation-section.tsx | 140 +- .../components/motion/route-transition.tsx | 30 + .../src/components/motion/stagger.tsx | 53 + .../src/components/motion/variants.ts | 31 + .../src/components/primitives/avatar.tsx | 39 + .../src/components/primitives/badge.tsx | 42 + .../src/components/primitives/button.tsx | 55 + .../src/components/primitives/dialog.tsx | 75 + .../src/components/primitives/index.ts | 11 + .../src/components/primitives/input.tsx | 23 + .../src/components/primitives/progress.tsx | 39 + .../src/components/primitives/select.tsx | 26 + .../src/components/primitives/sheet.tsx | 74 + .../src/components/primitives/skeleton.tsx | 20 + .../src/components/primitives/toast.tsx | 147 + .../src/components/primitives/tooltip.tsx | 50 + .../components/recordings/recording-card.tsx | 161 - .../recordings/recording-player.tsx | 205 +- .../src/components/shared/empty-state.tsx | 16 +- .../src/components/shared/error-boundary.tsx | 20 +- .../src/components/shared/error-state.tsx | 13 +- .../src/components/shared/guild-selector.tsx | 76 +- .../frontend/src/components/three/index.tsx | 49 + .../src/components/three/orb-field.tsx | 136 + .../src/components/three/signal-field.tsx | 91 + .../src/components/three/static-fallback.tsx | 94 + .../src/components/three/use-three-scene.ts | 135 + .../src/components/three/webgl-guard.tsx | 35 + .../frontend/src/components/ui/accordion.tsx | 72 - .../src/components/ui/alert-dialog.tsx | 187 - services/frontend/src/components/ui/alert.tsx | 76 - .../src/components/ui/aspect-ratio.tsx | 22 - .../frontend/src/components/ui/avatar.tsx | 109 - services/frontend/src/components/ui/badge.tsx | 52 - .../frontend/src/components/ui/breadcrumb.tsx | 125 - .../frontend/src/components/ui/button.tsx | 58 - .../frontend/src/components/ui/calendar.tsx | 221 -- services/frontend/src/components/ui/card.tsx | 103 - .../frontend/src/components/ui/carousel.tsx | 242 -- services/frontend/src/components/ui/chart.tsx | 373 -- .../frontend/src/components/ui/checkbox.tsx | 29 - .../src/components/ui/collapsible.tsx | 21 - .../src/components/ui/context-menu.tsx | 271 -- .../frontend/src/components/ui/dialog.tsx | 160 - .../frontend/src/components/ui/drawer.tsx | 228 -- .../src/components/ui/dropdown-menu.tsx | 268 -- .../frontend/src/components/ui/hover-card.tsx | 51 - .../frontend/src/components/ui/input-otp.tsx | 87 - services/frontend/src/components/ui/input.tsx | 20 - services/frontend/src/components/ui/label.tsx | 20 - .../frontend/src/components/ui/menubar.tsx | 280 -- .../src/components/ui/navigation-menu.tsx | 168 - .../frontend/src/components/ui/pagination.tsx | 130 - .../frontend/src/components/ui/popover.tsx | 90 - .../frontend/src/components/ui/progress.tsx | 83 - .../src/components/ui/radio-group.tsx | 38 - .../frontend/src/components/ui/resizable.tsx | 50 - .../src/components/ui/scroll-area.tsx | 55 - .../frontend/src/components/ui/select.tsx | 201 -- .../frontend/src/components/ui/separator.tsx | 25 - services/frontend/src/components/ui/sheet.tsx | 138 - .../frontend/src/components/ui/sidebar.tsx | 723 ---- .../frontend/src/components/ui/skeleton.tsx | 13 - .../frontend/src/components/ui/slider.tsx | 52 - .../frontend/src/components/ui/sonner.tsx | 45 - .../frontend/src/components/ui/switch.tsx | 32 - services/frontend/src/components/ui/table.tsx | 116 - services/frontend/src/components/ui/tabs.tsx | 82 - .../frontend/src/components/ui/textarea.tsx | 18 - services/frontend/src/components/ui/toast.tsx | 234 -- .../src/components/ui/toggle-group.tsx | 89 - .../frontend/src/components/ui/toggle.tsx | 45 - .../frontend/src/components/ui/tooltip.tsx | 66 - .../voice/active-speakers-panel.tsx | 83 +- .../components/voice/activity-timeline.tsx | 48 +- .../src/components/voice/connection-card.tsx | 200 - .../src/components/voice/listen-control.tsx | 183 +- .../src/components/voice/mic-control.tsx | 87 +- .../src/components/voice/microphone-card.tsx | 57 - .../src/components/voice/speaker-waveform.tsx | 97 +- .../voice/voice-connection-card.tsx | 152 - services/frontend/src/hooks/use-mobile.ts | 24 +- 130 files changed, 4431 insertions(+), 12978 deletions(-) create mode 100644 services/frontend/src/app/(dashboard)/moderation/view.tsx create mode 100644 services/frontend/src/components/charts/area-activity.tsx create mode 100644 services/frontend/src/components/charts/radial-gauge.tsx create mode 100644 services/frontend/src/components/charts/session-ribbon.tsx create mode 100644 services/frontend/src/components/charts/sparkline.tsx create mode 100644 services/frontend/src/components/charts/waveform.tsx delete mode 100644 services/frontend/src/components/dashboard/stat-card.tsx delete mode 100644 services/frontend/src/components/layout/app-sidebar.tsx create mode 100644 services/frontend/src/components/layout/spine.tsx create mode 100644 services/frontend/src/components/layout/status-bar.tsx delete mode 100644 services/frontend/src/components/layout/sub-nav.tsx delete mode 100644 services/frontend/src/components/media/music-player.tsx delete mode 100644 services/frontend/src/components/messages/message-card.tsx delete mode 100644 services/frontend/src/components/messages/message-detail.tsx create mode 100644 services/frontend/src/components/messages/message-entry.tsx create mode 100644 services/frontend/src/components/motion/route-transition.tsx create mode 100644 services/frontend/src/components/motion/stagger.tsx create mode 100644 services/frontend/src/components/motion/variants.ts create mode 100644 services/frontend/src/components/primitives/avatar.tsx create mode 100644 services/frontend/src/components/primitives/badge.tsx create mode 100644 services/frontend/src/components/primitives/button.tsx create mode 100644 services/frontend/src/components/primitives/dialog.tsx create mode 100644 services/frontend/src/components/primitives/index.ts create mode 100644 services/frontend/src/components/primitives/input.tsx create mode 100644 services/frontend/src/components/primitives/progress.tsx create mode 100644 services/frontend/src/components/primitives/select.tsx create mode 100644 services/frontend/src/components/primitives/sheet.tsx create mode 100644 services/frontend/src/components/primitives/skeleton.tsx create mode 100644 services/frontend/src/components/primitives/toast.tsx create mode 100644 services/frontend/src/components/primitives/tooltip.tsx delete mode 100644 services/frontend/src/components/recordings/recording-card.tsx create mode 100644 services/frontend/src/components/three/index.tsx create mode 100644 services/frontend/src/components/three/orb-field.tsx create mode 100644 services/frontend/src/components/three/signal-field.tsx create mode 100644 services/frontend/src/components/three/static-fallback.tsx create mode 100644 services/frontend/src/components/three/use-three-scene.ts create mode 100644 services/frontend/src/components/three/webgl-guard.tsx delete mode 100644 services/frontend/src/components/ui/accordion.tsx delete mode 100644 services/frontend/src/components/ui/alert-dialog.tsx delete mode 100644 services/frontend/src/components/ui/alert.tsx delete mode 100644 services/frontend/src/components/ui/aspect-ratio.tsx delete mode 100644 services/frontend/src/components/ui/avatar.tsx delete mode 100644 services/frontend/src/components/ui/badge.tsx delete mode 100644 services/frontend/src/components/ui/breadcrumb.tsx delete mode 100644 services/frontend/src/components/ui/button.tsx delete mode 100644 services/frontend/src/components/ui/calendar.tsx delete mode 100644 services/frontend/src/components/ui/card.tsx delete mode 100644 services/frontend/src/components/ui/carousel.tsx delete mode 100644 services/frontend/src/components/ui/chart.tsx delete mode 100644 services/frontend/src/components/ui/checkbox.tsx delete mode 100644 services/frontend/src/components/ui/collapsible.tsx delete mode 100644 services/frontend/src/components/ui/context-menu.tsx delete mode 100644 services/frontend/src/components/ui/dialog.tsx delete mode 100644 services/frontend/src/components/ui/drawer.tsx delete mode 100644 services/frontend/src/components/ui/dropdown-menu.tsx delete mode 100644 services/frontend/src/components/ui/hover-card.tsx delete mode 100644 services/frontend/src/components/ui/input-otp.tsx delete mode 100644 services/frontend/src/components/ui/input.tsx delete mode 100644 services/frontend/src/components/ui/label.tsx delete mode 100644 services/frontend/src/components/ui/menubar.tsx delete mode 100644 services/frontend/src/components/ui/navigation-menu.tsx delete mode 100644 services/frontend/src/components/ui/pagination.tsx delete mode 100644 services/frontend/src/components/ui/popover.tsx delete mode 100644 services/frontend/src/components/ui/progress.tsx delete mode 100644 services/frontend/src/components/ui/radio-group.tsx delete mode 100644 services/frontend/src/components/ui/resizable.tsx delete mode 100644 services/frontend/src/components/ui/scroll-area.tsx delete mode 100644 services/frontend/src/components/ui/select.tsx delete mode 100644 services/frontend/src/components/ui/separator.tsx delete mode 100644 services/frontend/src/components/ui/sheet.tsx delete mode 100644 services/frontend/src/components/ui/sidebar.tsx delete mode 100644 services/frontend/src/components/ui/skeleton.tsx delete mode 100644 services/frontend/src/components/ui/slider.tsx delete mode 100644 services/frontend/src/components/ui/sonner.tsx delete mode 100644 services/frontend/src/components/ui/switch.tsx delete mode 100644 services/frontend/src/components/ui/table.tsx delete mode 100644 services/frontend/src/components/ui/tabs.tsx delete mode 100644 services/frontend/src/components/ui/textarea.tsx delete mode 100644 services/frontend/src/components/ui/toast.tsx delete mode 100644 services/frontend/src/components/ui/toggle-group.tsx delete mode 100644 services/frontend/src/components/ui/toggle.tsx delete mode 100644 services/frontend/src/components/ui/tooltip.tsx delete mode 100644 services/frontend/src/components/voice/connection-card.tsx delete mode 100644 services/frontend/src/components/voice/microphone-card.tsx delete mode 100644 services/frontend/src/components/voice/voice-connection-card.tsx diff --git a/services/frontend/package.json b/services/frontend/package.json index d28e25a..c4ff444 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -10,27 +10,16 @@ "format": "biome format --write" }, "dependencies": { - "@base-ui/react": "^1.6.0", - "@shadcn/react": "^0.2.1", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^4.4.0", - "embla-carousel-react": "^8.6.0", - "input-otp": "^1.4.2", "lucide-react": "^1.27.0", + "motion": "^12.0.0", "next": "16.2.12", "next-themes": "^0.4.6", "react": "19.2.4", - "react-day-picker": "^10.0.1", "react-dom": "19.2.4", - "react-resizable-panels": "^4.12.2", - "recharts": "3.8.0", - "shadcn": "^4.15.0", - "sonner": "^2.0.7", "swr": "^2.4.2", "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" + "three": "^0.180.0" }, "devDependencies": { "@biomejs/biome": "2.2.0", @@ -38,8 +27,9 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0.180.0", "babel-plugin-react-compiler": "1.0.0", "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/services/frontend/pnpm-lock.yaml b/services/frontend/pnpm-lock.yaml index 0c297b0..9f5b918 100644 --- a/services/frontend/pnpm-lock.yaml +++ b/services/frontend/pnpm-lock.yaml @@ -8,33 +8,15 @@ importers: .: dependencies: - '@base-ui/react': - specifier: ^1.6.0 - version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@shadcn/react': - specifier: ^0.2.1 - version: 0.2.1(@types/react@19.2.17)(react@19.2.4) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 clsx: specifier: ^2.1.1 version: 2.1.1 - cmdk: - specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - date-fns: - specifier: ^4.4.0 - version: 4.4.0 - embla-carousel-react: - specifier: ^8.6.0 - version: 8.6.0(react@19.2.4) - input-otp: - specifier: ^1.4.2 - version: 1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) lucide-react: specifier: ^1.27.0 version: 1.28.0(react@19.2.4) + motion: + specifier: ^12.0.0 + version: 12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: 16.2.12 version: 16.2.12(@babel/core@7.29.7)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -44,33 +26,18 @@ importers: react: specifier: 19.2.4 version: 19.2.4 - react-day-picker: - specifier: ^10.0.1 - version: 10.0.1(@types/react@19.2.17)(react@19.2.4) react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) - react-resizable-panels: - specifier: ^4.12.2 - version: 4.12.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - recharts: - specifier: 3.8.0 - version: 3.8.0(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.8)(react@19.2.4)(redux@5.0.1) - shadcn: - specifier: ^4.15.0 - version: 4.16.0(typescript@5.9.3) - sonner: - specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) swr: specifier: ^2.4.2 version: 2.4.2(react@19.2.4) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 - tw-animate-css: - specifier: ^1.4.0 - version: 1.4.0 + three: + specifier: ^0.180.0 + version: 0.180.0 devDependencies: '@biomejs/biome': specifier: 2.2.0 @@ -87,6 +54,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.17) + '@types/three': + specifier: ^0.180.0 + version: 0.180.0 babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 @@ -119,28 +89,14 @@ packages: resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.29.7': - resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.29.7': resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.29.7': - resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.29.7': - resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} - engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.29.7': resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} @@ -151,24 +107,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-optimise-call-expression@7.29.7': - resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.29.7': - resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-replace-supers@7.29.7': - resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': - resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} - engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.29.7': resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} @@ -190,40 +128,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.29.7': - resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.29.7': - resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.29.7': - resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.29.7': - resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.29.7': - resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime@7.29.7': - resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} - engines: {node: '>=6.9.0'} - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} @@ -236,33 +140,6 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.6.0': - resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} - engines: {node: '>=14.0.0'} - peerDependencies: - '@date-fns/tz': ^1.2.0 - '@types/react': ^17 || ^18 || ^19 - date-fns: ^4.0.0 - react: ^17 || ^18 || ^19 - react-dom: ^17 || ^18 || ^19 - peerDependenciesMeta: - '@date-fns/tz': - optional: true - '@types/react': - optional: true - date-fns: - optional: true - - '@base-ui/utils@0.3.1': - resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} - peerDependencies: - '@types/react': ^17 || ^18 || ^19 - react: ^17 || ^18 || ^19 - react-dom: ^17 || ^18 || ^19 - peerDependenciesMeta: - '@types/react': - optional: true - '@biomejs/biome@2.2.0': resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==} engines: {node: '>=14.21.3'} @@ -320,40 +197,12 @@ packages: cpu: [x64] os: [win32] - '@date-fns/tz@1.5.0': - resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} - - '@dotenvx/dotenvx@1.75.1': - resolution: {integrity: sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ==} - hasBin: true - - '@dotenvx/primitives@0.8.0': - resolution: {integrity: sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg==} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@floating-ui/core@1.8.0': - resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - - '@floating-ui/dom@1.8.0': - resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - - '@floating-ui/react-dom@2.1.9': - resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - - '@floating-ui/utils@0.2.12': - resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} - - '@hono/node-server@2.0.12': - resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} - engines: {node: '>=20'} - peerDependencies: - hono: ^4 - '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -523,16 +372,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@modelcontextprotocol/sdk@1.30.0': - resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@next/env@16.2.12': resolution: {integrity: sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==} @@ -588,215 +427,6 @@ packages: cpu: [x64] os: [win32] - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@radix-ui/primitive@1.1.7': - resolution: {integrity: sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==} - - '@radix-ui/react-compose-refs@1.1.5': - resolution: {integrity: sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.2.2': - resolution: {integrity: sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.23': - resolution: {integrity: sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.19': - resolution: {integrity: sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.6': - resolution: {integrity: sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.16': - resolution: {integrity: sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.4': - resolution: {integrity: sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-portal@1.1.17': - resolution: {integrity: sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.10': - resolution: {integrity: sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.10': - resolution: {integrity: sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.3.3': - resolution: {integrity: sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.4': - resolution: {integrity: sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.6': - resolution: {integrity: sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.5': - resolution: {integrity: sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.4': - resolution: {integrity: sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@reduxjs/toolkit@2.12.0': - resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} - peerDependencies: - react: ^16.9.0 || ^17.0.0 || ^18 || ^19 - react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 - peerDependenciesMeta: - react: - optional: true - react-redux: - optional: true - - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - - '@shadcn/react@0.2.1': - resolution: {integrity: sha512-5krgi3dRMKb5jH6a+qPzVJUy/54s0kKE4Rw4LjDfLqOdVQTWKUgxWf1kW8r912I0jX/Lzxqc+pgjkjWxUIK5BQ==} - peerDependencies: - '@types/react': '>=19' - react: '>=19' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@standard-schema/utils@0.3.0': - resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -892,35 +522,8 @@ packages: '@tailwindcss/postcss@4.3.3': resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} - '@ts-morph/common@0.27.0': - resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} - - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -933,129 +536,34 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - '@types/use-sync-external-store@0.0.6': - resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} - '@types/validate-npm-package-name@4.0.2': - resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@types/three@0.180.0': + resolution: {integrity: sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - - atomically@1.7.0: - resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} - engines: {node: '>=10.12.0'} + '@webgpu/types@0.1.71': + resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - baseline-browser-mapping@2.11.7: resolution: {integrity: sha512-APw5YuIQAg6L9w4sHDI6j26DGFJI6RpYOhnkMPdC9lWbkKvsyPHzDsve1yd73lk21yz7Y09Kci8B2Pp9FonzWA==} engines: {node: '>=6.0.0'} hasBin: true - body-parser@2.3.0: - resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} - engines: {node: '>=18'} - - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} - engines: {node: 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - browserslist@4.28.7: resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1063,126 +571,12 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cmdk@1.1.1: - resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - react-dom: ^18 || ^19 || ^19.0.0-rc - - code-block-writer@13.0.3: - resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} - - commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - - conf@10.2.0: - resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} - engines: {node: '>=12'} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - - cosmiconfig@9.0.2: - resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} - - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} - - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} - - date-fns@4.4.0: - resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} - - debounce-fn@4.0.0: - resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} - engines: {node: '>=10'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1192,41 +586,6 @@ packages: supports-color: optional: true - decimal.js-light@2.5.1: - resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} - - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - default-browser-id@5.0.1: - resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} - engines: {node: '>=18'} - - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} - engines: {node: '>=18'} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - - define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1235,423 +594,58 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - - dot-prop@6.0.1: - resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} - engines: {node: '>=10'} - - dotenv@17.4.2: - resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} - engines: {node: '>=12'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.398: resolution: {integrity: sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==} - embla-carousel-react@8.6.0: - resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} - peerDependencies: - react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - - embla-carousel-reactive-utils@8.6.0: - resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} - peerDependencies: - embla-carousel: 8.6.0 - - embla-carousel@8.6.0: - resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} - - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enhanced-resolve@5.24.4: resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - - es-toolkit@1.50.0: - resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - eventsource-parser@3.1.0: - resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - execa@9.6.1: - resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} - engines: {node: ^18.19.0 || >=20.5.0} - - express-rate-limit@8.6.1: - resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} - engines: {node: '>= 16'} + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-uri@3.1.4: - resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - picomatch: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: optional: true - - figures@6.1.0: - resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} - engines: {node: '>=18'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - fs-extra@11.4.0: - resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} - engines: {node: '>=14.14'} - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - fuzzysort@3.1.0: - resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} - engines: {node: '>=18'} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - - get-own-enumerable-keys@1.0.0: - resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} - engines: {node: '>=14.16'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-stream@9.0.1: - resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} - engines: {node: '>=18'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - - hono@4.12.32: - resolution: {integrity: sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==} - engines: {node: '>=16.9.0'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - human-signals@8.0.1: - resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} - engines: {node: '>=18.18.0'} - - iconv-lite@0.7.3: - resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} - engines: {node: '>=0.10.0'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - immer@10.2.0: - resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} - - immer@11.1.15: - resolution: {integrity: sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - input-otp@1.4.2: - resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} - peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - - ip-address@10.3.1: - resolution: {integrity: sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==} - engines: {node: '>= 12'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-in-ssh@1.0.0: - resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} - engines: {node: '>=20'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - - is-obj@3.0.0: - resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} - engines: {node: '>=12'} - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - is-regexp@3.1.0: - resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} - engines: {node: '>=12'} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-stream@4.0.1: - resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} - engines: {node: '>=18'} - - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - - is-unicode-supported@2.1.0: - resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} - engines: {node: '>=18'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isexe@3.1.5: - resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} - engines: {node: '>=18'} - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - jose@6.2.5: - resolution: {integrity: sha512-2E5L2yRp03FnwreJLJX8/r7mHiZICCf8kG7fAsTWkSQTDAcc46NIZoQLKy+EJ8sPoJlxyS4OQR5H70LjIZZlIQ==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@7.0.3: - resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -1726,17 +720,6 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1748,55 +731,28 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + meshoptimizer@0.22.0: + resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} - media-typer@1.1.1: - resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} - engines: {node: '>= 0.8'} + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - - mimic-fn@3.1.0: - resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} - engines: {node: '>=8'} - - mimic-function@5.0.1: - resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} - engines: {node: '>=18'} - - minimatch@10.2.6: - resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} - engines: {node: 18 || 20 || >=22} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + motion@12.43.0: + resolution: {integrity: sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==} + 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 ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1806,10 +762,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1841,122 +793,9 @@ packages: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - npm-run-path@6.0.0: - resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} - engines: {node: '>=18'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-treeify@1.1.33: - resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} - engines: {node: '>= 10'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} - engines: {node: '>=20'} - - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - - ora@8.2.0: - resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} - engines: {node: '>=18'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-ms@4.0.0: - resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} - engines: {node: '>=18'} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - - pkg-up@3.1.0: - resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} - engines: {node: '>=8'} - - postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} - engines: {node: '>=4'} - postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -1965,163 +804,15 @@ packages: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} - powershell-utils@0.1.0: - resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} - engines: {node: '>=20'} - - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} - engines: {node: '>=18'} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - qs@6.15.3: - resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - range-parser@1.3.0: - resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - react-day-picker@10.0.1: - resolution: {integrity: sha512-eNh6BlwcYInWaJtRv18mXQ06Ys/H6rdTZAnTaSdOYJuTpwP1JMCHNd1FDRadA+gbeinq+psdULN5Xnowy9mV8w==} - engines: {node: '>=18'} - peerDependencies: - '@types/react': '>=16.8.0' - react: '>=16.8.0' - peerDependenciesMeta: - '@types/react': - optional: true - react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: react: ^19.2.4 - react-is@19.2.8: - resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} - - react-redux@9.3.0: - resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} - peerDependencies: - '@types/react': ^18.2.25 || ^19 - react: ^18.0 || ^19 - redux: ^5.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - redux: - optional: true - - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-resizable-panels@4.12.2: - resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} - recast@0.23.12: - resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} - engines: {node: '>= 4'} - - recharts@3.8.0: - resolution: {integrity: sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==} - engines: {node: '>=18'} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - redux-thunk@3.1.0: - resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} - peerDependencies: - redux: ^5.0.0 - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - reselect@5.1.1: - resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} - - reselect@5.2.0: - resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - run-applescript@7.1.0: - resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} - engines: {node: '>=18'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2134,110 +825,14 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shadcn@4.16.0: - resolution: {integrity: sha512-kPr4RrQmbbZeAjwBYeBSpFvAHV8rkTlNPUluIxkRriq5TpevfFelVO5xvcF6oguHPVn0R6wPuVer4HudfcLy/A==} - engines: {node: '>=20.18.1'} - hasBin: true - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.1: - resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} - engines: {node: '>= 0.4'} - - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - sonner@2.0.7: - resolution: {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 - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} - - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - - stringify-object@5.0.0: - resolution: {integrity: sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg==} - engines: {node: '>=14.16'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-final-newline@4.0.0: - resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} - engines: {node: '>=18'} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -2256,12 +851,6 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - systeminformation@5.33.1: - resolution: {integrity: sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==} - engines: {node: '>=10.0.0'} - os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] - hasBin: true - tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -2272,34 +861,12 @@ packages: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - ts-morph@26.0.0: - resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} - - tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} + three@0.180.0: + resolution: {integrity: sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tw-animate-css@1.4.0: - resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} - - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2308,103 +875,20 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici@7.29.0: - resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} - engines: {node: '>=20.18.1'} - - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - validate-npm-package-name@7.0.2: - resolution: {integrity: sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==} - engines: {node: ^20.17.0 || >=22.9.0} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - victory-vendor@37.3.6: - resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} - engines: {node: '>=20'} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yocto-spinner@1.2.2: - resolution: {integrity: sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==} - engines: {node: '>=18.19'} - - yoctocolors@2.2.0: - resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} - engines: {node: '>=18'} - - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - snapshots: '@alloc/quick-lru@5.2.0': {} @@ -2414,8 +898,10 @@ snapshots: '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 + optional: true - '@babel/compat-data@7.29.7': {} + '@babel/compat-data@7.29.7': + optional: true '@babel/core@7.29.7': dependencies: @@ -2436,6 +922,7 @@ snapshots: semver: 6.3.1 transitivePeerDependencies: - supports-color + optional: true '@babel/generator@7.29.7': dependencies: @@ -2444,10 +931,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - - '@babel/helper-annotate-as-pure@7.29.7': - dependencies: - '@babel/types': 7.29.7 + optional: true '@babel/helper-compilation-targets@7.29.7': dependencies: @@ -2456,28 +940,10 @@ snapshots: browserslist: 4.28.7 lru-cache: 5.1.1 semver: 6.3.1 + optional: true - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.7 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-globals@7.29.7': {} - - '@babel/helper-member-expression-to-functions@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color + '@babel/helper-globals@7.29.7': + optional: true '@babel/helper-module-imports@7.29.7': dependencies: @@ -2485,6 +951,7 @@ snapshots: '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color + optional: true '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: @@ -2494,91 +961,32 @@ snapshots: '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - - '@babel/helper-optimise-call-expression@7.29.7': - dependencies: - '@babel/types': 7.29.7 - - '@babel/helper-plugin-utils@7.29.7': {} - - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 - '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': - dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 - transitivePeerDependencies: - - supports-color + optional: true '@babel/helper-string-parser@7.29.7': {} '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.29.7': {} + '@babel/helper-validator-option@7.29.7': + optional: true '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 '@babel/types': 7.29.7 + optional: true '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 - - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color - - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - transitivePeerDependencies: - - supports-color - - '@babel/runtime@7.29.7': {} + optional: true '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 '@babel/parser': 7.29.7 '@babel/types': 7.29.7 + optional: true '@babel/traverse@7.29.7': dependencies: @@ -2591,37 +999,13 @@ snapshots: debug: 4.4.3 transitivePeerDependencies: - supports-color + optional: true '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@base-ui/react@1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@babel/runtime': 7.29.7 - '@base-ui/utils': 0.3.1(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@floating-ui/utils': 0.2.12 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - '@date-fns/tz': 1.5.0 - '@types/react': 19.2.17 - date-fns: 4.4.0 - - '@base-ui/utils@0.3.1(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@babel/runtime': 7.29.7 - '@floating-ui/utils': 0.2.12 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - reselect: 5.2.0 - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@biomejs/biome@2.2.0': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.2.0 @@ -2657,55 +1041,13 @@ snapshots: '@biomejs/cli-win32-x64@2.2.0': optional: true - '@date-fns/tz@1.5.0': {} - - '@dotenvx/dotenvx@1.75.1': - dependencies: - '@dotenvx/primitives': 0.8.0 - commander: 11.1.0 - conf: 10.2.0 - dotenv: 17.4.2 - enquirer: 2.4.1 - env-paths: 2.2.1 - execa: 5.1.1 - fdir: 6.5.0(picomatch@4.0.5) - ignore: 5.3.2 - object-treeify: 1.1.33 - open: 8.4.2 - picomatch: 4.0.5 - systeminformation: 5.33.1 - undici: 7.29.0 - which: 4.0.0 - yocto-spinner: 1.2.2 - - '@dotenvx/primitives@0.8.0': {} + '@dimforge/rapier3d-compat@0.12.0': {} '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@floating-ui/core@1.8.0': - dependencies: - '@floating-ui/utils': 0.2.12 - - '@floating-ui/dom@1.8.0': - dependencies: - '@floating-ui/core': 1.8.0 - '@floating-ui/utils': 0.2.12 - - '@floating-ui/react-dom@2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/dom': 1.8.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - '@floating-ui/utils@0.2.12': {} - - '@hono/node-server@2.0.12(hono@4.12.32)': - dependencies: - hono: 4.12.32 - '@img/colour@1.1.0': optional: true @@ -2822,28 +1164,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)': - dependencies: - '@hono/node-server': 2.0.12(hono@4.12.32) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.1.0 - express: 5.2.1 - express-rate-limit: 8.6.1(express@5.2.1) - hono: 4.12.32 - jose: 6.2.5 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - supports-color - '@next/env@16.2.12': {} '@next/swc-darwin-arm64@16.2.12': @@ -2870,180 +1190,6 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.12': optional: true - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@radix-ui/primitive@1.1.7': {} - - '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.4)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.4)': - dependencies: - '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.4)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.4) - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.4)': - dependencies: - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - - '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': - dependencies: - '@standard-schema/spec': 1.1.0 - '@standard-schema/utils': 0.3.0 - immer: 11.1.15 - redux: 5.0.1 - redux-thunk: 3.1.0(redux@5.0.1) - reselect: 5.1.1 - optionalDependencies: - react: 19.2.4 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.4)(redux@5.0.1) - - '@sec-ant/readable-stream@0.4.1': {} - - '@shadcn/react@0.2.1(@types/react@19.2.17)(react@19.2.4)': - optionalDependencies: - '@types/react': 19.2.17 - react: 19.2.4 - - '@sindresorhus/merge-streams@4.0.0': {} - - '@standard-schema/spec@1.1.0': {} - - '@standard-schema/utils@0.3.0': {} - '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3117,35 +1263,7 @@ snapshots: postcss: 8.5.25 tailwindcss: 4.3.3 - '@ts-morph/common@0.27.0': - dependencies: - fast-glob: 3.3.3 - minimatch: 10.2.6 - path-browserify: 1.0.1 - - '@types/d3-array@3.2.2': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} + '@tweenjs/tween.js@23.1.3': {} '@types/node@20.19.43': dependencies: @@ -3159,78 +1277,28 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/use-sync-external-store@0.0.6': {} + '@types/stats.js@0.17.4': {} - '@types/validate-npm-package-name@4.0.2': {} - - accepts@2.0.0: + '@types/three@0.180.0': dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + '@webgpu/types': 0.1.71 + fflate: 0.8.3 + meshoptimizer: 0.22.0 - ajv-formats@2.1.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 + '@types/webxr@0.5.24': {} - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.4 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-colors@4.1.3: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - argparse@2.0.1: {} - - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - - atomically@1.7.0: {} + '@webgpu/types@0.1.71': {} babel-plugin-react-compiler@1.0.0: dependencies: '@babel/types': 7.29.7 - balanced-match@4.0.4: {} - baseline-browser-mapping@2.11.7: {} - body-parser@2.3.0: - dependencies: - bytes: 3.1.2 - content-type: 2.0.0 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - on-finished: 2.4.1 - qs: 6.15.3 - raw-body: 3.0.2 - type-is: 2.1.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@5.0.8: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - browserslist@4.28.7: dependencies: baseline-browser-mapping: 2.11.7 @@ -3238,556 +1306,65 @@ snapshots: electron-to-chromium: 1.5.398 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) - - bundle-name@4.1.0: - dependencies: - run-applescript: 7.1.0 - - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: {} + optional: true caniuse-lite@1.0.30001806: {} - chalk@5.6.2: {} - - class-variance-authority@0.7.1: - dependencies: - clsx: 2.1.1 - - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - - cli-spinners@2.9.2: {} - client-only@0.0.1: {} clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - - code-block-writer@13.0.3: {} - - commander@11.1.0: {} - - commander@14.0.3: {} - - conf@10.2.0: - dependencies: - ajv: 8.20.0 - ajv-formats: 2.1.1(ajv@8.20.0) - atomically: 1.7.0 - debounce-fn: 4.0.0 - dot-prop: 6.0.1 - env-paths: 2.2.1 - json-schema-typed: 7.0.3 - onetime: 5.1.2 - pkg-up: 3.1.0 - semver: 7.8.5 - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - - convert-source-map@2.0.0: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - cosmiconfig@9.0.2(typescript@5.9.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.3.0 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.3 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} + convert-source-map@2.0.0: + optional: true csstype@3.2.3: {} - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-color@3.1.0: {} - - d3-ease@3.0.1: {} - - d3-format@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@3.1.0: {} - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - - date-fns@4.4.0: {} - - debounce-fn@4.0.0: - dependencies: - mimic-fn: 3.1.0 - debug@4.4.3: dependencies: ms: 2.1.3 - - decimal.js-light@2.5.1: {} - - dedent@1.7.2: {} - - deepmerge@4.3.1: {} - - default-browser-id@5.0.1: {} - - default-browser@5.5.0: - dependencies: - bundle-name: 4.1.0 - default-browser-id: 5.0.1 - - define-lazy-prop@2.0.0: {} - - define-lazy-prop@3.0.0: {} - - depd@2.0.0: {} + optional: true dequal@2.0.3: {} detect-libc@2.1.2: {} - detect-node-es@1.1.0: {} - - diff@8.0.4: {} - - dot-prop@6.0.1: - dependencies: - is-obj: 2.0.0 - - dotenv@17.4.2: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - electron-to-chromium@1.5.398: {} - - embla-carousel-react@8.6.0(react@19.2.4): - dependencies: - embla-carousel: 8.6.0 - embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) - react: 19.2.4 - - embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): - dependencies: - embla-carousel: 8.6.0 - - embla-carousel@8.6.0: {} - - emoji-regex@10.6.0: {} - - encodeurl@2.0.0: {} + electron-to-chromium@1.5.398: + optional: true enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 - enquirer@2.4.1: + escalade@3.2.0: + optional: true + + fflate@0.8.3: {} + + framer-motion@12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - - env-paths@2.2.1: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - es-toolkit@1.50.0: {} - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - esprima@4.0.1: {} - - etag@1.8.1: {} - - eventemitter3@5.0.4: {} - - eventsource-parser@3.1.0: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.1.0 - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - execa@9.6.1: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.6 - figures: 6.1.0 - get-stream: 9.0.1 - human-signals: 8.0.1 - is-plain-obj: 4.1.0 - is-stream: 4.0.1 - npm-run-path: 6.0.0 - pretty-ms: 9.3.0 - signal-exit: 4.1.0 - strip-final-newline: 4.0.0 - yoctocolors: 2.2.0 - - express-rate-limit@8.6.1(express@5.2.1): - dependencies: - debug: 4.4.3 - express: 5.2.1 - ip-address: 10.3.1 - transitivePeerDependencies: - - supports-color - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.3.0 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.3 - range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.1.0 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-uri@3.1.4: {} - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.5): + motion-dom: 12.43.0 + motion-utils: 12.39.0 + tslib: 2.8.1 optionalDependencies: - picomatch: 4.0.5 - - figures@6.1.0: - dependencies: - is-unicode-supported: 2.1.0 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - - forwarded@0.2.0: {} - - fresh@2.0.0: {} - - fs-extra@11.4.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - - function-bind@1.1.2: {} - - fuzzysort@3.1.0: {} - - gensync@1.0.0-beta.2: {} - - get-east-asian-width@1.6.0: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - - get-nonce@1.0.1: {} - - get-own-enumerable-keys@1.0.0: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - - get-stream@6.0.1: {} - - get-stream@9.0.1: - dependencies: - '@sec-ant/readable-stream': 0.4.1 - is-stream: 4.0.1 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - has-symbols@1.1.0: {} - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - - hono@4.12.32: {} - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - human-signals@2.1.0: {} - - human-signals@8.0.1: {} - - iconv-lite@0.7.3: - dependencies: - safer-buffer: 2.1.2 - - ignore@5.3.2: {} - - immer@10.2.0: {} - - immer@11.1.15: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - inherits@2.0.4: {} - - input-otp@1.4.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - internmap@2.0.3: {} + gensync@1.0.0-beta.2: + optional: true - ip-address@10.3.1: {} - - ipaddr.js@1.9.1: {} - - is-arrayish@0.2.1: {} - - is-docker@2.2.1: {} - - is-docker@3.0.0: {} - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-in-ssh@1.0.0: {} - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-interactive@2.0.0: {} - - is-number@7.0.0: {} - - is-obj@2.0.0: {} - - is-obj@3.0.0: {} - - is-plain-obj@4.1.0: {} - - is-promise@4.0.0: {} - - is-regexp@3.1.0: {} - - is-stream@2.0.1: {} - - is-stream@4.0.1: {} - - is-unicode-supported@1.3.0: {} - - is-unicode-supported@2.1.0: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - is-wsl@3.1.1: - dependencies: - is-inside-container: 1.0.0 - - isexe@2.0.0: {} - - isexe@3.1.5: {} + graceful-fs@4.2.11: {} jiti@2.7.0: {} - jose@6.2.5: {} + js-tokens@4.0.0: + optional: true - js-tokens@4.0.0: {} + jsesc@3.1.0: + optional: true - js-yaml@4.3.0: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-traverse@1.0.0: {} - - json-schema-typed@7.0.3: {} - - json-schema-typed@8.0.2: {} - - json5@2.2.3: {} - - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - kleur@3.0.3: {} - - kleur@4.1.5: {} + json5@2.2.3: + optional: true lightningcss-android-arm64@1.32.0: optional: true @@ -3838,21 +1415,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - lines-and-columns@1.2.4: {} - - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - - log-symbols@6.0.0: - dependencies: - chalk: 5.6.2 - is-unicode-supported: 1.3.0 - lru-cache@5.1.1: dependencies: yallist: 3.1.1 + optional: true lucide-react@1.28.0(react@19.2.4): dependencies: @@ -3862,45 +1428,27 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - math-intrinsics@1.1.0: {} + meshoptimizer@0.22.0: {} - media-typer@1.1.1: {} - - merge-descriptors@2.0.0: {} - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: + motion-dom@12.43.0: dependencies: - braces: 3.0.3 - picomatch: 2.3.2 + motion-utils: 12.39.0 - mime-db@1.54.0: {} + motion-utils@12.39.0: {} - mime-types@3.0.2: + motion@12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - mime-db: 1.54.0 + framer-motion: 12.43.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - mimic-fn@2.1.0: {} - - mimic-fn@3.1.0: {} - - mimic-function@5.0.1: {} - - minimatch@10.2.6: - dependencies: - brace-expansion: 5.0.8 - - minimist@1.2.8: {} - - ms@2.1.3: {} + ms@2.1.3: + optional: true nanoid@3.3.16: {} - negotiator@1.0.0: {} - next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -3931,118 +1479,11 @@ snapshots: - '@babel/core' - babel-plugin-macros - node-releases@2.0.51: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - npm-run-path@6.0.0: - dependencies: - path-key: 4.0.0 - unicorn-magic: 0.3.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - object-treeify@1.1.33: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - - open@11.0.0: - dependencies: - default-browser: 5.5.0 - define-lazy-prop: 3.0.0 - is-in-ssh: 1.0.0 - is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 - - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - ora@8.2.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.2.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - - p-try@2.2.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.7 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse-ms@4.0.0: {} - - parseurl@1.3.3: {} - - path-browserify@1.0.1: {} - - path-exists@3.0.0: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-to-regexp@8.4.2: {} + node-releases@2.0.51: + optional: true picocolors@1.1.1: {} - picomatch@2.3.2: {} - - picomatch@4.0.5: {} - - pkce-challenge@5.0.1: {} - - pkg-up@3.1.0: - dependencies: - find-up: 3.0.0 - - postcss-selector-parser@7.1.4: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss@8.4.31: dependencies: nanoid: 3.3.16 @@ -4055,235 +1496,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - powershell-utils@0.1.0: {} - - pretty-ms@9.3.0: - dependencies: - parse-ms: 4.0.0 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - qs@6.15.3: - dependencies: - es-define-property: 1.0.1 - side-channel: 1.1.1 - - queue-microtask@1.2.3: {} - - range-parser@1.3.0: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.3 - unpipe: 1.0.0 - - react-day-picker@10.0.1(@types/react@19.2.17)(react@19.2.4): - dependencies: - '@date-fns/tz': 1.5.0 - date-fns: 4.4.0 - react: 19.2.4 - optionalDependencies: - '@types/react': 19.2.17 - react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 scheduler: 0.27.0 - react-is@19.2.8: {} - - react-redux@9.3.0(@types/react@19.2.17)(react@19.2.4)(redux@5.0.1): - dependencies: - '@types/use-sync-external-store': 0.0.6 - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - redux: 5.0.1 - - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.4): - dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.4) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.4): - dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.4) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.4) - optionalDependencies: - '@types/react': 19.2.17 - - react-resizable-panels@4.12.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.4): - dependencies: - get-nonce: 1.0.1 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - react@19.2.4: {} - recast@0.23.12: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - - recharts@3.8.0(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.8)(react@19.2.4)(redux@5.0.1): - dependencies: - '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.4)(redux@5.0.1))(react@19.2.4) - clsx: 2.1.1 - decimal.js-light: 2.5.1 - es-toolkit: 1.50.0 - eventemitter3: 5.0.4 - immer: 10.2.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-is: 19.2.8 - react-redux: 9.3.0(@types/react@19.2.17)(react@19.2.4)(redux@5.0.1) - reselect: 5.1.1 - tiny-invariant: 1.3.3 - use-sync-external-store: 1.6.0(react@19.2.4) - victory-vendor: 37.3.6 - transitivePeerDependencies: - - '@types/react' - - redux - - redux-thunk@3.1.0(redux@5.0.1): - dependencies: - redux: 5.0.1 - - redux@5.0.1: {} - - require-from-string@2.0.2: {} - - reselect@5.1.1: {} - - reselect@5.2.0: {} - - resolve-from@4.0.0: {} - - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - - reusify@1.1.0: {} - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - run-applescript@7.1.0: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - semver@6.3.1: {} + semver@6.3.1: + optional: true - semver@7.8.5: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.3.0 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - shadcn@4.16.0(typescript@5.9.3): - dependencies: - '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@dotenvx/dotenvx': 1.75.1 - '@modelcontextprotocol/sdk': 1.30.0(zod@3.25.76) - '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.7 - commander: 14.0.3 - cosmiconfig: 9.0.2(typescript@5.9.3) - dedent: 1.7.2 - deepmerge: 4.3.1 - diff: 8.0.4 - execa: 9.6.1 - fast-glob: 3.3.3 - fs-extra: 11.4.0 - fuzzysort: 3.1.0 - kleur: 4.1.5 - open: 11.0.0 - ora: 8.2.0 - postcss: 8.5.25 - postcss-selector-parser: 7.1.4 - prompts: 2.4.2 - recast: 0.23.12 - stringify-object: 5.0.0 - tailwind-merge: 3.6.0 - ts-morph: 26.0.0 - tsconfig-paths: 4.2.0 - undici: 7.29.0 - validate-npm-package-name: 7.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) - transitivePeerDependencies: - - '@cfworker/json-schema' - - babel-plugin-macros - - supports-color - - typescript + semver@7.8.5: + optional: true sharp@0.34.5: dependencies: @@ -4317,85 +1543,8 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@3.0.7: {} - - signal-exit@4.1.0: {} - - sisteransi@1.0.5: {} - - sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - source-map-js@1.2.1: {} - source-map@0.6.1: {} - - statuses@2.0.2: {} - - stdin-discarder@0.2.2: {} - - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 - strip-ansi: 7.2.0 - - stringify-object@5.0.0: - dependencies: - get-own-enumerable-keys: 1.0.0 - is-obj: 3.0.0 - is-regexp: 3.1.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.2.2 - - strip-bom@3.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-final-newline@4.0.0: {} - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.4): dependencies: client-only: 0.0.1 @@ -4409,128 +1558,30 @@ snapshots: react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) - systeminformation@5.33.1: {} - tailwind-merge@3.6.0: {} tailwindcss@4.3.3: {} tapable@2.3.3: {} - tiny-invariant@1.3.3: {} - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - ts-morph@26.0.0: - dependencies: - '@ts-morph/common': 0.27.0 - code-block-writer: 13.0.3 - - tsconfig-paths@4.2.0: - dependencies: - json5: 2.2.3 - minimist: 1.2.8 - strip-bom: 3.0.0 + three@0.180.0: {} tslib@2.8.1: {} - tw-animate-css@1.4.0: {} - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.1 - mime-types: 3.0.2 - typescript@5.9.3: {} undici-types@6.21.0: {} - undici@7.29.0: {} - - unicorn-magic@0.3.0: {} - - universalify@2.0.1: {} - - unpipe@1.0.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 escalade: 3.2.0 picocolors: 1.1.1 - - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.4): - dependencies: - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.4): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.4 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 + optional: true use-sync-external-store@1.6.0(react@19.2.4): dependencies: react: 19.2.4 - util-deprecate@1.0.2: {} - - validate-npm-package-name@7.0.2: {} - - vary@1.1.2: {} - - victory-vendor@37.3.6: - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-ease': 3.0.2 - '@types/d3-interpolate': 3.0.4 - '@types/d3-scale': 4.0.9 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-timer': 3.0.2 - d3-array: 3.2.4 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-scale: 4.0.2 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-timer: 3.0.1 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - which@4.0.0: - dependencies: - isexe: 3.1.5 - - wrappy@1.0.2: {} - - wsl-utils@0.3.1: - dependencies: - is-wsl: 3.1.1 - powershell-utils: 0.1.0 - - yallist@3.1.1: {} - - yocto-spinner@1.2.2: - dependencies: - yoctocolors: 2.2.0 - - yoctocolors@2.2.0: {} - - zod-to-json-schema@3.25.2(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} + yallist@3.1.1: + optional: true diff --git a/services/frontend/src/app/(dashboard)/analysis/page.tsx b/services/frontend/src/app/(dashboard)/analysis/page.tsx index 1771cc7..9444804 100644 --- a/services/frontend/src/app/(dashboard)/analysis/page.tsx +++ b/services/frontend/src/app/(dashboard)/analysis/page.tsx @@ -4,7 +4,7 @@ import { SearchPanel } from "@/components/analysis/search-panel"; export default function AnalysisPage() { return ( -
+
); diff --git a/services/frontend/src/app/(dashboard)/dashboard/page.tsx b/services/frontend/src/app/(dashboard)/dashboard/page.tsx index 9df5ce0..58656c2 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/page.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/page.tsx @@ -1,24 +1,26 @@ /** - * Dashboard page — Server Component. - * - * Fetches y the initial stats + activity on the server (no client round-trip - * for first paint) and hands them to the hydrated client view. This is the - * "data on the server" leg of the reworked data flow. + * Dashboard — Server Component. + * Fetches initial stats + activity on the server (SSR first paint), hands to + * the hydrated client View. Keeps the documented server-seed data flow. */ import { getActivity, getDashboardStats } from "@/lib/api/server"; import DashboardView from "./view"; export default async function DashboardPage() { const [stats, activity] = await Promise.allSettled([ - getDashboardStats(), - getActivity(14), + getDashboardStats().catch(() => undefined), + getActivity(14).catch(() => undefined), ]); return ( ); diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index 9e999ad..fd09a58 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -1,44 +1,33 @@ "use client"; import { - AlertCircle, - Clock, - Hash, - Heart, - Shield, - Sparkles, + Activity as ActivityIcon, + Flag, + MessagesSquare, + ShieldCheck, Users, } from "lucide-react"; +import { motion, useReducedMotion } from "motion/react"; import { useState } from "react"; +import { RadialGauge } from "@/components/charts/radial-gauge"; +import { Sparkline } from "@/components/charts/sparkline"; import { ActivityChart } from "@/components/dashboard/activity-chart"; import { ChannelsSection } from "@/components/dashboard/channels-section"; import { HourlyActivityChart } from "@/components/dashboard/hourly-activity-chart"; import { ModerationDonut } from "@/components/dashboard/moderation-donut"; import { ReactionsSection } from "@/components/dashboard/reactions-section"; -import { StatCard } from "@/components/dashboard/stat-card"; import { TopChannelsChart } from "@/components/dashboard/top-channels-chart"; import { UsersSection } from "@/components/dashboard/users-section"; -import { SubNav } from "@/components/layout/sub-nav"; -import { ErrorState, LoadingSkeleton } from "@/components/shared"; +import { StaggerGroup, StaggerItem } from "@/components/motion/stagger"; +import { SignalField } from "@/components/three"; import { useActivity, useStats } from "@/hooks"; import type { DashboardActivity, DashboardStats } from "@/lib/types"; import { cn } from "@/lib/utils"; -type DashboardTab = "stats" | "users" | "channels" | "reactions"; +type Tab = "stats" | "users" | "channels" | "reactions"; -const DAY_RANGES = [7, 14, 30] as const; +const DAYS = [7, 14, 30] as const; -const MODERATION_COLORS: Record = { - Clean: "oklch(0.72 0.16 155)", - Flagged: "oklch(0.62 0.19 25)", - Warned: "oklch(0.78 0.15 80)", - Error: "oklch(0.55 0.02 245)", -}; - -/** - * Dashboard view — hydrated on the client but seeded with server-rendered - * initial data. SWR takes over for revalidation after first paint. - */ export default function DashboardView({ initialStats, initialActivity, @@ -46,143 +35,216 @@ export default function DashboardView({ initialStats?: DashboardStats; initialActivity?: DashboardActivity; }) { - const [tab, setTab] = useState("stats"); + const [tab, setTab] = useState("stats"); const [days, setDays] = useState(14); - const { data: stats, error, mutate: refetch } = useStats(initialStats); + const reduce = useReducedMotion(); + + const { data: stats } = useStats(initialStats); const { data: activity } = useActivity( days, days === 14 ? initialActivity : undefined, ); - const subNavTabs = [ - { id: "stats", label: "Stats", icon: }, - { id: "users", label: "Users", icon: }, - { id: "channels", label: "Channels", icon: }, - { id: "reactions", label: "Reactions", icon: }, + const clean = stats?.total_clean ?? 0; + const flagged = stats?.total_flagged ?? 0; + const warned = stats?.total_warned ?? 0; + const total = clean + flagged + warned || 1; + const health = clean / total; + const activityRatio = Math.min( + 1, + (activity?.daily.at(-1)?.messages ?? 0) / + (Math.max(...(activity?.daily.map((d) => d.messages) ?? [1]), 1) || 1), + ); + + const daily = activity?.daily ?? []; + const spark = daily.map((d) => d.messages); + const flaggedSpark = daily.map((d) => d.flagged); + const usersSpark = daily.map((d) => d.active_users); + + const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [ + { + id: "stats", + label: "Stats", + icon: , + }, + { id: "users", label: "Users", icon: }, + { + id: "channels", + label: "Channels", + icon: , + }, + { + id: "reactions", + label: "Reactions", + icon: , + }, ]; - const moderationData = stats - ? [ - { - name: "Clean", - value: stats.total_clean, - color: MODERATION_COLORS.Clean, - }, - { - name: "Flagged", - value: stats.total_flagged, - color: MODERATION_COLORS.Flagged, - }, - { - name: "Warned", - value: stats.total_warned, - color: MODERATION_COLORS.Warned, - }, - { - name: "Error", - value: stats.total_error, - color: MODERATION_COLORS.Error, - }, - ].filter((d) => d.value > 0) - : []; - return ( -
- setTab(t as DashboardTab)} - /> +
+ - {tab === "stats" && ( -
- {error ? ( - - ) : !stats ? ( - - ) : ( - <> -
- - - - - - -
- -
- {DAY_RANGES.map((range) => ( - - ))} -
- -
-
- {activity && } -
- -
- -
-
- {activity && } -
- ({ - name: c.channel_name ?? c.channel_id, - count: c.message_count, - }))} - /> -
- - )} + {/* Tabs */} +
+
+ {tabs.map((t) => ( + + ))}
+
+ {DAYS.map((d) => ( + + ))} +
+
+ + {/* Ticker row (stats tab) */} + {tab === "stats" && ( + + } + label="Messages" + value={stats?.total_messages ?? 0} + data={spark} + /> + } + label="Flagged" + value={flagged} + data={flaggedSpark} + tone="vermilion" + /> + } + label="Active 24h" + value={stats?.active_users_24h ?? 0} + data={usersSpark} + tone="amber" + /> + } + label="Recordings" + value={stats?.total_voice_recordings ?? 0} + data={spark} + /> + )} - {tab === "users" && } - - {tab === "channels" && } - - {tab === "reactions" && } +
+
+ {tab === "stats" && ( + <> + + + + + )} + {tab === "users" && } + {tab === "channels" && } + {tab === "reactions" && } +
+
+ +
+
); } + +function Hero({ + stats, + activityRatio, + health, +}: { + stats?: DashboardStats; + activityRatio: number; + health: number; +}) { + return ( +
+
+ +
+
+
+
Bete Console
+
+ {stats?.total_messages?.toLocaleString() ?? 0} messages watched ·{" "} + {stats?.total_users?.toLocaleString() ?? 0} users +
+
+ 0.8 ? "signal" : health > 0.6 ? "amber" : "vermilion"} + /> +
+
+ ); +} + +function Ticker({ + icon, + label, + value, + data, + tone = "signal", +}: { + icon: React.ReactNode; + label: string; + value: number; + data: number[]; + tone?: "signal" | "amber" | "vermilion"; +}) { + const color = { + signal: "var(--color-signal)", + amber: "var(--color-amber)", + vermilion: "var(--color-vermilion)", + }[tone]; + return ( + +
+ {icon} + + {label} + +
+
+ {value.toLocaleString()} +
+ +
+ ); +} diff --git a/services/frontend/src/app/(dashboard)/layout.tsx b/services/frontend/src/app/(dashboard)/layout.tsx index d06d1fa..8959c9d 100644 --- a/services/frontend/src/app/(dashboard)/layout.tsx +++ b/services/frontend/src/app/(dashboard)/layout.tsx @@ -7,25 +7,19 @@ import { ChatbotProvider, useChatbot, } from "@/components/chatbot/chatbot-context"; -import { AppSidebar } from "@/components/layout/app-sidebar"; +import { Spine } from "@/components/layout/spine"; +import { StatusBar } from "@/components/layout/status-bar"; import { MiniPlayer } from "@/components/media/mini-player"; +import { RouteTransition } from "@/components/motion/route-transition"; import { GuildSelector } from "@/components/shared/guild-selector"; -import { Separator } from "@/components/ui/separator"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; import { MediaPlayerProvider } from "@/lib/hooks/use-media-player"; import { useWebSocket, WsProvider } from "@/lib/ws/context"; function ChatbotGuildSync({ guildId }: { guildId: string }) { const { setGuildId } = useChatbot(); - useEffect(() => { setGuildId(guildId); }, [guildId, setGuildId]); - return null; } @@ -39,17 +33,12 @@ function ChatbotExpressionSync() { setTimeout(() => setExpression("idle"), 2000); } }); - - const unsub2 = ws.on("voice_active_user", () => { - setExpression("listening"); - }); - + const unsub2 = ws.on("voice_active_user", () => setExpression("listening")); return () => { unsub1(); unsub2(); }; }, [ws, setExpression]); - return null; } @@ -74,41 +63,27 @@ export default function DashboardLayout({ -
- - - -
- - -
Overview
-
- setGuildId(g ?? "")} - /> -
-
- -
-
- -
-
- } - > - {children} -
-
-
-
-
- +
+ +
+ setGuildId(g)} + /> +
+
+ +
+
+ } + > + {children} +
+
+
+
diff --git a/services/frontend/src/app/(dashboard)/media/view.tsx b/services/frontend/src/app/(dashboard)/media/view.tsx index 41daa67..e1fc536 100644 --- a/services/frontend/src/app/(dashboard)/media/view.tsx +++ b/services/frontend/src/app/(dashboard)/media/view.tsx @@ -1,7 +1,30 @@ "use client"; -import { MusicPlayer } from "@/components/media/music-player"; +import { + Pause, + Play, + Repeat2, + SkipForward, + Square, + Volume2, +} from "lucide-react"; +import { motion } from "motion/react"; +import { useEffect } from "react"; +import { Waveform } from "@/components/charts/waveform"; +import { StaggerGroup, StaggerItem } from "@/components/motion/stagger"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import { Progress } from "@/components/primitives/progress"; +import { + useMediaLoop, + useMediaSkip, + useMediaState, + useMediaStop, + useMediaWsSync, +} from "@/hooks"; import type { MediaState } from "@/lib/types"; +import { cn } from "@/lib/utils"; import { useWebSocket } from "@/lib/ws/context"; export default function MediaView({ @@ -10,10 +33,140 @@ export default function MediaView({ initialStatus?: MediaState; }) { const ws = useWebSocket(); + const { data: state, mutate } = useMediaState(initialStatus); + const skip = useMediaSkip(); + const stop = useMediaStop(); + const loopMut = useMediaLoop(); + useMediaWsSync(ws); + + const current = state?.current; + const playing = state?.playing ?? false; + const queue = state?.queue ?? []; + const loop = state?.loop ?? false; + + const duration = current?.durationMs ?? 0; + const [seed] = useWaveformSeed(); return ( -
- +
+ {/* Turntable hero */} +
+ {current && ( + + {current.title} + + )} +
+
+ {current?.title ?? "No track playing"} +
+
+ {current?.source ?? "idle"} · {duration ? formatMs(duration) : "—"} +
+
+ +
+ 0:00 + {duration ? formatMs(duration) : "—"} +
+
+
+
+ + {/* Transport */} + + + + + + + + + + + + + + + + + + + {/* Queue */} + {queue.length > 0 && ( +
+
+

Queue ({queue.length})

+ {loop ? "loop" : "queue"} +
+
+ {queue.map((item, i) => ( + + + {item.title} + + ))} +
+
+ )}
); } + +function formatMs(ms: number): string { + const m = Math.floor(ms / 60000); + const s = Math.floor((ms % 60000) / 1000); + return `${m}:${s.toString().padStart(2, "0")}`; +} + +function useWaveformSeed() { + const [seed] = useStateValue(); + return [seed]; +} +function useStateValue(): [string] { + // lightweight deterministic seed so waveform shape is stable per session + return ["media-waveform"]; +} diff --git a/services/frontend/src/app/(dashboard)/messages/page.tsx b/services/frontend/src/app/(dashboard)/messages/page.tsx index e94d6f3..f24417e 100644 --- a/services/frontend/src/app/(dashboard)/messages/page.tsx +++ b/services/frontend/src/app/(dashboard)/messages/page.tsx @@ -1,11 +1,8 @@ /** - * Messages page — Server Component. - * - * Reads the URL (guild/channel/tab/selected) on the server and, when a guild - * is already selected, fetches the first message page server-side so the - * initial list is server-rendered, not a client round-trip. + * Messages — Server Component. + * Reads URL guild/channel/selected/tab on the server; seeds first page SSR. */ -import { getMessages, type MessagePageResult } from "@/lib/api/server"; +import { getMessages } from "@/lib/api/server"; import MessagesView from "./view"; export default async function MessagesPage({ @@ -22,7 +19,7 @@ export default async function MessagesPage({ ? (sp.tab as "all" | "images" | "review") : "all"; - let initialPage: MessagePageResult | undefined; + let initialPage; if (guild) { initialPage = await getMessages(guild, channel || undefined).catch( () => undefined, diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index 0b8b5e7..91be505 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -1,24 +1,20 @@ "use client"; -import { Flag, Image, Loader2, Search } from "lucide-react"; +import { Flag, Image, Loader2, Search, Send, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; -import { SubNav } from "@/components/layout/sub-nav"; import { Lightbox } from "@/components/messages/lightbox"; -import { extractFirstImage } from "@/components/messages/message-card"; import { MessageDetailView } from "@/components/messages/message-detail-view"; import { MessageList } from "@/components/messages/message-list"; import { SearchOverlay } from "@/components/messages/search-overlay"; +import { StaggerGroup, StaggerItem } from "@/components/motion/stagger"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Dialog } from "@/components/primitives/dialog"; +import { Input } from "@/components/primitives/input"; +import { Select } from "@/components/primitives/select"; import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared"; import { GuildSelector } from "@/components/shared/guild-selector"; -import { Card } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { useImages, useLoadMore, @@ -44,11 +40,6 @@ interface MessagesViewProps { initialMessagePage?: { data: MessageRecord[]; nextCursor: string | null }; } -/** - * Messages view — hydrated on the client. Initial guild/channel/detail/tab - * come from the URL (server-read on first SSR), and the first message page is - * seeded from the server when a guild is already selected. - */ export default function MessagesView({ initialGuild = "", initialChannel = "", @@ -87,16 +78,11 @@ export default function MessagesView({ const loadMoreMut = useLoadMore(); const { data: images } = useImages(guildId); const { data: reviews } = useReview(selectedChannel || undefined); - - const { - message: detailMessage, - attachments: detailAttachments, - loading: detailLoading, - } = useMessageDetail(detailId); + const { message: detailMessage, loading: detailLoading } = + useMessageDetail(detailId); useMessagesWsSync(ws, guildId); - // Sync state to URL useEffect(() => { const params = new URLSearchParams(); if (guildId) params.set("guild", guildId); @@ -106,16 +92,16 @@ export default function MessagesView({ router.replace(`/messages?${params.toString()}`, { scroll: false }); }, [guildId, selectedChannel, detailId, tab, router]); - // Global Cmd+K search trigger + // global Cmd+K useEffect(() => { - const handleKey = (e: KeyboardEvent) => { + const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setSearchOpen(true); } }; - document.addEventListener("keydown", handleKey); - return () => document.removeEventListener("keydown", handleKey); + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); }, []); const handleLoadMore = useCallback(() => { @@ -133,154 +119,152 @@ export default function MessagesView({ setDetailId(null); }, []); - const subNavTabs = [ + const tabs: { id: MessagesTab; label: string; icon: React.ReactNode }[] = [ { id: "all", label: "All", icon: null }, - { id: "images", label: "Images", icon: }, - { id: "review", label: "Review", icon: }, + { id: "images", label: "Images", icon: }, + { id: "review", label: "Review", icon: }, ]; const currentMessages = messages ?? []; return ( -
- {/* ── Controls bar ── */} +
+ {/* Controls */}
{channels.length > 0 && ( )}
- {/* ── Sub navigation ── */} - setTab(t as MessagesTab)} - /> - - {/* ── Split pane ── */} - {error ? ( - - ) : !messages ? ( - - ) : ( -
- {/* Left pane */} -
+ {tabs.map((t) => ( +
+ {t.icon} + {t.label} + + ))} +
- {/* Right pane — message detail */} - {detailId && ( -
+
+ {/* Left — timeline spine + entries */} +
+ {error ? ( + + ) : !messages ? ( + + ) : tab === "all" ? ( + + ) : tab === "images" ? ( + + ) : ( + + )} +
+ + {/* Right — detail */} + {detailId && ( +
+
{detailLoading ? ( - - - +
+ +
) : detailMessage ? ( -
+
- { - const imgs = (detailAttachments ?? []) - .filter((a) => a.type?.startsWith("image/")) - .map((a) => ({ - src: a.uploaded_url || a.discord_url, - alt: a.filename, - })); - if (imgs.length > 0) { - setLightbox({ images: imgs, index }); - } - }} - /> + + {detailMessage && ( + setLightbox(null)} + images={extractImages(detailMessage.metadata)} + /> + )}
) : null}
- )} -
- )} +
+ )} +
- {/* ── Search overlay ── */} setSearchOpen(false)} - onSelect={(id) => { - setDetailId(id); + results={(currentMessages ?? []).map((m) => ({ + id: m.id, + content: m.edited_content ?? m.content, + username: m.username ?? "unknown", + channel: m.channel_id, + time: m.created_at + ? new Date(m.created_at * 1000).toLocaleTimeString() + : "", + }))} + onSelect={(msg) => { + const found = currentMessages.find((m) => m.id === msg.id); + if (found) setDetailId(found.id); setTab("all"); }} /> - {/* ── Lightbox ── */} {lightbox && ( setLightbox(null)} images={lightbox.images} initialIndex={lightbox.index} - open - onClose={() => setLightbox(null)} /> )}
); } -// ── Inline ImageGrid (glass-styled) ──────────────── - function ImageGrid({ items, onSelect, @@ -288,46 +272,42 @@ function ImageGrid({ items: MessageRecord[]; onSelect: (id: string) => void; }) { - return ( -
+ return !items.length ? ( + + ) : ( +
{items.map((item) => { - const imgUrl = extractFirstImage(item.metadata); + const url = extractFirstImage(item.metadata); return ( ); })} - {items.length === 0 && ( - - )}
); } -// ── Inline ReviewList (glass-styled) ──────────────── - function ReviewList({ items, onSelect, @@ -335,36 +315,63 @@ function ReviewList({ items: MessageRecord[]; onSelect: (id: string) => void; }) { - return ( -
+ return !items.length ? ( + + ) : ( + {items.map((item) => ( - onSelect(item.id)} - > -
- -
-

- {renderMessageContent(item.content, item.metadata) || item.id} -

-
-
-
+ + + ))} - {items.length === 0 && ( - - )} -
+ ); } + +function extractFirstImage(metadata?: string | null): string | null { + try { + if (!metadata) return null; + const m = JSON.parse(metadata); + const atts = m?.attachments ?? []; + const img = atts.find( + (a: { + contentType?: string | null; + url?: string; + discord_url?: string; + }) => /image/i.test(a.contentType ?? ""), + ); + return img?.url ?? img?.discord_url ?? null; + } catch { + return null; + } +} + +function extractImages(metadata?: string | null) { + try { + if (!metadata) return []; + const m = JSON.parse(metadata); + return (m?.attachments ?? []) + .filter((a: { contentType?: string | null }) => + /image/i.test(a.contentType ?? ""), + ) + .map((a: { url?: string; discord_url?: string; name?: string }) => ({ + src: a.url ?? a.discord_url ?? "", + alt: a.name, + })); + } catch { + return []; + } +} diff --git a/services/frontend/src/app/(dashboard)/moderation/page.tsx b/services/frontend/src/app/(dashboard)/moderation/page.tsx index 4a78db9..933293e 100644 --- a/services/frontend/src/app/(dashboard)/moderation/page.tsx +++ b/services/frontend/src/app/(dashboard)/moderation/page.tsx @@ -1,24 +1,25 @@ /** - * Moderation page — Server Component. Seeds summary + action log from - * server-fetched moderation state (shared across all users). + * Moderation — Server Component. + * Seeds moderation stats + action log for SSR first paint; live via WS. */ -import { ModerationSection } from "@/components/moderation/moderation-section"; import { getModerationActions, getModerationStats } from "@/lib/api/server"; +import ModerationView from "./view"; export default async function ModerationPage() { const [stats, actions] = await Promise.allSettled([ - getModerationStats(), - getModerationActions(100), + getModerationStats().catch(() => undefined), + getModerationActions(100).catch(() => undefined), ]); - return ( -
- -
+ ); } diff --git a/services/frontend/src/app/(dashboard)/moderation/view.tsx b/services/frontend/src/app/(dashboard)/moderation/view.tsx new file mode 100644 index 0000000..d98b833 --- /dev/null +++ b/services/frontend/src/app/(dashboard)/moderation/view.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { ModerationSection } from "@/components/moderation/moderation-section"; +import type { ModerationAction, ModerationStats } from "@/lib/types"; + +export default function ModerationView({ + initialStats, + initialActions, +}: { + initialStats?: ModerationStats; + initialActions?: ModerationAction[]; +}) { + return ( + + ); +} diff --git a/services/frontend/src/app/(dashboard)/recordings/page.tsx b/services/frontend/src/app/(dashboard)/recordings/page.tsx index cb14eff..571a4c2 100644 --- a/services/frontend/src/app/(dashboard)/recordings/page.tsx +++ b/services/frontend/src/app/(dashboard)/recordings/page.tsx @@ -1,12 +1,12 @@ /** - * Recordings page — Server Component. Seeds the library from server-fetched - * recordings; live `voice_recording_uploaded` events keep it fresh over WS. + * Recordings — Server Component. + * Seeds the library from server-fetched recordings; live `voice_recording_uploaded` + * events (synced in the client View via WS) keep it fresh. */ import { getRecordings } from "@/lib/api/server"; import RecordingsView from "./view"; export default async function RecordingsPage() { const data = await getRecordings(50).catch(() => undefined); - return ; } diff --git a/services/frontend/src/app/(dashboard)/recordings/view.tsx b/services/frontend/src/app/(dashboard)/recordings/view.tsx index 71e2988..f23053e 100644 --- a/services/frontend/src/app/(dashboard)/recordings/view.tsx +++ b/services/frontend/src/app/(dashboard)/recordings/view.tsx @@ -1,189 +1,200 @@ "use client"; -import { Clock, Database, Mic, Users } from "lucide-react"; -import { useMemo, useRef, useState } from "react"; -import { StatCard } from "@/components/dashboard/stat-card"; -import { SubNav } from "@/components/layout/sub-nav"; -import { RecordingCard } from "@/components/recordings/recording-card"; -import { RecordingPlayer } from "@/components/recordings/recording-player"; -import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/shared"; -import { useRecordings, useRecordingsWsSync } from "@/hooks"; -import { formatBytes } from "@/lib/format"; +import { Delete, Download, Play } from "lucide-react"; +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useState } from "react"; +import { Waveform } from "@/components/charts/waveform"; +import { StaggerGroup, StaggerItem } from "@/components/motion/stagger"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import { Dialog } from "@/components/primitives/dialog"; +import { + useDeleteRecording, + useRecordings, + useRecordingsWsSync, +} from "@/hooks"; import type { VoiceRecording } from "@/lib/types"; +import { cn } from "@/lib/utils"; import { useWebSocket } from "@/lib/ws/context"; -type RecordingsTab = "library" | "stats"; +interface RecordingsListProps { + recordings: VoiceRecording[]; + error: Error | null; + isLoading: boolean; + deleting: string | null; + onSelect: (rec: VoiceRecording) => void; + onDelete: (rec: VoiceRecording) => void; + preview: VoiceRecording | null; + onClosePreview: () => void; +} + +function RecordingsList({ + recordings, + error, + isLoading, + deleting, + onSelect, + onDelete, + preview, + onClosePreview, +}: RecordingsListProps) { + if (isLoading) { + return ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ ); + } + if (error) + return ( +

+ Failed to load: {error.message} +

+ ); + + return ( +
+ + {recordings.map((rec) => ( + + + + +
+
+ + {rec.username ?? "unknown"} + + + .{rec.filename.split(".").pop() ?? "mp3"} + +
+
+ {(rec.size_bytes / 1024).toFixed(0)} KB ·{" "} + {new Date(rec.created_at * 1000).toLocaleTimeString()} +
+
+
+ {rec.download_url && ( + <> + + + + + + )} + +
+
+
+ ))} +
+ +
+ ); +} + +function PreviewDialog({ + open, + onClose, + recording, +}: { + open: boolean; + onClose: () => void; + recording: VoiceRecording | null; +}) { + if (!recording) return null; + return ( + +
+
+ {recording.filename} +
+
+
+ ); +} export default function RecordingsView({ initialRecordings, }: { initialRecordings?: VoiceRecording[]; }) { - const { - data: recordings, - error, - mutate: refetch, - } = useRecordings(initialRecordings); - const [playingId, setPlayingId] = useState(null); - const [isPlaying, setIsPlaying] = useState(false); - const [isLoadingAudio, setIsLoadingAudio] = useState(false); - const [tab, setTab] = useState("library"); const ws = useWebSocket(); - const audioRef = useRef(null); - - // Live-update the library when the gateway publishes voice_recording_uploaded + const { + data: recordings = [], + error, + isLoading, + } = useRecordings(initialRecordings); + const del = useDeleteRecording(); + const [deleting, setDeleting] = useState(null); useRecordingsWsSync(ws); - const currentTrack = - playingId && recordings - ? recordings.find((r: VoiceRecording) => r.id === playingId) - : null; + const handleDelete = useCallback( + (rec: VoiceRecording) => { + setDeleting(rec.id); + del.mutate(rec.id); + setTimeout(() => setDeleting(null), 800); + }, + [del], + ); - const togglePlay = (id: string) => { - if (playingId !== id) { - setPlayingId(id); // RecordingPlayer picks up the new url + autoplays - } else { - const audio = audioRef.current; - if (!audio) return; - if (audio.paused) audio.play().catch(() => {}); - else audio.pause(); - } - }; - - const stats = useMemo(() => { - const list = recordings ?? []; - const totalSize = list.reduce((sum, r) => sum + (r.size_bytes ?? 0), 0); - const byUser = new Map< - string, - { name: string; count: number; size: number } - >(); - for (const rec of list) { - const key = rec.user_id ?? rec.username; - const cur = byUser.get(key) ?? { name: rec.username, count: 0, size: 0 }; - cur.count += 1; - cur.size += rec.size_bytes ?? 0; - byUser.set(key, cur); - } - const topUsers = [...byUser.values()] - .sort((a, b) => b.count - a.count) - .slice(0, 8); - return { - total: list.length, - totalSize, - uniqueUsers: byUser.size, - topUsers, - }; - }, [recordings]); + const [preview, setPreview] = useState(null); return ( -
- setTab(t as RecordingsTab)} - /> - - {tab === "library" && - (error ? ( - - ) : !recordings ? ( - - ) : ( -
- {(recordings ?? []).map((rec: VoiceRecording) => ( - - ))} - {(recordings ?? []).length === 0 && ( - - )} -
- ))} - - {tab === "stats" && - (!recordings ? ( - - ) : stats.total === 0 ? ( - - ) : ( -
-
- - formatBytes(v)} - /> - -
- - {stats.topUsers.length > 0 && ( -
-

- Top Speakers -

-
- {stats.topUsers.map((u) => ( -
- - {u.count} - - - {u.name} - - - {formatBytes(u.size)} - -
- ))} -
-
- )} -
- ))} - - togglePlay(playingId!)} - onStateChange={(s) => { - setIsPlaying(s.playing); - setIsLoadingAudio(s.loading); - }} - onClose={() => setPlayingId(null)} - /> -
+ setPreview(null)} + /> ); } diff --git a/services/frontend/src/app/(dashboard)/voice/page.tsx b/services/frontend/src/app/(dashboard)/voice/page.tsx index f2158a5..a36d4ea 100644 --- a/services/frontend/src/app/(dashboard)/voice/page.tsx +++ b/services/frontend/src/app/(dashboard)/voice/page.tsx @@ -1,23 +1,13 @@ /** - * Voice page — Server Component. - * - * Fetches the authoritative voice connection status + guild list on the server - * so the first paint reflects the shared gateway voice state (which channel is - * joined, across ALL users), independent of any single browser's WS history. + * Voice — Server Component. + * Seeds authoritative voice status (shared active speakers snapshot) on the + * server, then hands off to the client View for the 3D scene + WS live updates. */ -import { getGuilds, getVoiceStatus } from "@/lib/api/server"; +import { getVoiceStatus } from "@/lib/api/server"; +import type { VoiceStatus } from "@/lib/types"; import VoiceView from "./view"; export default async function VoicePage() { - const [status, guilds] = await Promise.allSettled([ - getVoiceStatus(), - getGuilds(), - ]); - - return ( - - ); + const status = await getVoiceStatus().catch(() => undefined); + return ; } diff --git a/services/frontend/src/app/(dashboard)/voice/view.tsx b/services/frontend/src/app/(dashboard)/voice/view.tsx index 50fb7ae..6f8fb2c 100644 --- a/services/frontend/src/app/(dashboard)/voice/view.tsx +++ b/services/frontend/src/app/(dashboard)/voice/view.tsx @@ -1,177 +1,176 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; -import { SubNav } from "@/components/layout/sub-nav"; -import { VoiceActivityTimeline } from "@/components/voice/activity-timeline"; -import { VoiceConnectionCard } from "@/components/voice/connection-card"; +import { + Activity, + Headphones, + Mic, + MicOff, + Pause, + Play, + Settings, + Wifi, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { SessionRibbon } from "@/components/charts/session-ribbon"; +import { StaggerGroup, StaggerItem } from "@/components/motion/stagger"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import { SignalField } from "@/components/three"; +import { StaticFallback } from "@/components/three/static-fallback"; +import { WebGLGuard } from "@/components/three/webgl-guard"; +import { ActiveSpeakersPanel } from "@/components/voice/active-speakers-panel"; import { ListenControl } from "@/components/voice/listen-control"; import { MicControl } from "@/components/voice/mic-control"; import { SpeakerWaveform } from "@/components/voice/speaker-waveform"; import { - useGuilds, - useMicTransmit, useSpeakers, - useVoiceChannels, useVoiceConnect, useVoiceDisconnect, useVoiceListen, - useVoiceStatus, } from "@/hooks"; -import type { Guild, VoiceStatus } from "@/lib/types"; +import type { ActiveSpeaker, VoiceStatus } from "@/lib/types"; +import { cn } from "@/lib/utils"; import { useWebSocket } from "@/lib/ws/context"; -type VoiceTab = "connection" | "activity"; +type VoiceTab = "stage" | "activity"; -/** - * Voice view — hydrated on the client. Seeded from server-rendered status + - * guild list so every user's first paint reflects the same shared voice - * connection state; live updates come over WS. - */ export default function VoiceView({ initialStatus, - initialGuilds = [], }: { initialStatus?: VoiceStatus; - initialGuilds?: Guild[]; }) { const ws = useWebSocket(); - const { data: voiceStatus } = useVoiceStatus(initialStatus); - const { data: guilds = [] } = useGuilds(initialGuilds); - const [selectedGuild, setSelectedGuild] = useState(""); - const { data: voiceChannels = [] } = useVoiceChannels(selectedGuild); - const { speakers, subscribe } = useSpeakers(initialStatus?.activeSpeakers); - const connectMut = useVoiceConnect(); - const disconnectMut = useVoiceDisconnect(); - const micMut = useMicTransmit(ws); + const [tab, setTab] = useState("stage"); + + const initialSpeakers = useMemo( + () => initialStatus?.activeSpeakers ?? [], + [initialStatus], + ); + const { speakers, subscribe } = useSpeakers(initialSpeakers); + const connect = useVoiceConnect(); + const disconnect = useVoiceDisconnect(); const listen = useVoiceListen(ws); - const [selectedChannel, setSelectedChannel] = useState(""); - const [micActive, setMicActive] = useState(false); - const [volume, setVolume] = useState(75); - const [listenVolume, setListenVolume] = useState(75); - const [tab, setTab] = useState("connection"); useEffect(() => { const unsub = subscribe(ws); - return () => unsub(); - }, [ws, subscribe]); + return unsub; + }, [subscribe, ws]); - const handleMicToggle = useCallback( - async (checked: boolean) => { - if (checked) { - try { - await micMut.mutateAsync(true); - setMicActive(true); - } catch { - setMicActive(false); - } - } else { - setMicActive(false); - try { - await micMut.mutateAsync(false); - } catch { - // Stop already tore down the local transmitter — ignore remote errors - } - } - }, - [micMut], - ); - - const handleVolumeChange = useCallback( - (v: number) => { - setVolume(v); - micMut.setVolume(v); - }, - [micMut], - ); - - const handleGuildChange = useCallback((guildId: string | null) => { - if (!guildId) { - setSelectedGuild(""); - setSelectedChannel(""); - return; - } - setSelectedGuild(guildId); - }, []); - - const activeSpeakers = speakers.filter((s) => s.speaking); - const connected = voiceStatus?.connected ?? false; + const active = speakers.filter((s) => s.speaking); return ( -
- setTab(t as VoiceTab)} - /> +
+ {/* Connection bar */} +
+ + {initialStatus?.connected ? "Connected" : "Disconnected"} + + {initialStatus?.activeChannelName && ( + + #{initialStatus.activeChannelName} + + )} + {initialStatus?.connected ? ( + + ) : ( + + )} +
- setSelectedChannel(v ?? "")} - onConnect={() => { - void connectMut - .mutateAsync({ - guildId: selectedGuild, - channelId: selectedChannel, - }) - .catch((err: unknown) => { - const msg = - err instanceof Error - ? err.message - : "Gagal connect ke voice channel"; - toast.error("Voice connect gagal", { - description: msg, - }); - }); - }} - onDisconnect={() => { - if (micActive) { - setMicActive(false); - void micMut.mutateAsync(false).catch(() => {}); + {/* Stage hero */} +
+ } - if (listen.active) listen.toggle(false); - disconnectMut.mutate(undefined); - }} - connecting={connectMut.isPending} - /> + > + 0 ? 0.6 : 0.2} + className="absolute inset-0" + /> + +
+ +
+
- {tab === "connection" && ( -
- -
- listen.toggle(on)} - volume={listenVolume} - onVolumeChange={(v) => { - setListenVolume(v); - listen.setVolume(v); - }} - /> - -
+ + + listen.toggle(on)} + levels={listen.levels} + /> + + + listen.toggle(on)} + volume={75} + onVolume={listen.setVolume} + /> + + + + {/* Tabs */} +
+ {(["stage", "activity"] as const).map((t) => ( + + ))} +
+ + {tab === "stage" && } + {tab === "activity" && ( +
+

Live session timeline

+ ({ + id: s.userId, + label: s.username, + value: s.speaking ? 3 : 1, + tone: s.speaking ? "signal" : "neutral", + }))} + />
)} - - {tab === "activity" && }
); } diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css index 8b14898..6dca6d7 100644 --- a/services/frontend/src/app/globals.css +++ b/services/frontend/src/app/globals.css @@ -1,327 +1,270 @@ @import "tailwindcss"; -@import "tw-animate-css"; -@import "shadcn/tailwind.css"; @custom-variant dark (&:is(.dark *)); /* - * Theme variables — light mode lives in :root (default), dark mode overrides - * via `.dark`. Toggle in TopNav swaps the class on . + * GMW — new design system (visual overhaul). * - * NOTE: this is `@theme` (NOT `@theme inline`) on purpose — inline inlines - * literal values into utilities, so a `.dark` class override of the custom - * property would NOT re-skin utilities. With plain `@theme`, utilities - * reference `var(--color-*)`, so runtime theme switching works. + * Replaces the old teal-cyan + purple + glassmorphism language with a warm, + * signal-driven ops-console aesthetic. Hierarchy comes from scale/weight and + * tonal surface blocks, NOT from borders/shadows. Three semantic signals: + * lime = OK / live (--signal) + * amber = warn (--amber) + * vermilion = flag/danger (--vermilion) */ + @theme { - /* ── Light (default) ── */ - /* Canvas — soft off-white */ - --color-canvas: oklch(0.97 0.005 250); - --color-surface: oklch(1 0 0 / 0.7); - --color-surface-hover: oklch(0.93 0.01 250 / 0.8); + /* ── Surfaces ── */ + --color-canvas: oklch(0.96 0.012 80); + --color-surface: oklch(0.92 0.014 80); + --color-surface-2: oklch(0.88 0.016 80); - /* Glass */ - --color-glass-bg: oklch(0 0 0 / 0.04); - --color-glass-border: oklch(0.2 0.02 250 / 0.12); - --glass-shadow: 0 8px 32px oklch(0.2 0.02 250 / 0.12); + /* ── Ink ── */ + --color-ink: oklch(0.22 0.02 70); + --color-ink-soft: oklch(0.46 0.02 70); - /* Primary — teal-cyan */ - --color-primary: oklch(0.52 0.17 215); - --color-primary-glow: oklch(0.52 0.17 215 / 0.35); - --color-primary-foreground: oklch(0.98 0 0); - --color-border: oklch(0.2 0.02 250 / 0.08); - --color-border-glow: oklch(0.52 0.17 215 / 0.35); + /* ── Structural ── */ + --color-hairline: oklch(0.22 0.02 70 / 0.1); + --hairline-w: 1px; - /* Accents */ - --color-accent-purple: oklch(0.55 0.2 280); - --color-accent-amber: oklch(0.65 0.17 75); - --color-destructive: oklch(0.577 0.245 27.325); - --color-success: oklch(0.55 0.18 160); + /* ── Semantic signals ── */ + --color-signal: oklch(0.78 0.17 125); + --color-signal-ink: oklch(0.20 0.03 70); + --color-signal-glow: oklch(0.78 0.17 125 / 0.35); + --color-amber: oklch(0.80 0.15 70); + --color-vermilion: oklch(0.62 0.21 25); + --color-vermilion-soft: oklch(0.62 0.21 25 / 0.15); - /* Text */ - --color-text-primary: oklch(0.25 0.02 250); - --color-text-secondary: oklch(0.48 0.02 250); - --color-text-mono: oklch(0.52 0.17 215); + --color-ring: var(--color-signal); - /* Legacy overrides for shadcn compatibility */ - --color-background: var(--color-canvas); - --color-foreground: var(--color-text-primary); - --color-card: var(--color-surface); - --color-card-foreground: var(--color-text-primary); - --color-muted: oklch(0.9 0.01 250); - --color-muted-foreground: var(--color-text-secondary); - --color-accent: var(--color-primary); - --color-accent-foreground: var(--color-primary-foreground); + /* ── Fonts ── */ + --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, monospace; + --font-display: "Bricolage Grotesque", "Inter", sans-serif; - /* Ring / focus outline for shadcn outline-ring utility */ - --color-ring: var(--color-primary); - - /* Base-ui / shadcn primitives */ - --color-popover: oklch(0.99 0.005 250 / 0.95); - --color-popover-foreground: var(--color-text-primary); - --color-input: oklch(0.55 0.02 250 / 0.35); - --color-secondary: oklch(0.92 0.01 250 / 0.6); - --color-secondary-foreground: var(--color-text-primary); - - /* Sidebar (shadcn) */ - --color-sidebar: oklch(1 0 0 / 0.6); - --color-sidebar-foreground: var(--color-text-primary); - --color-sidebar-primary: var(--color-primary); - --color-sidebar-primary-foreground: var(--color-primary-foreground); - --color-sidebar-accent: oklch(0.93 0.01 250 / 0.7); - --color-sidebar-accent-foreground: var(--color-text-primary); - --color-sidebar-border: var(--color-border); - --color-sidebar-ring: var(--color-ring); - - /* Radius */ - --radius-card: 16px; - --radius-panel: 12px; - --radius-control: 8px; - --radius-pill: 9999px; - --radius: 0.625rem; /* shadcn compat */ - - /* Fonts */ - --font-sans: "Inter", sans-serif; - --font-mono: "JetBrains Mono", monospace; + /* ── Radii ── */ + --radius-r: 14px; + --radius-r-panel: 12px; + --radius-r-control: 8px; + --radius-r-pill: 9999px; } /* ── Dark theme overrides ── */ .dark { - --color-canvas: oklch(0.07 0.015 250); - --color-surface: oklch(0.11 0.02 245 / 0.6); - --color-surface-hover: oklch(0.15 0.02 245 / 0.7); + --color-canvas: oklch(0.13 0.015 70); + --color-surface: oklch(0.18 0.02 70); + --color-surface-2: oklch(0.23 0.022 70); - --color-glass-bg: oklch(1 0 0 / 0.04); - --color-glass-border: oklch(1 0 0 / 0.08); - --glass-shadow: 0 8px 32px oklch(0 0 0 / 0.4); + --color-ink: oklch(0.93 0.01 75); + --color-ink-soft: oklch(0.62 0.02 75); - --color-primary: oklch(0.62 0.17 215); - --color-primary-glow: oklch(0.62 0.17 215 / 0.4); - --color-primary-foreground: oklch(0.98 0 0); - --color-border: oklch(1 0 0 / 0.06); - --color-border-glow: oklch(0.62 0.17 215 / 0.3); + --color-hairline: oklch(1 0 0 / 0.09); - --color-accent-purple: oklch(0.65 0.2 280); - --color-accent-amber: oklch(0.7 0.17 75); - --color-destructive: oklch(0.577 0.245 27.325); - --color-success: oklch(0.6 0.18 160); - - --color-text-primary: oklch(0.93 0.01 245); - --color-text-secondary: oklch(0.55 0.02 245); - --color-text-mono: oklch(0.62 0.17 215); - - --color-background: var(--color-canvas); - --color-foreground: var(--color-text-primary); - --color-card: var(--color-surface); - --color-card-foreground: var(--color-text-primary); - --color-muted: oklch(0.17 0.015 245); - --color-muted-foreground: var(--color-text-secondary); - --color-accent: var(--color-primary); - --color-accent-foreground: var(--color-primary-foreground); - --color-ring: var(--color-primary); - - /* Base-ui / shadcn primitives */ - --color-popover: oklch(0.13 0.02 245 / 0.96); - --color-popover-foreground: var(--color-text-primary); - --color-input: oklch(1 0 0 / 0.18); - --color-secondary: oklch(0.2 0.02 245 / 0.7); - --color-secondary-foreground: var(--color-text-primary); - - /* Sidebar (shadcn) */ - --color-sidebar: oklch(0.11 0.02 245 / 0.55); - --color-sidebar-foreground: var(--color-text-primary); - --color-sidebar-primary: var(--color-primary); - --color-sidebar-primary-foreground: var(--color-primary-foreground); - --color-sidebar-accent: oklch(0.17 0.02 245 / 0.7); - --color-sidebar-accent-foreground: var(--color-text-primary); - --color-sidebar-border: var(--color-border); - --color-sidebar-ring: var(--color-ring); -} - -@layer utilities { - .glass { - background: var(--color-glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-glass-border); - box-shadow: var(--glass-shadow); - } - .glass-elevated { - background: var(--color-glass-bg); - backdrop-filter: blur(16px); - border: 1px solid var(--color-border-glow); - box-shadow: 0 8px 32px var(--glass-shadow), 0 0 20px var(--color-primary-glow); - } - .glass-intense { - background: oklch(1 0 0 / 0.75); - backdrop-filter: blur(20px); - border: 1px solid oklch(0.2 0.02 250 / 0.12); - box-shadow: 0 8px 32px oklch(0.2 0.02 250 / 0.12); - } -} - -/* Dark variant of glass-intense (reads better on dark canvas) */ -.dark .glass-intense { - background: oklch(1 0 0 / 0.08); - border: 1px solid oklch(1 0 0 / 0.12); - box-shadow: none; + --color-signal: oklch(0.88 0.18 125); + --color-signal-ink: oklch(0.18 0.03 70); + --color-signal-glow: oklch(0.88 0.18 125 / 0.4); + --color-amber: oklch(0.85 0.15 70); + --color-vermilion: oklch(0.68 0.21 25); + --color-vermilion-soft: oklch(0.68 0.21 25 / 0.18); } @layer base { - * { @apply border-border; } + * { + @apply border-transparent; + } + + html { + @apply font-sans antialiased scroll-smooth; + } + body { - @apply bg-canvas text-text-primary font-sans antialiased; + @apply bg-canvas text-ink font-sans antialiased; + /* warm dot-grid texture + faint glow — replaces old bluish radial layers */ background-image: - radial-gradient(circle, oklch(0.3 0.02 250 / 0.05) 1px, transparent 1px), - radial-gradient(ellipse 80% 50% at 50% -20%, oklch(0.52 0.17 215 / 0.08), transparent), - radial-gradient(ellipse 50% 40% at 80% 80%, oklch(0.55 0.2 280 / 0.05), transparent); - background-size: 24px 24px, 100% 100%, 100% 100%; + radial-gradient(circle, oklch(0.45 0.03 70 / 0.05) 1px, transparent 1px), + radial-gradient(ellipse 80% 50% at 50% -20%, oklch(0.78 0.17 125 / 0.06), transparent), + radial-gradient(ellipse 50% 40% at 85% 90%, oklch(0.80 0.15 70 / 0.04), transparent); + background-size: 26px 26px, 100% 100%, 100% 100%; } - /* Dark body background */ + .dark body { background-image: - radial-gradient(circle, oklch(1 0 0 / 0.025) 1px, transparent 1px), - radial-gradient(ellipse 80% 50% at 50% -20%, oklch(0.62 0.17 215 / 0.06), transparent), - radial-gradient(ellipse 50% 40% at 80% 80%, oklch(0.65 0.2 280 / 0.04), transparent); - background-size: 24px 24px, 100% 100%, 100% 100%; - } - html { - @apply font-sans scroll-smooth; + radial-gradient(circle, oklch(1 0 0 / 0.022) 1px, transparent 1px), + radial-gradient(ellipse 80% 50% at 50% -20%, oklch(0.88 0.18 125 / 0.05), transparent), + radial-gradient(ellipse 50% 40% at 85% 90%, oklch(0.85 0.15 70 / 0.03), transparent); + background-size: 26px 26px, 100% 100%, 100% 100%; } - /* Custom selection color */ ::selection { - background: oklch(0.62 0.17 215 / 0.4); + background: oklch(0.78 0.17 125 / 0.35); color: inherit; } - /* Scrollbar styling */ + /* Scrollbar — warm */ ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 7px; + height: 7px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { - background: oklch(0.4 0.02 250 / 0.2); + background: oklch(0.4 0.02 70 / 0.22); border-radius: 999px; } ::-webkit-scrollbar-thumb:hover { - background: oklch(0.4 0.02 250 / 0.35); + background: oklch(0.4 0.02 70 / 0.38); + } + + :focus-visible { + outline: 2px solid var(--color-signal); + outline-offset: 2px; } - ::-webkit-scrollbar { width: 6px; height: 6px; } - ::-webkit-scrollbar-track { background: transparent; } - ::-webkit-scrollbar-thumb { background: oklch(0.4 0.02 250 / 0.2); border-radius: 999px; } - ::-webkit-scrollbar-thumb:hover { background: oklch(0.4 0.02 250 / 0.35); } } -/* ── Animations ──────────────────────────── */ +@layer utilities { + /* Tonal block that replaces bordered cards */ + .surface { + background: var(--color-surface); + border-radius: var(--radius-r); + border: var(--hairline-w) solid var(--color-hairline); + } + .surface-2 { + background: var(--color-surface-2); + border-radius: var(--radius-r-panel); + border: var(--hairline-w) solid var(--color-hairline); + } + /* 1px animated pulse line marking a live/section header */ + .scan-tick { + position: relative; + overflow: hidden; + } + .scan-tick::after { + content: ""; + position: absolute; + inset-inline-start: 0; + inset-block-start: 0; + block-size: 1px; + inline-size: 100%; + background: linear-gradient( + 90deg, + transparent, + var(--color-signal) 20%, + var(--color-signal) 80%, + transparent + ); + background-size: 200% 100%; + animation: scan 2.6s linear infinite; + opacity: 0.7; + } + + .ticker { + background: var(--color-surface); + border-radius: var(--radius-r); + border: var(--hairline-w) solid var(--color-hairline); + padding: clamp(1rem, 2vw, 1.5rem); + } + + .pill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.2rem 0.7rem; + border-radius: var(--radius-r-pill); + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.02em; + } + + .mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; + } + + .display { + font-family: var(--font-display); + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1.02; + } + + /* text tone helpers */ + .text-signal { color: var(--color-signal); } + .text-amber { color: var(--color-amber); } + .text-vermilion { color: var(--color-vermilion); } + .text-ink-soft { color: var(--color-ink-soft); } + + /* focus ring helper for interactive blocks */ + .ring-focus { + transition: box-shadow 0.18s ease; + } + .ring-focus:hover { + box-shadow: 0 0 0 1px var(--color-signal-glow); + } +} + +/* ── Keyframes ──────────────────────────── */ +@keyframes scan { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} +@keyframes eq { + 0%, 100% { transform: scaleY(0.25); } + 30% { transform: scaleY(1); } + 60% { transform: scaleY(0.5); } +} +@keyframes fade-up { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes spin-disc { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +/* kept for live status dots */ @keyframes pulse-ring { - 0% { - transform: scale(0.8); - opacity: 1; - } - 100% { - transform: scale(2.5); - opacity: 0; - } + 0% { transform: scale(0.8); opacity: 1; } + 100% { transform: scale(2.5); opacity: 0; } } - -@keyframes fade-in-up { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - @keyframes shimmer { - 0% { - background-position: -200% 0; - } - 100% { - background-position: 200% 0; - } + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } } -.animate-fade-in-up { - animation: fade-in-up 0.3s ease-out forwards; +.animate-eq { + animation: eq 0.9s ease-in-out infinite; + transform-origin: bottom; +} +.animate-spin-disc { + animation: spin-disc 8s linear infinite; +} +.animate-spin-disc.paused { + animation-play-state: paused; } - .animate-pulse-ring { animation: pulse-ring 1.5s ease-out infinite; } - .animate-shimmer { - background: linear-gradient(90deg, transparent, oklch(0.62 0.17 215 / 0.08), transparent); + background: linear-gradient( + 90deg, + transparent, + oklch(0.78 0.17 125 / 0.08), + transparent + ); background-size: 200% 100%; animation: shimmer 1.5s infinite; } -/* Equalizer bars — bouncing heights for the "now playing" waveform */ -@keyframes eq-bounce { - 0%, - 100% { - transform: scaleY(0.25); - } - 30% { - transform: scaleY(1); - } - 60% { - transform: scaleY(0.5); +/* ── Reduced motion: kill all decorative animation ── */ +@media (prefers-reduced-motion: reduce) { + .scan-tick::after, + .animate-eq, + .animate-spin-disc, + .animate-pulse-ring, + .animate-shimmer { + animation: none !important; } -} -.animate-eq { - animation: eq-bounce 0.9s ease-in-out infinite; - transform-origin: bottom; -} - -/* Soft pulsing glow for the active/loading card */ -@keyframes card-glow { - 0%, - 100% { - box-shadow: 0 0 0 0 oklch(0.62 0.17 215 / 0); - } - 50% { - box-shadow: 0 0 22px 0 oklch(0.62 0.17 215 / 0.25); - } -} -.animate-card-glow { - animation: card-glow 1.8s ease-in-out infinite; -} - -/* ── Utility classes ─────────────────────── */ - -/* Gradient text */ -.text-gradient { - background: linear-gradient(135deg, oklch(0.62 0.17 215), oklch(0.6 0.15 195), oklch(0.65 0.12 185)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* Gradient border (via pseudo-element trick) */ -.gradient-border { - position: relative; -} -.gradient-border::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - padding: 1px; - background: linear-gradient(135deg, oklch(0.62 0.17 215), oklch(0.6 0.15 195), oklch(0.65 0.12 185)); - -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; -} - -/* Live pulse ring (compat alias) */ -.live-pulse-ring { - animation: pulse-ring 1.5s ease-out infinite; + html { scroll-behavior: auto; } } diff --git a/services/frontend/src/app/layout.tsx b/services/frontend/src/app/layout.tsx index 230eb75..4e68124 100644 --- a/services/frontend/src/app/layout.tsx +++ b/services/frontend/src/app/layout.tsx @@ -1,22 +1,31 @@ import type { Metadata } from "next"; -import { Inter, JetBrains_Mono } from "next/font/google"; +import { Bricolage_Grotesque, Inter, JetBrains_Mono } from "next/font/google"; import { ThemeProvider } from "next-themes"; -import { Toaster } from "@/components/ui/sonner"; +import { Toaster } from "@/components/primitives/toast"; import "./globals.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-inter", + display: "swap", }); const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains-mono", + display: "swap", +}); + +const bricolage = Bricolage_Grotesque({ + subsets: ["latin"], + variable: "--font-display", + display: "swap", }); export const metadata: Metadata = { - title: "Discord Automod — Moderation Dashboard", - description: "AI-powered Discord moderation and voice monitoring dashboard", + title: "Bete — Discord Moderation Console", + description: + "AI-powered Discord moderation, voice monitoring, and media control console", }; export default function RootLayout({ @@ -27,7 +36,7 @@ export default function RootLayout({ return ( @@ -39,8 +48,8 @@ export default function RootLayout({ disableTransitionOnChange > {children} + - ); diff --git a/services/frontend/src/components/analysis/search-panel.tsx b/services/frontend/src/components/analysis/search-panel.tsx index 44ead66..7260af0 100644 --- a/services/frontend/src/components/analysis/search-panel.tsx +++ b/services/frontend/src/components/analysis/search-panel.tsx @@ -2,17 +2,14 @@ import { Loader2, Search, Sparkles } from "lucide-react"; import { useCallback, useState } from "react"; - +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import { Input } from "@/components/primitives/input"; +import { Progress } from "@/components/primitives/progress"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Progress } from "@/components/ui/progress"; import { useMessageSearch } from "@/hooks"; import { renderMessageContent, safeParseJsonArray } from "@/lib/format"; -import { cn } from "@/lib/utils"; export function SearchPanel() { const [query, setQuery] = useState(""); @@ -29,10 +26,10 @@ export function SearchPanel() { }, [query]); return ( -
+
- + ) : results !== undefined ? ( <> -

+

Found {results.length} result{results.length !== 1 ? "s" : ""}

{results.length === 0 ? ( @@ -62,92 +59,84 @@ export function SearchPanel() { ) : (
{results.map((msg) => ( - - -
- - - - {msg.username?.charAt(0).toUpperCase() ?? "?"} - - -
-
- - {msg.username} - - - {msg.created_at - ? new Date(msg.created_at).toLocaleString() - : ""} - - {msg.ai_status && ( - - {msg.ai_status} - - )} -
-

- {renderMessageContent( - msg.edited_content ?? msg.content, - msg.metadata, - )} -

- {msg.ai_moderation_flags && - msg.ai_moderation_flags !== "[]" && ( -
- {safeParseJsonArray(msg.ai_moderation_flags).map( - (flag) => ( - - {flag} - - ), - )} -
- )} - {msg.ai_analysis && ( -

- - {msg.ai_analysis} -

- )} - {msg.ai_confidence != null && ( -
- - - {(msg.ai_confidence * 100).toFixed(0)}% - -
+
+
+ +
+
+ + {msg.username} + + + {msg.created_at + ? new Date(msg.created_at).toLocaleString() + : ""} + + {msg.ai_status && ( + + {msg.ai_status} + )}
+

+ {renderMessageContent( + msg.edited_content ?? msg.content, + msg.metadata, + )} +

+ {msg.ai_moderation_flags && + msg.ai_moderation_flags !== "[]" && ( +
+ {safeParseJsonArray(msg.ai_moderation_flags).map( + (flag) => ( + + {flag} + + ), + )} +
+ )} + {msg.ai_analysis && ( +

+ + {msg.ai_analysis} +

+ )} + {msg.ai_confidence != null && ( +
+ + + {(msg.ai_confidence * 100).toFixed(0)}% + +
+ )}
- - +
+
))}
)} ) : (
- -

+ +

Enter a search query to find messages across all channels.

-

+

Searches message content, AI flags, and analysis text.

diff --git a/services/frontend/src/components/charts/area-activity.tsx b/services/frontend/src/components/charts/area-activity.tsx new file mode 100644 index 0000000..00f29ac --- /dev/null +++ b/services/frontend/src/components/charts/area-activity.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { motion, useReducedMotion } from "motion/react"; +import { useId } from "react"; + +export interface AreaPoint { + label: string; + value: number; +} + +export interface AreaActivityProps { + data: AreaPoint[]; + height?: number; + stroke?: string; + className?: string; + label?: string; +} + +export function AreaActivity({ + data, + height = 160, + stroke = "var(--color-signal)", + className, + label, +}: AreaActivityProps) { + const id = useId().replace(/:/g, ""); + const reduce = useReducedMotion(); + const width = 600; + if (data.length === 0) + return
; + + const max = Math.max(...data.map((d) => d.value), 1); + const stepX = width / Math.max(data.length - 1, 1); + const pts = data.map((d, i) => { + const x = i * stepX; + const y = height - (d.value / max) * (height - 10) - 5; + return [x, y] as const; + }); + const line = pts + .map( + (p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`, + ) + .join(" "); + const area = `${line} L${width},${height} L0,${height} Z`; + const pathLen = 1400; + + return ( + + + + + + + + + + + ); +} diff --git a/services/frontend/src/components/charts/radial-gauge.tsx b/services/frontend/src/components/charts/radial-gauge.tsx new file mode 100644 index 0000000..3dd22c1 --- /dev/null +++ b/services/frontend/src/components/charts/radial-gauge.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { motion, useReducedMotion } from "motion/react"; +import { useId } from "react"; + +export interface RadialGaugeProps { + /** 0..1 health ratio */ + value: number; + size?: number; + label?: string; + sublabel?: string; + tone?: "signal" | "amber" | "vermilion"; +} + +const toneColor = { + signal: "var(--color-signal)", + amber: "var(--color-amber)", + vermilion: "var(--color-vermilion)", +}; + +export function RadialGauge({ + value, + size = 160, + label, + sublabel, + tone = "signal", +}: RadialGaugeProps) { + const id = useId().replace(/:/g, ""); + const reduce = useReducedMotion(); + const stroke = 12; + const r = (size - stroke) / 2; + const c = 2 * Math.PI * r; + const pct = Math.max(0, Math.min(1, value)); + const dash = c * pct; + + return ( +
+ + + + +
+ + {Math.round(pct * 100)}% + + {label && ( + + {label} + + )} + {sublabel && ( + + {sublabel} + + )} +
+
+ ); +} diff --git a/services/frontend/src/components/charts/session-ribbon.tsx b/services/frontend/src/components/charts/session-ribbon.tsx new file mode 100644 index 0000000..a2e6e4d --- /dev/null +++ b/services/frontend/src/components/charts/session-ribbon.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { cn } from "@/lib/utils"; + +export interface RibbonSegment { + id: string; + label: string; + value: number; // relative duration + tone?: "signal" | "amber" | "vermilion" | "neutral"; +} + +const toneClass = { + signal: "bg-[var(--color-signal)]", + amber: "bg-[var(--color-amber)]", + vermilion: "bg-[var(--color-vermilion)]", + neutral: "bg-[var(--color-ink-soft)]/40", +}; + +export interface SessionRibbonProps { + segments: RibbonSegment[]; + className?: string; + height?: number; +} + +export function SessionRibbon({ + segments, + className, + height = 28, +}: SessionRibbonProps) { + const total = segments.reduce((s, x) => s + x.value, 0) || 1; + return ( +
+ {segments.map((s) => ( +
+ + {s.label} + +
+ ))} +
+ ); +} diff --git a/services/frontend/src/components/charts/sparkline.tsx b/services/frontend/src/components/charts/sparkline.tsx new file mode 100644 index 0000000..9b7a9e3 --- /dev/null +++ b/services/frontend/src/components/charts/sparkline.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useId } from "react"; + +export interface SparklineProps { + data: number[]; + width?: number; + height?: number; + stroke?: string; + className?: string; + fill?: boolean; +} + +export function Sparkline({ + data, + width = 120, + height = 36, + stroke = "var(--color-signal)", + className, + fill = true, +}: SparklineProps) { + const id = useId().replace(/:/g, ""); + if (data.length < 2) + return ; + + const min = Math.min(...data); + const max = Math.max(...data); + const span = max - min || 1; + const stepX = width / (data.length - 1); + const pts = data.map((v, i) => { + const x = i * stepX; + const y = height - ((v - min) / span) * (height - 4) - 2; + return [x, y] as const; + }); + const line = pts + .map( + (p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`, + ) + .join(" "); + const area = `${line} L${width},${height} L0,${height} Z`; + + return ( + + + + + + + + {fill && } + + + ); +} diff --git a/services/frontend/src/components/charts/waveform.tsx b/services/frontend/src/components/charts/waveform.tsx new file mode 100644 index 0000000..746e756 --- /dev/null +++ b/services/frontend/src/components/charts/waveform.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { motion, useReducedMotion } from "motion/react"; +import { useMemo } from "react"; +import { cn } from "@/lib/utils"; + +export interface WaveformProps { + seed: string | number; + bars?: number; + height?: number; + className?: string; + tone?: "signal" | "amber" | "vermilion"; +} + +// deterministic pseudo-random from seed so the shape is stable per recording +function hashSeed(seed: string | number): number { + const s = String(seed); + let h = 2166136261; + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619); + } + return h >>> 0; +} + +export function Waveform({ + seed, + bars = 40, + height = 40, + className, + tone = "signal", +}: WaveformProps) { + const reduce = useReducedMotion(); + const values = useMemo(() => { + let state = hashSeed(seed) || 1; + const out: number[] = []; + for (let i = 0; i < bars; i++) { + state = (Math.imul(state, 1103515245) + 12345) >>> 0; + const r = (state % 1000) / 1000; + // envelope: louder in the middle, quieter at edges + const env = Math.sin((i / (bars - 1)) * Math.PI); + out.push(0.18 + r * 0.82 * (0.4 + env * 0.6)); + } + return out; + }, [seed, bars]); + + const color = { + signal: "var(--color-signal)", + amber: "var(--color-amber)", + vermilion: "var(--color-vermilion)", + }[tone]; + + return ( +
+ {values.map((v, i) => ( + + ))} +
+ ); +} diff --git a/services/frontend/src/components/dashboard/activity-chart.tsx b/services/frontend/src/components/dashboard/activity-chart.tsx index 5cccd9b..c958f0f 100644 --- a/services/frontend/src/components/dashboard/activity-chart.tsx +++ b/services/frontend/src/components/dashboard/activity-chart.tsx @@ -1,129 +1,35 @@ -"use client"; +import type { AreaPoint } from "@/components/charts/area-activity"; +import { AreaActivity } from "@/components/charts/area-activity"; -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { Card } from "@/components/ui/card"; -import { useMounted } from "@/lib/hooks/use-mounted"; -import { cn } from "@/lib/utils"; - -interface ActivityChartProps { - data?: { day: string; messages: number; flagged: number }[]; +export interface ActivityChartProps { + data: { + day: string; + messages: number; + flagged: number; + active_users: number; + }[]; } -const TOOLTIP_STYLE = { - background: "oklch(0.11 0.02 245 / 0.95)", - border: "1px solid oklch(1 0 0 / 0.08)", - borderRadius: 8, - fontSize: 12, - color: "oklch(0.93 0.01 245)", -} as const; - -export function ActivityChart({ data = [] }: ActivityChartProps) { - const mounted = useMounted(); - +export function ActivityChart({ data }: ActivityChartProps) { + const points: AreaPoint[] = data.map((d) => ({ + label: d.day, + value: d.messages, + })); return ( - -
- - Message Activity - - - messages · flagged per day +
+
+

Daily messages

+ + {data.length}d
-
- {mounted ? ( - - - - - - - - - - - - - - { - const [, m, d] = v.split("-"); - return `${Number(m)}/${Number(d)}`; - }} - minTickGap={24} - /> - - { - const [y, m, d] = String(label).split("-"); - return `${d}/${m}/${y}`; - }} - /> - - - - - ) : ( -
- )} -
- + +
+ ); +} + +function ActivityChartInner({ points }: { points: AreaPoint[] }) { + return ( + ); } diff --git a/services/frontend/src/components/dashboard/channels-section.tsx b/services/frontend/src/components/dashboard/channels-section.tsx index 67aaaaa..4bad6f8 100644 --- a/services/frontend/src/components/dashboard/channels-section.tsx +++ b/services/frontend/src/components/dashboard/channels-section.tsx @@ -2,193 +2,139 @@ import { Hash, Search } from "lucide-react"; import { useCallback, useState } from "react"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import { Input } from "@/components/primitives/input"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { useChannelDetail, useChannels } from "@/hooks"; -import { renderMessageContent } from "@/lib/format"; import type { DashboardChannel } from "@/lib/types"; -import { cn } from "@/lib/utils"; export function ChannelsSection({ guildId }: { guildId?: string }) { const [search, setSearch] = useState(""); const [selectedId, setSelectedId] = useState(null); - const { - data: channels = [], - isLoading, - error, - mutate: refetch, - } = useChannels(guildId ?? "", search); + const { data: channels = [], isLoading } = useChannels(guildId ?? "", search); const { data: detail } = useChannelDetail(selectedId); - const handleSearch = useCallback((v: string) => { - setSearch(v); - setSelectedId(null); - }, []); + const handleSearch = useCallback((v: string) => setSearch(v), []); - if (error) { + if (isLoading) return ; + if (channels.length === 0) return ( - - Failed to load channels: {error.message} - - + ); - } return ( -
-
+
+
- + handleSearch(e.target.value)} - className="pl-9 h-9" + className="pl-9" />
- - {isLoading ? ( - - ) : channels.length === 0 ? ( - - ) : ( -
- {channels.map((channel) => ( - - ))} -
- )} + {channels.map((c) => ( + setSelectedId(c.channel_id)} + /> + ))}
- +
{detail ? ( -
-
- -

- {detail.channel_name ?? detail.channel_id} -

-

- {detail.channel_id} -

-
- -
- Messages: {detail.total_messages} - - Flagged: {detail.flagged_count} - - - Clean: {detail.clean_count} - -
- - {detail.culture_summary && ( -
-

- Culture summary -

-

- {detail.culture_summary} -

+
+
+ + + +
+
+ {detail.channel_name ?? detail.channel_id} +
+
+ {detail.total_messages.toLocaleString()} messages +
- )} - - {detail.recent_messages.length > 0 && ( -
-

- Recent messages -

- {detail.recent_messages.slice(0, 5).map((msg) => ( -
-

- {msg.username}:{" "} - {renderMessageContent(msg.content, msg.metadata) || - "(no text content)"} -

-

- {new Date(msg.created_at).toLocaleString()} -

-
- ))} -
- )} -
- ) : ( -
- -

- Select a channel to see its culture summary and recent messages. +

+ +

+ {detail.culture_summary ?? "No data yet."}

+ ) : ( +

+ Select a channel to inspect. +

)} - +
); } function ChannelRow({ channel, - active, + selected, onSelect, }: { channel: DashboardChannel; - active: boolean; - onSelect: (id: string) => void; + selected: boolean; + onSelect: () => void; }) { + const total = channel.total_messages + channel.flagged_count || 1; return ( - onSelect(channel.channel_id)} + + ); +} + +function Stat({ + label, + value, + tone, +}: { + label: string; + value: number; + tone: "vermilion"; +}) { + return ( +
+ {label} + + {value} + +
); } diff --git a/services/frontend/src/components/dashboard/hourly-activity-chart.tsx b/services/frontend/src/components/dashboard/hourly-activity-chart.tsx index cf49cf8..3aba1a4 100644 --- a/services/frontend/src/components/dashboard/hourly-activity-chart.tsx +++ b/services/frontend/src/components/dashboard/hourly-activity-chart.tsx @@ -1,109 +1,29 @@ -"use client"; +import type { AreaPoint } from "@/components/charts/area-activity"; +import { AreaActivity } from "@/components/charts/area-activity"; -import { - Bar, - BarChart, - Cell, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { Card } from "@/components/ui/card"; -import { useMounted } from "@/lib/hooks/use-mounted"; -import { cn } from "@/lib/utils"; - -interface HourlyActivityChartProps { - data?: { hour: number; messages: number; flagged: number }[]; +export interface HourlyActivityChartProps { + data: { hour: number; messages: number; flagged: number }[]; } -const HOUR_LABELS = Array.from({ length: 24 }, (_, i) => { - const h = i % 12 === 0 ? 12 : i % 12; - return `${h}${i < 12 ? "am" : "pm"}`; -}); - -const TOOLTIP_STYLE = { - background: "oklch(0.11 0.02 245 / 0.95)", - border: "1px solid oklch(1 0 0 / 0.08)", - borderRadius: 8, - fontSize: 12, - color: "oklch(0.93 0.01 245)", -} as const; - -export function HourlyActivityChart({ data = [] }: HourlyActivityChartProps) { - const mounted = useMounted(); - - const full = Array.from({ length: 24 }, (_, hour) => { - const found = data.find((d) => d.hour === hour); - return { - hour, - label: HOUR_LABELS[hour], - messages: found?.messages ?? 0, - flagged: found?.flagged ?? 0, - }; - }); - - const peak = Math.max(1, ...full.map((d) => d.messages)); - const maxMessages = Math.max(...full.map((d) => d.messages)); - +export function HourlyActivityChart({ data }: HourlyActivityChartProps) { + const points: AreaPoint[] = data.map((d) => ({ + label: `${d.hour}:00`, + value: d.messages, + })); return ( - -
- - Hourly Activity - - - last 24h · peak {maxMessages} msgs +
+
+

Hourly distribution

+ + 00:00 – 23:00
-
- {mounted ? ( - - - - Math.max(1, dataMax)]} - /> - `Hour ${label}`} - /> - - {full.map((d) => ( - 0 - ? "var(--color-primary)" - : d.messages > peak * 0.5 - ? "oklch(0.52 0.13 245 / 0.7)" - : "oklch(0.52 0.13 245 / 0.35)" - } - /> - ))} - - - - ) : ( -
- )} -
- + +
); } diff --git a/services/frontend/src/components/dashboard/moderation-donut.tsx b/services/frontend/src/components/dashboard/moderation-donut.tsx index 98abe6d..08e4307 100644 --- a/services/frontend/src/components/dashboard/moderation-donut.tsx +++ b/services/frontend/src/components/dashboard/moderation-donut.tsx @@ -1,102 +1,53 @@ -"use client"; +import { RadialGauge } from "@/components/charts/radial-gauge"; +import type { DashboardStats } from "@/lib/types"; -import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; -import { Card } from "@/components/ui/card"; -import { useMounted } from "@/lib/hooks/use-mounted"; -import { cn } from "@/lib/utils"; - -interface ModerationDonutProps { - data?: { name: string; value: number; color: string }[]; +export interface ModerationDonutProps { + stats?: DashboardStats; } -const TOOLTIP_STYLE = { - background: "oklch(0.11 0.02 245 / 0.95)", - border: "1px solid oklch(1 0 0 / 0.08)", - borderRadius: 8, - fontSize: 12, - color: "oklch(0.93 0.01 245)", -} as const; - -export function ModerationDonut({ data = [] }: ModerationDonutProps) { - const mounted = useMounted(); - const total = data.reduce((sum, d) => sum + d.value, 0); - const cleanPct = - total > 0 - ? Math.round( - ((data.find((d) => d.name === "Clean")?.value ?? 0) / total) * 100, - ) - : 0; +export function ModerationDonut({ stats }: ModerationDonutProps) { + const clean = stats?.total_clean ?? 0; + const flagged = stats?.total_flagged ?? 0; + const warned = stats?.total_warned ?? 0; + const total = clean + flagged + warned || 1; + const ratio = clean / total; return ( - -
- - Moderation Breakdown - +
+

Moderation health

+ 0.8 ? "signal" : ratio > 0.6 ? "amber" : "vermilion"} + /> +
+ + +
-
-
- {mounted ? ( - - - - {data.map((entry) => ( - - ))} - - - - - ) : ( -
- )} -
- - {total.toLocaleString()} - - - messages - -
-
+
+ ); +} -
- {data.map((d) => ( -
- - {d.name} - - {d.value.toLocaleString()} - - - {total > 0 ? Math.round((d.value / total) * 100) : 0}% - -
- ))} - {cleanPct >= 90 && ( -

- ✓ Server is {cleanPct}% clean — moderation is holding up well -

- )} - {cleanPct < 90 && cleanPct > 0 && ( -

- {100 - cleanPct}% of messages were flagged or warned — review - activity in the Analysis tab -

- )} -
-
- +function Row({ + label, + value, + tone, +}: { + label: string; + value: number; + tone: string; +}) { + return ( +
+ + + {label} + + + {value.toLocaleString()} + +
); } diff --git a/services/frontend/src/components/dashboard/reactions-section.tsx b/services/frontend/src/components/dashboard/reactions-section.tsx index b942d05..a5d6416 100644 --- a/services/frontend/src/components/dashboard/reactions-section.tsx +++ b/services/frontend/src/components/dashboard/reactions-section.tsx @@ -1,135 +1,85 @@ "use client"; import { Flame, Heart, SmilePlus } from "lucide-react"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; -import { Badge } from "@/components/ui/badge"; -import { Card } from "@/components/ui/card"; import { useTopReactions, useTopReactors } from "@/hooks"; -import { renderMessageContent } from "@/lib/format"; -import { cn } from "@/lib/utils"; -function formatReactionTime(ts: number | null): string { - if (!ts) return ""; - const diff = Date.now() - ts; - const hours = Math.floor(diff / 3600000); - if (hours < 1) return "baru saja"; - if (hours < 24) return `${hours} jam lalu`; - const days = Math.floor(hours / 24); - return `${days} hari lalu`; +export interface ReactionsSectionProps { + initialReactions?: Awaited>["data"]; } export function ReactionsSection() { const { data: reactions, isLoading: reactionsLoading } = useTopReactions(); const { data: reactors, isLoading: reactorsLoading } = useTopReactors(); + if (reactionsLoading || reactorsLoading) return ; + + const topReactions = (reactions ?? []).slice(0, 6); + const topReactors = (reactors ?? []).slice(0, 6); + return ( -
-
-

- - Top pesan paling di-reaksi +
+
+

+ + Top reactions

- {reactionsLoading ? ( - - ) : !reactions || reactions.length === 0 ? ( - - - + {topReactions.length === 0 ? ( + ) : ( -
- {reactions.map((r, i) => ( - - - {i + 1} +
+ {topReactions + .flatMap((m) => + m.top_emojis.map((e) => ({ emoji: e.emoji, count: e.count })), + ) + .reduce<{ emoji: string; count: number }[]>((acc, cur) => { + const found = acc.find((x) => x.emoji === cur.emoji); + if (found) found.count += cur.count; + else acc.push(cur); + return acc; + }, []) + .sort((a, b) => b.count - a.count) + .slice(0, 8) + .map((r) => ( + + {r.emoji} + + {r.count} + -
- {r.top_emojis.map((e) => ( - - {e.emoji} - - ))} - {r.top_emojis.length === 0 && ( - - )} -
-
-

- {renderMessageContent(r.content, undefined) || - "(tanpa teks)"} -

-

- {r.username ?? "unknown"} · # - {r.channel_name ?? r.channel_id?.slice(0, 8)} ·{" "} - {formatReactionTime(r.created_at)} -

-
- - - {r.reaction_count} - - - ))} + ))}
)}
-
-

- - Top reaktor — paling sering ngasih reaksi +
+

+ + Top reactors

- {reactorsLoading ? ( - - ) : !reactors || reactors.length === 0 ? ( - - - + {topReactors.length === 0 ? ( + ) : ( -
- {reactors.map((r, i) => ( - - - {i + 1} - -
-

- {r.username} -

-

- {r.messages_reacted} pesan di-reaksi · {r.emojis_used} emoji - unik · {r.adds_count} total reaksi -

-
- - - {r.net_count} - -
+
+ {topReactors.map((r) => ( +
+ + {r.username} + +{r.net_count} +
))}
)} @@ -137,5 +87,3 @@ export function ReactionsSection() {
); } - -export default ReactionsSection; diff --git a/services/frontend/src/components/dashboard/stat-card.tsx b/services/frontend/src/components/dashboard/stat-card.tsx deleted file mode 100644 index 22b3fcd..0000000 --- a/services/frontend/src/components/dashboard/stat-card.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"use client"; - -import type { LucideIcon } from "lucide-react"; -import { Area, AreaChart, ResponsiveContainer } from "recharts"; -import { Card } from "@/components/ui/card"; -import { useMounted } from "@/lib/hooks/use-mounted"; -import { cn } from "@/lib/utils"; - -interface StatCardProps { - label: string; - value: number | string; - icon: LucideIcon; - variant?: "default" | "danger" | "success"; - sparklineData?: { value: number }[]; - formatter?: (v: number) => string; -} - -export function StatCard({ - label, - value, - icon: Icon, - variant = "default", - sparklineData, - formatter = (v) => (typeof v === "number" ? v.toLocaleString() : v), -}: StatCardProps) { - const mounted = useMounted(); - const accentColor = { - default: "var(--color-primary)", - danger: "var(--color-destructive)", - success: "oklch(0.6 0.18 160)", - }[variant]; - - const bgAccent = { - default: "bg-primary/10 text-primary", - danger: "bg-destructive/10 text-destructive", - success: "bg-emerald-500/10 text-emerald-500", - }[variant]; - - const numValue = typeof value === "number" ? value : Number(value); - - return ( - -
-
- -
-
-
- {formatter(numValue)} -
-
- {label} -
- - {/* Sparkline background */} - {sparklineData && sparklineData.length > 0 && mounted && ( -
- - - - - - - - - - - -
- )} -
- ); -} diff --git a/services/frontend/src/components/dashboard/top-channels-chart.tsx b/services/frontend/src/components/dashboard/top-channels-chart.tsx index e065313..d162018 100644 --- a/services/frontend/src/components/dashboard/top-channels-chart.tsx +++ b/services/frontend/src/components/dashboard/top-channels-chart.tsx @@ -1,74 +1,39 @@ -"use client"; +import { Hash } from "lucide-react"; +import type { TopChannel } from "@/lib/types"; -import { - Bar, - BarChart, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from "recharts"; -import { Card } from "@/components/ui/card"; -import { useMounted } from "@/lib/hooks/use-mounted"; -import { cn } from "@/lib/utils"; - -interface TopChannelsChartProps { - data?: { name: string; count: number }[]; +export interface TopChannelsChartProps { + channels: TopChannel[]; } -export function TopChannelsChart({ data = [] }: TopChannelsChartProps) { - const mounted = useMounted(); - +export function TopChannelsChart({ channels }: TopChannelsChartProps) { + const max = Math.max(...channels.map((c) => c.message_count), 1); + const top = [...channels] + .sort((a, b) => b.message_count - a.message_count) + .slice(0, 8); return ( - -
- - Top Channels - +
+

Top channels

+
+ {top.map((c) => ( +
+ + + + + {c.channel_name ?? c.channel_id} + +
+
+
+ + {c.message_count.toLocaleString()} + +
+ ))}
-
- {mounted ? ( - - - - - - - - - ) : ( -
- )} -
- +
); } diff --git a/services/frontend/src/components/dashboard/users-section.tsx b/services/frontend/src/components/dashboard/users-section.tsx index 018929a..0b3a908 100644 --- a/services/frontend/src/components/dashboard/users-section.tsx +++ b/services/frontend/src/components/dashboard/users-section.tsx @@ -1,50 +1,24 @@ "use client"; -import { Search, Users, UserX } from "lucide-react"; +import { Search, Users } from "lucide-react"; import { useCallback, useState } from "react"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Input } from "@/components/primitives/input"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; import { useUserDetail, useUsers } from "@/hooks"; -import { renderMessageContent } from "@/lib/format"; -import type { DashboardUser } from "@/lib/types"; -import { cn } from "@/lib/utils"; const TRUST_TIERS = [ - { - min: 75, - label: "Trusted", - className: "border-green-500/40 text-green-500", - }, - { min: 40, label: "Netral", className: "border-sky-500/40 text-sky-500" }, - { - min: 10, - label: "At Risk", - className: "border-orange-500/40 text-orange-500", - }, - { min: 0, label: "Kritis", className: "border-red-500/40 text-red-500" }, -] as const; + { min: 75, label: "Trusted", tone: "signal" as const }, + { min: 40, label: "Neutral", tone: "neutral" as const }, + { min: 10, label: "At Risk", tone: "amber" as const }, + { min: 0, label: "Critical", tone: "vermilion" as const }, +]; -export function trustTier(score: number) { +function trustTier(score?: number | null) { + const s = score ?? 0; return ( - TRUST_TIERS.find((t) => score >= t.min) ?? - TRUST_TIERS[TRUST_TIERS.length - 1] - ); -} - -function TrustBadge({ score }: { score: number }) { - const tier = trustTier(score); - return ( - - {tier.label}: {score} - + TRUST_TIERS.find((t) => s >= t.min) ?? TRUST_TIERS[TRUST_TIERS.length - 1] ); } @@ -52,201 +26,126 @@ export function UsersSection() { const [search, setSearch] = useState(""); const [selectedId, setSelectedId] = useState(null); - const { - data: users = [], - isLoading, - error, - mutate: refetch, - } = useUsers(search); + const { data: users = [], isLoading } = useUsers(search); const { data: detail } = useUserDetail(selectedId); - const handleSearch = useCallback((v: string) => { - setSearch(v); - setSelectedId(null); - }, []); + const handleSearch = useCallback((v: string) => setSearch(v), []); - if (error) { + if (isLoading) return ; + if (users.length === 0) return ( - - Failed to load users: {error.message} - - + ); - } return ( -
-
+
+
- + handleSearch(e.target.value)} - className="pl-9 h-9" + className="pl-9" />
- - {isLoading ? ( - - ) : users.length === 0 ? ( - - ) : ( -
- {users.map((user) => ( - - ))} -
- )} +
+ {users.map((u) => { + const tier = trustTier(u.trust_score); + return ( + + ); + })} +
- +
{detail ? ( -
+
- - - - {detail.username?.charAt(0).toUpperCase() ?? "?"} - - -
-

- {detail.username ?? "Unknown user"} -

-

- {detail.user_id} -

+ +
+
{detail.username}
+
+ {detail.total_messages.toLocaleString()} messages +
- -
- Messages: {detail.total_messages} - - Flagged: {detail.flagged_count} - - - Clean: {detail.clean_count} - - {detail.trust_score != null && ( - - )} - {detail.clean_message_streak != null && ( - - Streak: {detail.clean_message_streak} - - )} - {detail.total_infractions != null && ( - - Infractions: {detail.total_infractions} - - )} +
+ + + +
- - {detail.profile_summary && ( -

- {detail.profile_summary} -

- )} - - {detail.recent_messages.length > 0 && ( -
-

- Recent messages -

- {detail.recent_messages.slice(0, 5).map((msg) => ( -
-

- {renderMessageContent(msg.content, msg.metadata) || - "(no text content)"} -

-

- {msg.channel_id?.slice(0, 8)} ·{" "} - {new Date(msg.created_at).toLocaleString()} -

-
- ))} -
- )} -
- ) : ( -
- -

- Select a user to see their profile, trust score and recent - messages. +

+ {detail.profile_summary}

+ ) : ( +

+ Select a user to inspect. +

)} - +
); } -function UserRow({ - user, - active, - onSelect, +function Stat({ + label, + value, + tone, }: { - user: DashboardUser; - active: boolean; - onSelect: (id: string) => void; + label: string; + value: string; + tone?: "amber" | "vermilion"; }) { return ( - onSelect(user.user_id)} - > - - - - - {user.username?.charAt(0).toUpperCase() ?? "?"} - - -
-

- {user.username ?? "Unknown user"} -

-

- {user.user_id} -

-
-
- {user.total_messages} - {user.flagged_count > 0 && ( - {user.flagged_count} - )} - {user.trust_score != null && } -
-
-
+
+
+ {label} +
+
+ {value} +
+
); } diff --git a/services/frontend/src/components/layout/app-sidebar.tsx b/services/frontend/src/components/layout/app-sidebar.tsx deleted file mode 100644 index f9691c0..0000000 --- a/services/frontend/src/components/layout/app-sidebar.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -import { ThemeToggle } from "@/components/layout/theme-toggle"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarRail, -} from "@/components/ui/sidebar"; -import { isActivePath, navItems } from "@/lib/navigation"; - -/** - * Primary application navigation — pure shadcn Sidebar primitives. - * Renders as a fixed desktop rail (collapsible to icons) and a Sheet - * on mobile via the Sidebar component itself. - */ -export function AppSidebar() { - const pathname = usePathname(); - - return ( - - - - - }> -
- D -
-
- Discord Automod - - Moderation dashboard - -
-
-
-
-
- - - - Navigation - - {navItems.map(({ href, label, icon: Icon, matchPrefix }) => { - const active = isActivePath(pathname, matchPrefix); - return ( - - } - isActive={active} - tooltip={label} - className="group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center" - > - - {label} - - - ); - })} - - - - - - - - - - - - - -
- ); -} diff --git a/services/frontend/src/components/layout/spine.tsx b/services/frontend/src/components/layout/spine.tsx new file mode 100644 index 0000000..87e5dab --- /dev/null +++ b/services/frontend/src/components/layout/spine.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { motion } from "motion/react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { navItems } from "@/lib/navigation"; +import { cn } from "@/lib/utils"; + +const titleFromPath: Record = { + "/dashboard": "Overview", + "/messages": "Messages", + "/voice": "Voice", + "/media": "Media", + "/recordings": "Recordings", + "/moderation": "Moderation", + "/analysis": "Analysis", +}; + +export function Spine() { + const pathname = usePathname(); + + return ( + <> + {/* Desktop rail */} + + + {/* Mobile bottom tab-bar */} + + + ); +} + +export function PageTitle() { + const pathname = usePathname(); + const key = + Object.keys(titleFromPath).find((k) => pathname.startsWith(k)) ?? + "/dashboard"; + return ( + {titleFromPath[key]} + ); +} diff --git a/services/frontend/src/components/layout/status-bar.tsx b/services/frontend/src/components/layout/status-bar.tsx new file mode 100644 index 0000000..587367d --- /dev/null +++ b/services/frontend/src/components/layout/status-bar.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useChatbot } from "@/components/chatbot/chatbot-context"; +import { GuildSelector } from "@/components/shared/guild-selector"; +import { cn } from "@/lib/utils"; +import { useWebSocket } from "@/lib/ws/context"; +import { PageTitle } from "./spine"; +import { ThemeToggle } from "./theme-toggle"; + +const statusTone: Record = { + connected: "bg-[var(--color-signal)]", + connecting: "bg-[var(--color-amber)]", + disconnected: "bg-[var(--color-ink-soft)]", + error: "bg-[var(--color-vermilion)]", +}; + +export function StatusBar({ + guildId, + onGuildChange, +}: { + guildId: string; + onGuildChange: (g: string) => void; +}) { + const ws = useWebSocket(); + const { expression } = useChatbot(); + const [clock, setClock] = useState("--:--:--"); + + useEffect(() => { + const tick = () => + setClock(new Date().toLocaleTimeString("en-GB", { hour12: false })); + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, []); + + return ( +
+ +
+ + + {ws.status} + + + + {expression} + + + + {clock} + + onGuildChange(g ?? "")} + /> + +
+
+ ); +} diff --git a/services/frontend/src/components/layout/sub-nav.tsx b/services/frontend/src/components/layout/sub-nav.tsx deleted file mode 100644 index 6c8e577..0000000 --- a/services/frontend/src/components/layout/sub-nav.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; - -interface SubNavTab { - id: string; - label: string; - icon?: React.ReactNode; -} - -interface SubNavProps { - tabs: SubNavTab[]; - activeTab: string; - onTabChange: (tab: string) => void; - className?: string; -} - -export function SubNav({ - tabs, - activeTab, - onTabChange, - className, -}: SubNavProps) { - return ( -
- {tabs.map((tab) => ( - - ))} -
- ); -} diff --git a/services/frontend/src/components/layout/theme-toggle.tsx b/services/frontend/src/components/layout/theme-toggle.tsx index 85be412..b9d8863 100644 --- a/services/frontend/src/components/layout/theme-toggle.tsx +++ b/services/frontend/src/components/layout/theme-toggle.tsx @@ -1,40 +1,38 @@ "use client"; import { Moon, Sun } from "lucide-react"; +import { motion } from "motion/react"; import { useTheme } from "next-themes"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +import { useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; export function ThemeToggle() { - const { setTheme } = useTheme(); + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + useEffect(() => setMounted(true), []); + + const isDark = resolvedTheme === "dark"; return ( - - - } - > - - - Toggle theme - - - setTheme("light")}> - - Light - - setTheme("dark")}> - - Dark - - - + ); } diff --git a/services/frontend/src/components/media/mini-player.tsx b/services/frontend/src/components/media/mini-player.tsx index 50e3950..f531fa1 100644 --- a/services/frontend/src/components/media/mini-player.tsx +++ b/services/frontend/src/components/media/mini-player.tsx @@ -1,80 +1,59 @@ "use client"; -import { Disc3, Music, Repeat, SkipForward, Square } from "lucide-react"; +import { Pause, Play, SkipForward, Volume2 } from "lucide-react"; +import { motion } from "motion/react"; +import { Button } from "@/components/primitives/button"; +import { useMediaSkip, useMediaState, useMediaWsSync } from "@/hooks"; import { useMediaPlayer } from "@/lib/hooks/use-media-player"; +import { cn } from "@/lib/utils"; +import { useWebSocket } from "@/lib/ws/context"; export function MiniPlayer() { - const { playing, current, queue, loop, pending, skip, stop, toggleLoop } = - useMediaPlayer(); + const ws = useWebSocket(); + const { data: state } = useMediaState(); + const { playing, current } = useMediaPlayer(); + useMediaWsSync(ws); + const skip = useMediaSkip(); - // Nothing to show if no track is playing and nothing is queued - if (!current && queue.length === 0) return null; + if (!current) return null; return ( -
- {/* Track info */} -
-
- {playing ? ( - - ) : ( - - )} -
-
-

- {current?.title ?? "Unknown track"} -

- {queue.length > 0 && ( -

- {queue.length > 1 ? `${queue.length} in queue` : "1 in queue"} -

- )} + + {current.title} +
+
{current.title}
+
+ {current.source}
- - {/* Controls */} -
- {playing && ( - - )} - {current && ( - - )} - + + {playing ? : } + +
-
+ ); } diff --git a/services/frontend/src/components/media/music-player.tsx b/services/frontend/src/components/media/music-player.tsx deleted file mode 100644 index e80bb0a..0000000 --- a/services/frontend/src/components/media/music-player.tsx +++ /dev/null @@ -1,178 +0,0 @@ -"use client"; - -import { Disc3, Music, Play, Repeat, SkipForward, Square } from "lucide-react"; -import Image from "next/image"; -import { useCallback, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { - useMediaLoop, - useMediaQueue, - useMediaSkip, - useMediaState, - useMediaStop, - useMediaWsSync, -} from "@/hooks"; -import type { MediaState } from "@/lib/types"; -import type { WsHook } from "@/lib/ws-hook"; - -interface MusicPlayerProps { - ws: WsHook; - /** Server-fetched media snapshot used to seed the first render. */ - initialData?: MediaState; -} - -export function MusicPlayer({ ws, initialData }: MusicPlayerProps) { - const { data: mediaState } = useMediaState(initialData); - const queueMut = useMediaQueue(); - const skipMut = useMediaSkip(); - const stopMut = useMediaStop(); - const loopMut = useMediaLoop(); - const [queueUrl, setQueueUrl] = useState(""); - const [screenMode, setScreenMode] = useState(false); - - // Sync WS media_state into the query cache - useMediaWsSync(ws); - - const handleQueue = useCallback(() => { - if (!queueUrl.trim()) return; - queueMut.mutate({ - url: queueUrl.trim(), - mode: screenMode ? "screen" : "music", - }); - setQueueUrl(""); - }, [queueUrl, queueMut, screenMode]); - - return ( - - - - - Music Player - - - -
- setQueueUrl(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && handleQueue()} - className="flex-1 h-9" - /> - - -
- - {mediaState?.activeMode && ( -

- {mediaState.activeMode === "screen" - ? "Screen share active" - : "Music playing"} -

- )} - - {mediaState?.current ? ( -
-

- - Now Playing -

-
- {mediaState.current.thumbnailUrl && ( - - )} -
-

- {mediaState.current.title ?? mediaState.current.source} -

-

- {mediaState.current.durationMs - ? `${Math.floor(mediaState.current.durationMs / 60000)}:${String(Math.floor((mediaState.current.durationMs % 60000) / 1000)).padStart(2, "0")}` - : "Live"} -

-
-
-
- ) : ( - !mediaState?.queue?.length && ( -

- No media queued. Paste a URL above to start playing. -

- ) - )} - -
- - - -
- - {mediaState && mediaState.queue.length > 0 && ( -
-

- Queue ({mediaState.queue.length}) -

-
- {mediaState.queue.map((item, i) => ( -
- - {i + 1}. - - - {item.title ?? item.source} - -
- ))} -
-
- )} -
-
- ); -} diff --git a/services/frontend/src/components/messages/ai-analysis-panel.tsx b/services/frontend/src/components/messages/ai-analysis-panel.tsx index 9448afe..caf04bb 100644 --- a/services/frontend/src/components/messages/ai-analysis-panel.tsx +++ b/services/frontend/src/components/messages/ai-analysis-panel.tsx @@ -1,7 +1,8 @@ "use client"; import { useState } from "react"; -import { Card } from "@/components/ui/card"; +import { Badge } from "@/components/primitives/badge"; +import { Progress } from "@/components/primitives/progress"; import { cn } from "@/lib/utils"; interface AiAnalysisPanelProps { @@ -16,11 +17,11 @@ interface AiAnalysisPanelProps { } const severityColor: Record = { - none: "text-emerald-500", - low: "text-text-secondary", - medium: "text-accent-amber", - high: "text-accent-purple", - critical: "text-destructive", + none: "text-[var(--color-ink-soft)]", + low: "text-[var(--color-ink-soft)]", + medium: "text-[var(--color-amber)]", + high: "text-orange-500", + critical: "text-[var(--color-vermilion)]", }; export function AiAnalysisPanel({ @@ -37,11 +38,11 @@ export function AiAnalysisPanel({ if (!status || status === "pending") { return ( - - +
+ AI analysis pending - +
); } @@ -54,28 +55,27 @@ export function AiAnalysisPanel({ : [] : categories || []; + const statusTone = + status === "clean" + ? "signal" + : status === "flagged" + ? "vermilion" + : status === "warn" + ? "amber" + : "neutral"; + return ( - +
- + AI Analysis - - {status} - + {status}
{severity && (
- Severity: + Severity: - Confidence: - {(confidence * 100).toFixed(0)}% + Confidence +
)} {score !== null && score !== undefined && ( -
- Score: +
+ Score {score.toFixed(2)}
)} @@ -104,12 +109,9 @@ export function AiAnalysisPanel({ {flagsArray.length > 0 && (
{flagsArray.map((f: string) => ( - + {f} - + ))}
)} @@ -117,21 +119,18 @@ export function AiAnalysisPanel({ {categoriesArray.length > 0 && (
{categoriesArray.map((c: string) => ( - + {c} - + ))}
)} {analysis && ( -
+

@@ -141,7 +140,7 @@ export function AiAnalysisPanel({ @@ -151,10 +150,10 @@ export function AiAnalysisPanel({ {action && action !== "none" && (

- Recommended: - {action} + Recommended: + {action}
)} - +
); } diff --git a/services/frontend/src/components/messages/ai-status-badge.tsx b/services/frontend/src/components/messages/ai-status-badge.tsx index d732bc6..d73829f 100644 --- a/services/frontend/src/components/messages/ai-status-badge.tsx +++ b/services/frontend/src/components/messages/ai-status-badge.tsx @@ -1,28 +1,44 @@ "use client"; +import type { AiSeverity, AiStatus } from "@/lib/types"; import { cn } from "@/lib/utils"; -const STATUS_STYLES: Record = { - clean: - "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", - flagged: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - warn: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20", - pending: "bg-muted text-muted-foreground border-border", - processing: - "bg-sky-500/10 text-sky-600 dark:text-sky-400 border-sky-500/20 animate-pulse", - error: "bg-destructive/10 text-destructive border-destructive/20", +const severityTick: Record, string> = { + none: "border-[var(--color-signal)]/30", + low: "border-[var(--color-amber)]/50", + medium: "border-[var(--color-amber)]", + high: "border-orange-500/80", + critical: "border-[var(--color-vermilion)]", }; -export function AiStatusBadge({ status }: { status?: string | null }) { - if (!status) return null; +const statusBadge: Record, string> = { + pending: "bg-[var(--color-ink-soft)]/20 text-[var(--color-ink-soft)]", + processing: "bg-[var(--color-amber)]/15 text-[var(--color-amber)]", + clean: "bg-[var(--color-signal)]/15 text-[var(--color-signal)]", + warn: "bg-[var(--color-amber)]/15 text-[var(--color-amber)]", + flagged: "bg-[var(--color-vermilion)]/15 text-[var(--color-vermilion)]", + error: "bg-[var(--color-vermilion)]/15 text-[var(--color-vermilion)]", +}; + +export function SeverityTick({ severity }: { severity?: AiSeverity | null }) { + const cls = severity ? severityTick[severity] : "border-transparent"; return ( - {status} + className={cn("absolute left-0 top-0 h-full w-0.5 border-l-2", cls)} + aria-hidden="true" + /> + ); +} + +export function AiStatusBadge({ status }: { status?: AiStatus | null }) { + if (!status) return null; + return ( + + + {status} ); } diff --git a/services/frontend/src/components/messages/attachments-grid.tsx b/services/frontend/src/components/messages/attachments-grid.tsx index 7733e1e..585f6c6 100644 --- a/services/frontend/src/components/messages/attachments-grid.tsx +++ b/services/frontend/src/components/messages/attachments-grid.tsx @@ -1,69 +1,35 @@ "use client"; -import { ImageIcon } from "lucide-react"; -import type { AttachmentRecord } from "@/lib/types"; - -interface AttachmentsGridProps { - attachments: AttachmentRecord[]; - onImageClick?: (index: number) => void; -} +import type { AttachmentRef } from "@/lib/types"; +import { cn } from "@/lib/utils"; export function AttachmentsGrid({ attachments, - onImageClick, -}: AttachmentsGridProps) { + onOpen, +}: { + attachments: AttachmentRef[]; + onOpen: (url: string) => void; +}) { if (attachments.length === 0) return null; - - const images = attachments.filter((a) => a.type?.startsWith("image/")); - const others = attachments.filter((a) => !a.type?.startsWith("image/")); - + const images = attachments.filter((a) => /image/i.test(a.contentType ?? "")); + if (images.length === 0) return null; return ( -
- {images.length > 0 && ( -
- {images.map((att, i) => ( -
- - {images.length > 1 && ( - - {i + 1}/{images.length} - - )} -
- ))} -
- )} - {others.length > 0 && ( -
- {others.map((att) => ( -
- - - {att.filename} - -
- ))} -
- )} +
+ {images.map((a, i) => ( + + ))}
); } diff --git a/services/frontend/src/components/messages/lightbox.tsx b/services/frontend/src/components/messages/lightbox.tsx index 9add7ea..c5250e8 100644 --- a/services/frontend/src/components/messages/lightbox.tsx +++ b/services/frontend/src/components/messages/lightbox.tsx @@ -1,128 +1,74 @@ "use client"; -import { ChevronLeft, ChevronRight, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; +import { X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Dialog } from "@/components/primitives/dialog"; +import { cn } from "@/lib/utils"; -interface LightboxProps { - images: Array<{ src: string; alt?: string }>; - initialIndex?: number; - open: boolean; - onClose: () => void; -} - -/** - * Fullscreen image viewer with keyboard navigation (←/→/Esc) and - * touch-swipe support. Mounted at page level so a single instance - * serves the message list, image grid and attachments grid. - */ export function Lightbox({ - images, - initialIndex = 0, open, onClose, -}: LightboxProps) { - const [index, setIndex] = useState(initialIndex); - - useEffect(() => { - if (open) setIndex(initialIndex); - }, [open, initialIndex]); - - const prev = useCallback(() => { - setIndex((i) => (i - 1 + images.length) % images.length); - }, [images.length]); - - const next = useCallback(() => { - setIndex((i) => (i + 1) % images.length); - }, [images.length]); - - useEffect(() => { - if (!open) return; - const onKey = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - if (e.key === "ArrowLeft") prev(); - if (e.key === "ArrowRight") next(); - }; - document.addEventListener("keydown", onKey); - // Lock body scroll while the lightbox is open - document.body.style.overflow = "hidden"; - return () => { - document.removeEventListener("keydown", onKey); - document.body.style.overflow = ""; - }; - }, [open, onClose, prev, next]); - - if (!open || images.length === 0) return null; - - const current = images[index] ?? images[0]; - + src, + alt, + images = [], + initialIndex = 0, +}: { + open: boolean; + onClose: () => void; + src?: string; + alt?: string; + images?: Array<{ src: string; alt?: string }>; + initialIndex?: number; +}) { + const gallery = images.length > 0 ? images : src ? [{ src, alt }] : []; + const [idx, setIdx] = useState(initialIndex); + useEffect(() => setIdx(initialIndex), [initialIndex]); + if (!gallery.length) return null; + const current = gallery[idx]; + const hasNav = gallery.length > 1; return ( -
{ - if (e.key === "Escape") onClose(); - }} - tabIndex={-1} + - {/* Close */} - - - {/* Image */} -
+
+ {hasNav && ( + + )} {current.alt + {hasNav && ( + + )} +
- - {/* Counter */} - {images.length > 1 && ( - - {index + 1} / {images.length} - - )} - - {/* Nav */} - {images.length > 1 && ( - <> - - - - )} -
+
); } diff --git a/services/frontend/src/components/messages/message-card.tsx b/services/frontend/src/components/messages/message-card.tsx deleted file mode 100644 index f86ba27..0000000 --- a/services/frontend/src/components/messages/message-card.tsx +++ /dev/null @@ -1,175 +0,0 @@ -"use client"; - -import { Hash } from "lucide-react"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; -import { Card, CardContent } from "@/components/ui/card"; -import { Progress } from "@/components/ui/progress"; -import { - getMessageChannelLabel, - renderMessageContent, - safeParseJsonArray, -} from "@/lib/format"; -import type { MessageRecord } from "@/lib/types"; -import { cn } from "@/lib/utils"; -import { AiStatusBadge } from "./ai-status-badge"; - -export function MessageCard({ - message: msg, - onClick, -}: { - message: MessageRecord; - onClick: (id: string) => void; -}) { - const severity = ( - { - low: "border-l-cyan-500/40", - medium: "border-l-amber-500/60", - high: "border-l-orange-500/70", - critical: "border-l-red-500/80", - } as Record - )[msg.ai_severity ?? ""]; - - return ( - onClick(msg.id)} - > - -
- - - - {msg.username?.charAt(0).toUpperCase() ?? "?"} - - -
-
- {msg.username} - - {msg.created_at - ? new Date(msg.created_at).toLocaleString() - : ""} - - - - {getMessageChannelLabel(msg)} - - - {msg.ai_severity && msg.ai_severity !== "none" && ( - - {msg.ai_severity} - - )} - {msg.type === "deleted" && ( - - deleted - - )} - {(msg.type === "edited" || msg.edited_content) && ( - - edited - - )} -
-

- {renderMessageContent( - msg.edited_content ?? msg.content, - msg.metadata, - )} -

- {(() => { - const u = extractFirstImage(msg.metadata); - if (!u) return null; - return ( - - ); - })()} - {msg.ai_moderation_flags && msg.ai_moderation_flags !== "[]" && ( -
- {safeParseJsonArray(msg.ai_moderation_flags).map((f) => ( - - {f} - - ))} -
- )} - {msg.ai_analysis && ( -

- {msg.ai_analysis} -

- )} - {msg.ai_confidence != null && ( -
- - - {(msg.ai_confidence * 100).toFixed(0)}% - -
- )} -
-
-
-
- ); -} - -export function extractFirstImage( - metadata: string | null | undefined, -): string | null { - if (!metadata) return null; - try { - const m = JSON.parse(metadata); - const atts: Array<{ url: string; contentType?: string }> = - m.attachments ?? []; - return atts.find((a) => a.contentType?.startsWith("image/"))?.url ?? null; - } catch { - return null; - } -} diff --git a/services/frontend/src/components/messages/message-detail-view.tsx b/services/frontend/src/components/messages/message-detail-view.tsx index dbbe4e9..6698471 100644 --- a/services/frontend/src/components/messages/message-detail-view.tsx +++ b/services/frontend/src/components/messages/message-detail-view.tsx @@ -1,102 +1,124 @@ "use client"; -import { ArrowLeft, MessageSquare, MessagesSquare, Pencil } from "lucide-react"; -import { Card } from "@/components/ui/card"; -import { getMessageChannelLabel, renderMessageContent } from "@/lib/format"; -import type { AttachmentRecord, MessageRecord } from "@/lib/types"; +import { useState } from "react"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { + getMessageChannelLabel, + renderMessageContent, + safeParseJsonArray, +} from "@/lib/format"; +import type { AttachmentRef, MessageRecord } from "@/lib/types"; import { cn } from "@/lib/utils"; -import { AiAnalysisPanel } from "./ai-analysis-panel"; +import { AiStatusBadge, SeverityTick } from "./ai-status-badge"; import { AttachmentsGrid } from "./attachments-grid"; +import { Lightbox } from "./lightbox"; -interface MessageDetailViewProps { +function extractAttachments(metadata?: string | null): AttachmentRef[] { + if (!metadata) return []; + try { + const m = JSON.parse(metadata); + return (m?.attachments ?? []) as AttachmentRef[]; + } catch { + return []; + } +} + +function fmtFull(ts?: number): string { + if (!ts) return ""; + return new Date(ts * 1000).toLocaleString(); +} + +export interface MessageDetailProps { message: MessageRecord; - attachments?: AttachmentRecord[]; - onBack?: () => void; - onImageClick?: (index: number) => void; + channelLabel?: string; } export function MessageDetailView({ - message, - attachments, - onBack, - onImageClick, -}: MessageDetailViewProps) { + message: msg, + channelLabel, +}: MessageDetailProps) { + const [img, setImg] = useState(null); + const severity = msg.ai_severity ?? "none"; + const flags = safeParseJsonArray(msg.ai_moderation_flags || "[]"); + return ( - - {onBack && ( - - )} - - {/* Message header */} -
- - - {message.username} - - - {message.thread_id && } - {getMessageChannelLabel(message)} - + <> +
+ +
+
+ {msg.username} + + {fmtFull(msg.created_at)} + + +
+
+ #{channelLabel ?? getMessageChannelLabel(msg)} + {msg.thread_id && ·} + {msg.thread_id && Thread {msg.thread_id.slice(0, 8)}} +
+
- {/* Content */} -
- {renderMessageContent( - message.edited_content ?? message.content, - message.metadata, - ) || "(no text content)"} +
+ +
+ {msg.deleted_at ? ( + + message deleted + + ) : ( + renderMessageContent( + msg.edited_content ?? msg.content, + msg.metadata, + ) + )} +
- {/* Edit history */} - {message.edit_history && message.edit_history.length > 0 && ( -
-

- - Riwayat edit · {message.edit_history.length} versi sebelumnya -

- {message.edit_history.map((edit, i) => ( -
-

- {new Date(edit.edited_at).toLocaleString("id-ID")} -

-

- {renderMessageContent(edit.old_content, message.metadata) || - "(kosong)"} -

-
+ {flags.length > 0 && ( +
+ {flags.map((f) => ( + + {f} + ))}
)} - {/* Attachments */} - {attachments && attachments.length > 0 && ( -
- + {msg.ai_analysis && ( +
+ + AI analysis: + {" "} + + {msg.ai_analysis} +
)} - {/* AI Analysis */} - 0 && ( + setImg(u)} + /> + )} + setImg(null)} + src={img ?? undefined} /> - + ); } diff --git a/services/frontend/src/components/messages/message-detail.tsx b/services/frontend/src/components/messages/message-detail.tsx deleted file mode 100644 index d420daa..0000000 --- a/services/frontend/src/components/messages/message-detail.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { ArrowLeft, MessageSquare, MessagesSquare } from "lucide-react"; -import { Card } from "@/components/ui/card"; -import { getMessageChannelLabel, renderMessageContent } from "@/lib/format"; -import type { AttachmentRecord, MessageRecord } from "@/lib/types"; -import { cn } from "@/lib/utils"; -import { AiAnalysisPanel } from "./ai-analysis-panel"; -import { AttachmentsGrid } from "./attachments-grid"; - -interface MessageDetailProps { - message: MessageRecord; - attachments?: AttachmentRecord[]; - onBack?: () => void; -} - -export function MessageDetail({ - message, - attachments, - onBack, -}: MessageDetailProps) { - return ( - - {onBack && ( - - )} - - {/* Message header */} -
- - - {message.username} - - - {message.thread_id && } - {getMessageChannelLabel(message)} - -
- - {/* Content */} -
- {renderMessageContent( - message.edited_content ?? message.content, - message.metadata, - ) || "(no text content)"} -
- - {/* Attachments */} - {attachments && attachments.length > 0 && ( -
- -
- )} - - {/* AI Analysis */} - -
- ); -} diff --git a/services/frontend/src/components/messages/message-entry.tsx b/services/frontend/src/components/messages/message-entry.tsx new file mode 100644 index 0000000..a84206b --- /dev/null +++ b/services/frontend/src/components/messages/message-entry.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Avatar } from "@/components/primitives/avatar"; +import { renderMessageContent } from "@/lib/format"; +import type { AiSeverity, MessageRecord } from "@/lib/types"; +import { cn } from "@/lib/utils"; +import { AiStatusBadge, SeverityTick } from "./ai-status-badge"; + +function fmtTime(ts?: number): string { + if (!ts) return ""; + return new Date(ts * 1000).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); +} + +const severityColor: Record, string> = { + none: "text-[var(--color-ink-soft)]", + low: "text-[var(--color-amber)]", + medium: "text-[var(--color-amber)]", + high: "text-orange-500", + critical: "text-[var(--color-vermilion)]", +}; + +export interface MessageEntryProps { + message: MessageRecord; + selected: boolean; + onSelect: () => void; + onAvatarClick?: () => void; +} + +export function MessageEntry({ + message: msg, + selected, + onSelect, +}: MessageEntryProps) { + const severity = (msg.ai_severity ?? "none") as AiSeverity; + const status = msg.ai_status ?? null; + + return ( +
+ + +
+
+ {msg.username} + + {fmtTime(msg.created_at)} + + {status && } + {severity !== "none" && ( + + {severity} + + )} +
+
+ {msg.deleted_at ? ( + + message deleted + + ) : ( + renderMessageContent( + msg.edited_content ?? msg.content, + msg.metadata, + ) + )} +
+
+
+ ); +} diff --git a/services/frontend/src/components/messages/message-list.tsx b/services/frontend/src/components/messages/message-list.tsx index 46b4283..cf89900 100644 --- a/services/frontend/src/components/messages/message-list.tsx +++ b/services/frontend/src/components/messages/message-list.tsx @@ -1,45 +1,55 @@ "use client"; -import { Loader2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; import type { MessageRecord } from "@/lib/types"; -import { MessageCard } from "./message-card"; +import { MessageEntry } from "./message-entry"; -interface MessageListProps { +export { MessageEntry }; + +export function MessageList({ + messages, + selectedId, + onSelect, + hasMore, + onLoadMore, + isLoadingMore, +}: { messages: MessageRecord[]; selectedId: string | null; onSelect: (id: string) => void; hasMore?: boolean; onLoadMore?: () => void; isLoadingMore?: boolean; -} - -export function MessageList({ - messages, - selectedId: _selectedId, - onSelect, - hasMore, - onLoadMore, - isLoadingMore, -}: MessageListProps) { +}) { + if (messages.length === 0) { + return ( +
+

+ No messages found. +

+
+ ); + } return ( <> - {messages.map((msg) => ( - - ))} +
+ {messages.map((m) => ( + onSelect(m.id)} + /> + ))} +
{hasMore && ( -
- -
+ )} ); diff --git a/services/frontend/src/components/messages/search-overlay.tsx b/services/frontend/src/components/messages/search-overlay.tsx index 9d17210..38fb643 100644 --- a/services/frontend/src/components/messages/search-overlay.tsx +++ b/services/frontend/src/components/messages/search-overlay.tsx @@ -1,108 +1,82 @@ "use client"; -import { Search, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useMessageSearch } from "@/hooks"; -import { getMessageChannelLabel, renderMessageContent } from "@/lib/format"; +import { Search } from "lucide-react"; +import { useState } from "react"; +import { Dialog } from "@/components/primitives/dialog"; +import { Input } from "@/components/primitives/input"; +import { cn } from "@/lib/utils"; -interface SearchOverlayProps { - open: boolean; - onClose: () => void; - onSelect: (id: string) => void; +export interface Message { + id: string; + content: string; + username: string; + channel: string; + time: string; } -export function SearchOverlay({ open, onClose, onSelect }: SearchOverlayProps) { +export interface SearchOverlayProps { + open: boolean; + onClose: () => void; + results: Message[]; + onSelect: (msg: Message) => void; +} + +export function SearchOverlay({ + open, + onClose, + results, + onSelect, +}: SearchOverlayProps) { const [query, setQuery] = useState(""); - const inputRef = useRef(null); - const { data: results } = useMessageSearch(query, true); - - useEffect(() => { - if (open) { - setTimeout(() => inputRef.current?.focus(), 100); - } else { - setQuery(""); - } - }, [open]); - - useEffect(() => { - const handleKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === "k") { - e.preventDefault(); - onClose(); // this is called when Cmd+K is pressed globally — toggle - } - if (e.key === "Escape") onClose(); - }; - document.addEventListener("keydown", handleKey); - return () => document.removeEventListener("keydown", handleKey); - }, [onClose]); - - if (!open) return null; + const filtered = query + ? results.filter( + (m) => + m.content.toLowerCase().includes(query.toLowerCase()) || + m.username.toLowerCase().includes(query.toLowerCase()), + ) + : results; return ( -
-
- - {/* Results */} -
- {!results || results.length === 0 ? ( -
- {query.length < 2 - ? "Type at least 2 characters" - : "No results found"} -
+
+ {filtered.length === 0 ? ( +

+ No results. +

) : ( - results.map((msg) => ( -
-

- {renderMessageContent(msg.content, msg.metadata)} -

- - )) + + ))} +
)}
-
+ ); } diff --git a/services/frontend/src/components/moderation/moderation-section.tsx b/services/frontend/src/components/moderation/moderation-section.tsx index b57d7c0..bac88d2 100644 --- a/services/frontend/src/components/moderation/moderation-section.tsx +++ b/services/frontend/src/components/moderation/moderation-section.tsx @@ -12,9 +12,8 @@ import { XCircle, } from "lucide-react"; import { useState } from "react"; +import { Badge } from "@/components/primitives/badge"; import { EmptyState, LoadingSkeleton } from "@/components/shared"; -import { Badge } from "@/components/ui/badge"; -import { Card } from "@/components/ui/card"; import { useModerationActions, useModerationStats } from "@/hooks"; import { renderMessageContent } from "@/lib/format"; import type { @@ -26,42 +25,22 @@ import { cn } from "@/lib/utils"; const ACTION_META: Record< ModerationActionType, - { label: string; Icon: typeof Trash2; className: string } + { label: string; Icon: typeof Trash2; tone: "vermilion" | "amber" } > = { - delete_message: { - label: "Delete message", - Icon: Trash2, - className: "text-red-500", - }, - mute_user: { label: "Mute user", Icon: MicOff, className: "text-orange-500" }, - warn_user: { - label: "Warn user", - Icon: AlertTriangle, - className: "text-amber-500", - }, - kick_user: { label: "Kick user", Icon: UserX, className: "text-orange-500" }, - ban_user: { label: "Ban user", Icon: Ban, className: "text-red-500" }, + delete_message: { label: "Delete message", Icon: Trash2, tone: "vermilion" }, + mute_user: { label: "Mute user", Icon: MicOff, tone: "amber" }, + warn_user: { label: "Warn user", Icon: AlertTriangle, tone: "amber" }, + kick_user: { label: "Kick user", Icon: UserX, tone: "amber" }, + ban_user: { label: "Ban user", Icon: Ban, tone: "vermilion" }, }; const STATUS_META: Record< ModerationAction["status"], - { label: string; className: string; dot: string } + { label: string; tone: "signal" | "vermilion" | "amber" } > = { - executed: { - label: "Executed", - className: "border-green-500/40 text-green-500", - dot: "bg-green-500", - }, - failed: { - label: "Failed", - className: "border-red-500/40 text-red-500", - dot: "bg-red-500", - }, - pending: { - label: "Pending", - className: "border-amber-500/40 text-amber-500", - dot: "bg-amber-500", - }, + executed: { label: "Executed", tone: "signal" }, + failed: { label: "Failed", tone: "vermilion" }, + pending: { label: "Pending", tone: "amber" }, }; function fmtTime(ts: number | null): string { @@ -115,31 +94,28 @@ export function ModerationSection({ ]; return ( -
+
{/* Summary cards */} -
- +
+ 0 ? `${s.failed_rate}%` : undefined} /> - +
{/* Filters */}
- + Status {statusFilters.map((f) => ( @@ -154,7 +130,7 @@ export function ModerationSection({ onClick={() => setStatus(f)} /> ))} - + Tipe {typeFilters.map((f) => ( @@ -173,13 +149,13 @@ export function ModerationSection({ {actionsLoading && !actions ? ( ) : !actions || actions.length === 0 ? ( - +
- +
) : (
{actions.map((a) => ( @@ -188,7 +164,7 @@ export function ModerationSection({
)} -

+

{actions?.length ?? 0} aksi ditampilkan · log moderasi gateway Discord

@@ -198,26 +174,34 @@ export function ModerationSection({ function SummaryCard({ label, value, - color, + tone, hint, }: { label: string; value: number; - color: string; + tone?: "signal" | "vermilion" | "amber"; hint?: string; }) { return ( - -

+

+

{label}

-

+

{value} {hint && ( ({hint}) )}

- +
); } @@ -237,8 +221,8 @@ function FilterChip({ className={cn( "rounded-full px-3 py-1 text-[11px] transition-colors", active - ? "bg-primary/20 text-primary" - : "text-text-secondary/60 hover:text-text-primary glass", + ? "bg-[var(--color-signal)] text-[var(--color-signal-ink)]" + : "text-[var(--color-ink-soft)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-ink)]", )} > {label} @@ -251,49 +235,45 @@ function ActionRow({ action }: { action: ModerationAction }) { const st = STATUS_META[action.status]; const Icon = meta.Icon; return ( - - +
+
- + {meta.label} {action.username && ( - + @{action.username} )} - - - {st.label} - + {st.label}
{action.content && ( -

+

{renderMessageContent(action.content, null)}

)} {action.reason && ( -

+

Alasan: {action.reason}

)} {action.error && ( -

+

Error: {action.error}

)} -

+

dibuat {fmtTime(action.created_at)} {action.executed_at ? ` · dieksekusi ${fmtTime(action.executed_at)}` @@ -301,13 +281,13 @@ function ActionRow({ action }: { action: ModerationAction }) {

{action.status === "executed" ? ( - + ) : action.status === "failed" ? ( - + ) : ( - + )} - +
); } diff --git a/services/frontend/src/components/motion/route-transition.tsx b/services/frontend/src/components/motion/route-transition.tsx new file mode 100644 index 0000000..45cd1c0 --- /dev/null +++ b/services/frontend/src/components/motion/route-transition.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { usePathname } from "next/navigation"; +import type { ReactNode } from "react"; +import { ease } from "./variants"; + +export function RouteTransition({ children }: { children: ReactNode }) { + const pathname = usePathname(); + const reduce = useReducedMotion(); + + if (reduce) { + return
{children}
; + } + + return ( + + + {children} + + + ); +} diff --git a/services/frontend/src/components/motion/stagger.tsx b/services/frontend/src/components/motion/stagger.tsx new file mode 100644 index 0000000..86bb79c --- /dev/null +++ b/services/frontend/src/components/motion/stagger.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { motion, type Variants } from "motion/react"; +import type { ReactNode } from "react"; +import { fadeUp, stagger } from "./variants"; + +type V = Variants; + +interface StaggerGroupProps { + children: ReactNode; + className?: string; + variants?: V; + as?: "div" | "ul" | "section"; +} + +export function StaggerGroup({ + children, + className, + variants = stagger, + as = "div", +}: StaggerGroupProps) { + const Tag = motion[as]; + return ( + + {children} + + ); +} + +interface StaggerItemProps { + children: ReactNode; + className?: string; + variants?: V; + layout?: boolean; +} + +export function StaggerItem({ + children, + className, + variants = fadeUp, + layout, +}: StaggerItemProps) { + return ( + + {children} + + ); +} diff --git a/services/frontend/src/components/motion/variants.ts b/services/frontend/src/components/motion/variants.ts new file mode 100644 index 0000000..eb9360f --- /dev/null +++ b/services/frontend/src/components/motion/variants.ts @@ -0,0 +1,31 @@ +import type { Transition, Variants } from "motion/react"; + +/** Spring tuned for UI micro-interactions. */ +export const spring: Transition = { + type: "spring", + stiffness: 260, + damping: 24, +}; + +/** Expressive ease-out for page/section transitions. */ +export const ease = [0.22, 1, 0.36, 1] as const; + +/** Single-element fade + rise. */ +export const fadeUp: Variants = { + hidden: { opacity: 0, y: 8 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.32, ease } }, +}; + +/** Parent that staggers its children. */ +export const stagger: Variants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.06, delayChildren: 0.08 }, + }, +}; + +/** Scale-in for emphasis blocks. */ +export const popIn: Variants = { + hidden: { opacity: 0, scale: 0.94 }, + visible: { opacity: 1, scale: 1, transition: spring }, +}; diff --git a/services/frontend/src/components/primitives/avatar.tsx b/services/frontend/src/components/primitives/avatar.tsx new file mode 100644 index 0000000..d2537ad --- /dev/null +++ b/services/frontend/src/components/primitives/avatar.tsx @@ -0,0 +1,39 @@ +import { cn } from "@/lib/utils"; + +export interface AvatarProps { + src?: string | null; + name?: string | null; + size?: number; + className?: string; +} + +function initials(name?: string | null): string { + if (!name) return "?"; + const parts = name.trim().split(/\s+/); + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); +} + +export function Avatar({ src, name, size = 36, className }: AvatarProps) { + return ( + + {src ? ( + {name + ) : ( + initials(name) + )} + + ); +} diff --git a/services/frontend/src/components/primitives/badge.tsx b/services/frontend/src/components/primitives/badge.tsx new file mode 100644 index 0000000..6f0b767 --- /dev/null +++ b/services/frontend/src/components/primitives/badge.tsx @@ -0,0 +1,42 @@ +import type { ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +export type BadgeTone = "signal" | "amber" | "vermilion" | "neutral"; + +const toneClass: Record = { + signal: "bg-[var(--color-signal)]/15 text-[var(--color-signal)]", + amber: "bg-[var(--color-amber)]/15 text-[var(--color-amber)]", + vermilion: "bg-[var(--color-vermilion)]/15 text-[var(--color-vermilion)]", + neutral: "bg-[var(--color-hairline)] text-[var(--color-ink-soft)]", +}; + +export interface BadgeProps { + tone?: BadgeTone; + children: ReactNode; + className?: string; + dot?: boolean; +} + +export function Badge({ + tone = "neutral", + children, + className, + dot, +}: BadgeProps) { + return ( + + {dot && ( + + )} + {children} + + ); +} diff --git a/services/frontend/src/components/primitives/button.tsx b/services/frontend/src/components/primitives/button.tsx new file mode 100644 index 0000000..b74b926 --- /dev/null +++ b/services/frontend/src/components/primitives/button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { type HTMLMotionProps, motion, useReducedMotion } from "motion/react"; +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; + +type Variant = "primary" | "ghost" | "danger" | "outline"; +type Size = "sm" | "md" | "lg" | "icon"; + +const variantClass: Record = { + primary: + "bg-[var(--color-signal)] text-[var(--color-signal-ink)] hover:opacity-90", + ghost: + "bg-transparent text-[var(--color-ink)] hover:bg-[var(--color-surface-2)]", + danger: "bg-[var(--color-vermilion)] text-white hover:opacity-90", + outline: + "bg-transparent text-[var(--color-ink)] border border-[var(--color-hairline)] hover:bg-[var(--color-surface-2)]", +}; + +const sizeClass: Record = { + sm: "h-8 px-3 text-xs rounded-[var(--radius-r-control)]", + md: "h-10 px-4 text-sm rounded-[var(--radius-r-control)]", + lg: "h-12 px-6 text-base rounded-[var(--radius-r)]", + icon: "size-9 rounded-[var(--radius-r-control)]", +}; + +export interface ButtonProps extends Omit, "ref"> { + variant?: Variant; + size?: Size; +} + +export const Button = forwardRef( + ( + { className, variant = "primary", size = "md", children, ...props }, + ref, + ) => { + const reduce = useReducedMotion(); + return ( + + {children} + + ); + }, +); +Button.displayName = "Button"; diff --git a/services/frontend/src/components/primitives/dialog.tsx b/services/frontend/src/components/primitives/dialog.tsx new file mode 100644 index 0000000..3fa4bcf --- /dev/null +++ b/services/frontend/src/components/primitives/dialog.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { type ReactNode, useEffect, useRef } from "react"; +import { cn } from "@/lib/utils"; + +export interface DialogProps { + open: boolean; + onClose: () => void; + children: ReactNode; + className?: string; + labelledBy?: string; +} + +export function Dialog({ + open, + onClose, + children, + className, + labelledBy, +}: DialogProps) { + const ref = useRef(null); + const reduce = useReducedMotion(); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", onKey); + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = ""; + }; + }, [open, onClose]); + + return ( + + {open && ( + +
+ + {children} + + + )} + + ); +} diff --git a/services/frontend/src/components/primitives/index.ts b/services/frontend/src/components/primitives/index.ts new file mode 100644 index 0000000..73719ce --- /dev/null +++ b/services/frontend/src/components/primitives/index.ts @@ -0,0 +1,11 @@ +export { Avatar, type AvatarProps } from "./avatar"; +export { Badge, type BadgeProps, type BadgeTone } from "./badge"; +export { Button, type ButtonProps } from "./button"; +export { Dialog, type DialogProps } from "./dialog"; +export { Input, type InputProps } from "./input"; +export { Progress, type ProgressProps } from "./progress"; +export { Select, type SelectProps } from "./select"; +export { Sheet, type SheetProps } from "./sheet"; +export { Skeleton, type SkeletonProps } from "./skeleton"; +export { Toaster, type ToasterProps, useToast } from "./toast"; +export { Tooltip, type TooltipProps } from "./tooltip"; diff --git a/services/frontend/src/components/primitives/input.tsx b/services/frontend/src/components/primitives/input.tsx new file mode 100644 index 0000000..6423e68 --- /dev/null +++ b/services/frontend/src/components/primitives/input.tsx @@ -0,0 +1,23 @@ +import { forwardRef, type InputHTMLAttributes } from "react"; +import { cn } from "@/lib/utils"; + +export interface InputProps extends InputHTMLAttributes { + mono?: boolean; +} + +export const Input = forwardRef( + ({ className, mono, ...props }, ref) => ( + + ), +); +Input.displayName = "Input"; diff --git a/services/frontend/src/components/primitives/progress.tsx b/services/frontend/src/components/primitives/progress.tsx new file mode 100644 index 0000000..c5cdd44 --- /dev/null +++ b/services/frontend/src/components/primitives/progress.tsx @@ -0,0 +1,39 @@ +import { cn } from "@/lib/utils"; + +export interface ProgressProps { + value: number; + max?: number; + className?: string; + tone?: "signal" | "amber" | "vermilion"; + showLabel?: boolean; +} + +export function Progress({ + value, + max = 100, + className, + tone = "signal", + showLabel, +}: ProgressProps) { + const pct = Math.max(0, Math.min(100, (value / max) * 100)); + const stroke = { + signal: "var(--color-signal)", + amber: "var(--color-amber)", + vermilion: "var(--color-vermilion)", + }[tone]; + return ( +
+
+
+
+ {showLabel && ( + + {Math.round(pct)}% + + )} +
+ ); +} diff --git a/services/frontend/src/components/primitives/select.tsx b/services/frontend/src/components/primitives/select.tsx new file mode 100644 index 0000000..3280f9e --- /dev/null +++ b/services/frontend/src/components/primitives/select.tsx @@ -0,0 +1,26 @@ +import { forwardRef, type SelectHTMLAttributes } from "react"; +import { cn } from "@/lib/utils"; + +export interface SelectProps extends SelectHTMLAttributes { + mono?: boolean; +} + +export const Select = forwardRef( + ({ className, mono, children, ...props }, ref) => ( + + ), +); +Select.displayName = "Select"; diff --git a/services/frontend/src/components/primitives/sheet.tsx b/services/frontend/src/components/primitives/sheet.tsx new file mode 100644 index 0000000..49944d1 --- /dev/null +++ b/services/frontend/src/components/primitives/sheet.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { type ReactNode, useEffect } from "react"; +import { cn } from "@/lib/utils"; + +export interface SheetProps { + open: boolean; + onClose: () => void; + children: ReactNode; + side?: "left" | "right" | "bottom"; + className?: string; +} + +export function Sheet({ + open, + onClose, + children, + side = "left", + className, +}: SheetProps) { + const reduce = useReducedMotion(); + + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [open, onClose]); + + const dir = + side === "left" + ? { initial: { x: "-100%" }, animate: { x: 0 } } + : side === "right" + ? { initial: { x: "100%" }, animate: { x: 0 } } + : { initial: { y: "100%" }, animate: { y: 0 } }; + + return ( + + {open && ( + +
+ + {children} + + + )} + + ); +} diff --git a/services/frontend/src/components/primitives/skeleton.tsx b/services/frontend/src/components/primitives/skeleton.tsx new file mode 100644 index 0000000..68cc7d9 --- /dev/null +++ b/services/frontend/src/components/primitives/skeleton.tsx @@ -0,0 +1,20 @@ +import { cn } from "@/lib/utils"; + +export interface SkeletonProps { + className?: string; + rounded?: boolean; +} + +export function Skeleton({ className, rounded }: SkeletonProps) { + return ( +
+ ); +} diff --git a/services/frontend/src/components/primitives/toast.tsx b/services/frontend/src/components/primitives/toast.tsx new file mode 100644 index 0000000..c063044 --- /dev/null +++ b/services/frontend/src/components/primitives/toast.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { X } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { cn } from "@/lib/utils"; + +type ToastTone = "signal" | "amber" | "vermilion" | "neutral"; + +interface ToastItem { + id: number; + title?: string; + description?: string; + tone: ToastTone; +} + +interface ToastContextValue { + toast: (t: { + title?: string; + description?: string; + tone?: ToastTone; + }) => void; +} + +const ToastContext = createContext(null); + +export function useToast() { + const ctx = useContext(ToastContext); + if (!ctx) { + return { + toast: (_: { + title?: string; + description?: string; + tone?: ToastTone; + }) => {}, + }; + } + return ctx; +} + +const toneBar: Record = { + signal: "bg-[var(--color-signal)]", + amber: "bg-[var(--color-amber)]", + vermilion: "bg-[var(--color-vermilion)]", + neutral: "bg-[var(--color-ink-soft)]", +}; + +export interface ToasterProps { + position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"; +} + +export function Toaster({ position = "bottom-right" }: ToasterProps) { + const [items, setItems] = useState([]); + const reduce = useReducedMotion(); + + const toast = useCallback( + (t: { title?: string; description?: string; tone?: ToastTone }) => { + const id = Date.now() + Math.random(); + const item: ToastItem = { + id, + tone: t.tone ?? "neutral", + title: t.title, + description: t.description, + }; + setItems((prev) => [...prev, item]); + setTimeout(() => { + setItems((prev) => prev.filter((i) => i.id !== id)); + }, 4500); + }, + [], + ); + + useEffect(() => { + // expose a no-op provider only; actual provider wraps below + }, []); + + const posClass = + position === "bottom-right" + ? "bottom-4 right-4" + : position === "bottom-left" + ? "bottom-4 left-4" + : position === "top-right" + ? "top-4 right-4" + : "top-4 left-4"; + + return ( + +
+ + {items.map((it) => ( + + +
+ {it.title && ( +
+ {it.title} +
+ )} + {it.description && ( +
+ {it.description} +
+ )} +
+ +
+ ))} +
+
+
+ ); +} diff --git a/services/frontend/src/components/primitives/tooltip.tsx b/services/frontend/src/components/primitives/tooltip.tsx new file mode 100644 index 0000000..78c5106 --- /dev/null +++ b/services/frontend/src/components/primitives/tooltip.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { type ReactNode, useState } from "react"; +import { cn } from "@/lib/utils"; + +export interface TooltipProps { + content: ReactNode; + children: ReactNode; + side?: "top" | "bottom" | "left" | "right"; + className?: string; +} + +const sidePos: Record, string> = { + top: "bottom-full left-1/2 -translate-x-1/2 mb-2", + bottom: "top-full left-1/2 -translate-x-1/2 mt-2", + left: "right-full top-1/2 -translate-y-1/2 mr-2", + right: "left-full top-1/2 -translate-y-1/2 ml-2", +}; + +export function Tooltip({ + content, + children, + side = "top", + className, +}: TooltipProps) { + const [show, setShow] = useState(false); + return ( + setShow(true)} + onMouseLeave={() => setShow(false)} + onFocus={() => setShow(true)} + onBlur={() => setShow(false)} + > + {children} + + {content} + + + ); +} diff --git a/services/frontend/src/components/recordings/recording-card.tsx b/services/frontend/src/components/recordings/recording-card.tsx deleted file mode 100644 index d202bad..0000000 --- a/services/frontend/src/components/recordings/recording-card.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; - -import { Download, Loader2, Pause, Play } from "lucide-react"; -import { useState } from "react"; -import { Card } from "@/components/ui/card"; -import { formatBytes } from "@/lib/format"; -import type { VoiceRecording } from "@/lib/types"; -import { cn } from "@/lib/utils"; - -interface RecordingCardProps { - recording: VoiceRecording; - active: boolean; - playing: boolean; - loading: boolean; - onTogglePlay: (id: string) => void; -} - -const BAR_COUNT = 40; -const barBase = (i: number) => 22 + Math.sin(i * 0.45) * 14 + ((i * 7) % 11); - -export function RecordingCard({ - recording, - active, - playing, - loading, - onTogglePlay, -}: RecordingCardProps) { - const [downloading, setDownloading] = useState(false); - const sizeStr = recording.size_bytes - ? formatBytes(recording.size_bytes) - : "--"; - - // Fetch the file (CORS is open on the uploader) → blob → force download with - // the real filename. Falls back to opening the URL in a new tab. - const handleDownload = async (e: React.MouseEvent) => { - e.stopPropagation(); - if (!recording.download_url || downloading) return; - setDownloading(true); - try { - const res = await fetch(recording.download_url); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const blob = await res.blob(); - const objUrl = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = objUrl; - a.download = recording.filename ?? `recording-${recording.id}.mp3`; - document.body.appendChild(a); - a.click(); - a.remove(); - setTimeout(() => URL.revokeObjectURL(objUrl), 30_000); - } catch { - window.open(recording.download_url, "_blank", "noopener,noreferrer"); - } finally { - setDownloading(false); - } - }; - - return ( - onTogglePlay(recording.id)} - > -
- - -
-
- - {recording.username} - - - {recording.channel_name} - - {active && ( - - {loading ? "Loading" : playing ? "Now Playing" : "Paused"} - - )} -
- - {/* Waveform — bounces while playing, pulses while loading */} -
- {Array.from({ length: BAR_COUNT }, (_, i) => ( -
- ))} -
- -
- - {sizeStr} - - - {new Date(recording.created_at).toLocaleString()} - -
-
- - {/* biome-ignore lint/a11y/noStaticElementInteractions: stopPropagation container — prevents card play toggle when clicking action buttons */} - {/* biome-ignore lint/a11y/useKeyWithClickEvents: no keyboard interaction — container only swallows clicks destined for the action buttons */} -
e.stopPropagation()} - > - {recording.download_url && ( - - )} -
-
- - ); -} diff --git a/services/frontend/src/components/recordings/recording-player.tsx b/services/frontend/src/components/recordings/recording-player.tsx index f99f9f0..8045c23 100644 --- a/services/frontend/src/components/recordings/recording-player.tsx +++ b/services/frontend/src/components/recordings/recording-player.tsx @@ -1,149 +1,78 @@ "use client"; -import { Loader2, Pause, Play, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { Card } from "@/components/ui/card"; -import { cn } from "@/lib/utils"; +/** + * RecordingPlayer — canonical single-recording row component. + */ +import { Delete, Download, Play } from "lucide-react"; +import { Waveform } from "@/components/charts/waveform"; +import { Avatar } from "@/components/primitives/avatar"; +import { Badge } from "@/components/primitives/badge"; +import { Button } from "@/components/primitives/button"; +import type { VoiceRecording } from "@/lib/types"; -interface RecordingPlayerProps { - url?: string; - filename?: string; - playing: boolean; - loading: boolean; - audioRef: React.RefObject; - onToggle: () => void; - onStateChange: (s: { playing: boolean; loading: boolean }) => void; - onClose: () => void; +export interface RecordingPlayerProps { + recording: VoiceRecording; + onSelect?: (rec: VoiceRecording) => void; + onDelete?: (rec: VoiceRecording) => void; + deleting?: boolean; } export function RecordingPlayer({ - url, - filename, - playing, - loading, - audioRef, - onToggle, - onStateChange, - onClose, + recording, + onSelect, + onDelete, + deleting, }: RecordingPlayerProps) { - const [progress, setProgress] = useState(0); - const [duration, setDuration] = useState(0); - const [error, setError] = useState(false); - const timerRef = useRef | null>(null); - - // Load the track whenever the URL changes; the click that opened the player - // counts as a user gesture so autoplay is allowed. - useEffect(() => { - const audio = audioRef.current; - if (!url || !audio) return; - setError(false); - setProgress(0); - setDuration(0); - audio.src = url; - audio.load(); - const p = audio.play(); - if (p) p.catch(() => {}); - }, [url, audioRef]); - - // Progress ticker + cleanup. - useEffect(() => { - if (timerRef.current) clearInterval(timerRef.current); - timerRef.current = setInterval(() => { - const audio = audioRef.current; - if (!audio) return; - if (duration === 0 && !Number.isNaN(audio.duration)) - setDuration(audio.duration); - if (!Number.isNaN(audio.currentTime)) setProgress(audio.currentTime); - }, 250); - return () => { - if (timerRef.current) clearInterval(timerRef.current); - }; - }, [duration, audioRef]); - - if (!url) return null; - - const fmt = (s: number) => { - if (!Number.isFinite(s) || s <= 0) return "0:00"; - const m = Math.floor(s / 60); - const ss = Math.floor(s % 60); - return `${m}:${String(ss).padStart(2, "0")}`; - }; - const pct = duration > 0 ? Math.min(100, (progress / duration) * 100) : 0; - return ( - -
- - -
-
- {filename ?? "recording"} -
-
- - {fmt(progress)} / {fmt(duration)} - - {loading && ( - loading… - )} - {error && ( - - playback failed - - )} -
-
- - -
- - {/* Progress bar */} -
-
-
- - {/* Hidden audio element drives everything above. */} -
); } diff --git a/services/frontend/src/components/shared/error-boundary.tsx b/services/frontend/src/components/shared/error-boundary.tsx index b8c6c6d..d4b732a 100644 --- a/services/frontend/src/components/shared/error-boundary.tsx +++ b/services/frontend/src/components/shared/error-boundary.tsx @@ -1,8 +1,5 @@ -"use client"; - import { AlertCircle, RefreshCw } from "lucide-react"; import { Component, type ReactNode } from "react"; -import { Card } from "@/components/ui/card"; import { cn } from "@/lib/utils"; interface Props { @@ -25,26 +22,19 @@ export class ErrorBoundary extends Component { if (this.state.hasError) { return ( this.props.fallback || ( - - -

+

+ +

{this.state.error?.message || "Something went wrong"}

- +
) ); } diff --git a/services/frontend/src/components/shared/error-state.tsx b/services/frontend/src/components/shared/error-state.tsx index a7a7993..1a6bf6f 100644 --- a/services/frontend/src/components/shared/error-state.tsx +++ b/services/frontend/src/components/shared/error-state.tsx @@ -1,21 +1,18 @@ import { AlertCircle, RefreshCw } from "lucide-react"; - -import { Button } from "@/components/ui/button"; +import { Button } from "@/components/primitives/button"; interface ErrorStateProps { message: string; onRetry?: () => void; } -/** - * Consistent error state for data-fetching pages. - * Shows the error message with an optional retry button. - */ export function ErrorState({ message, onRetry }: ErrorStateProps) { return (
- -

{message}

+ +

+ {message} +

{onRetry && (
); diff --git a/services/frontend/src/components/three/index.tsx b/services/frontend/src/components/three/index.tsx new file mode 100644 index 0000000..184d763 --- /dev/null +++ b/services/frontend/src/components/three/index.tsx @@ -0,0 +1,49 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type { OrbFieldProps } from "./orb-field"; +import type { SignalFieldProps } from "./signal-field"; +import { StaticFallback } from "./static-fallback"; +import { WebGLGuard } from "./webgl-guard"; + +const SignalFieldImpl = dynamic( + () => import("./signal-field").then((m) => m.SignalField), + { + ssr: false, + loading: () => null, + }, +); + +const OrbFieldImpl = dynamic( + () => import("./orb-field").then((m) => m.OrbField), + { + ssr: false, + loading: () => null, + }, +); + +export function SignalField(props: SignalFieldProps) { + return ( + } + > + + + ); +} + +export function OrbField(props: OrbFieldProps) { + return ( + + } + > + + + ); +} diff --git a/services/frontend/src/components/three/orb-field.tsx b/services/frontend/src/components/three/orb-field.tsx new file mode 100644 index 0000000..820d507 --- /dev/null +++ b/services/frontend/src/components/three/orb-field.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useRef } from "react"; +import * as THREE from "three"; +import { useThreeScene } from "./use-three-scene"; + +export interface OrbSpeaker { + id: string; + name: string; + speaking: boolean; + severity?: "none" | "low" | "medium" | "high" | "critical"; +} + +export interface OrbFieldProps { + speakers: OrbSpeaker[]; + className?: string; +} + +const severityColor: Record = { + none: "oklch(0.82 0.18 125)", + low: "oklch(0.80 0.15 70)", + medium: "oklch(0.78 0.16 70)", + high: "oklch(0.70 0.2 35)", + critical: "oklch(0.66 0.22 25)", +}; + +/** + * Voice page hero. Each speaker is a glowing orb; when speaking it rises and + * its ring radius expands. Warm palette only. + */ +export function OrbField({ speakers, className }: OrbFieldProps) { + const ref = useRef(null); + const speakersRef = useRef(speakers); + speakersRef.current = speakers; + + useThreeScene(ref, { + setup: (ctx) => { + const group = new THREE.Group(); + ctx.scene.add(group); + + const orbMeshes: Record = {}; + const ringMeshes: Record = {}; + + const layout = () => { + const list = speakersRef.current; + const n = Math.max(list.length, 1); + list.forEach((sp, i) => { + const angle = (i / n) * Math.PI * 2; + const radius = n === 1 ? 0 : 2.6; + const x = Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + + if (!orbMeshes[sp.id]) { + const geo = new THREE.SphereGeometry(0.5, 32, 32); + const mat = new THREE.MeshStandardMaterial({ + color: new THREE.Color(severityColor[sp.severity ?? "none"]), + emissive: new THREE.Color(severityColor[sp.severity ?? "none"]), + emissiveIntensity: 0.6, + roughness: 0.4, + metalness: 0, + }); + const orb = new THREE.Mesh(geo, mat); + orb.position.set(x, 0, z); + group.add(orb); + orbMeshes[sp.id] = orb; + + const ringGeo = new THREE.TorusGeometry(0.75, 0.03, 16, 64); + const ringMat = new THREE.MeshBasicMaterial({ + color: new THREE.Color(severityColor[sp.severity ?? "none"]), + transparent: true, + opacity: 0.5, + }); + const ring = new THREE.Mesh(ringGeo, ringMat); + ring.rotation.x = Math.PI / 2; + ring.position.set(x, 0, z); + group.add(ring); + ringMeshes[sp.id] = ring; + } else { + orbMeshes[sp.id].position.x = x; + orbMeshes[sp.id].position.z = z; + ringMeshes[sp.id].position.x = x; + ringMeshes[sp.id].position.z = z; + } + }); + // remove orbs no longer present + for (const id of Object.keys(orbMeshes)) { + if (!list.find((s) => s.id === id)) { + group.remove(orbMeshes[id]); + (orbMeshes[id].geometry as THREE.BufferGeometry).dispose(); + group.remove(ringMeshes[id]); + (ringMeshes[id].geometry as THREE.BufferGeometry).dispose(); + delete orbMeshes[id]; + delete ringMeshes[id]; + } + } + }; + layout(); + + const light = new THREE.PointLight(0xffffff, 1.2, 50); + light.position.set(0, 4, 6); + ctx.scene.add(light); + const amb = new THREE.AmbientLight(0xffffff, 0.4); + ctx.scene.add(amb); + + (ctx as any)._layout = layout; + (ctx as any)._orbs = orbMeshes; + (ctx as any)._rings = ringMeshes; + + return () => {}; + }, + onFrame: (ctx, t) => { + const layout = (ctx as any)._layout as () => void; + const orbs = (ctx as any)._orbs as Record; + const rings = (ctx as any)._rings as Record; + // relayout in case speaker set changed + layout(); + for (const sp of speakersRef.current) { + const orb = orbs[sp.id]; + const ring = rings[sp.id]; + if (!orb || !ring) continue; + const targetY = sp.speaking + ? 0.6 + Math.sin(t * 4 + (sp.id.charCodeAt(0) || 1)) * 0.15 + : 0; + orb.position.y += (targetY - orb.position.y) * 0.1; + const ringScale = sp.speaking ? 1.25 + Math.sin(t * 5) * 0.1 : 1; + ring.scale.setScalar(ringScale); + (ring.material as THREE.MeshBasicMaterial).opacity = sp.speaking + ? 0.7 + : 0.3; + } + ctx.scene.rotation.y = t * 0.08; + }, + }); + + return
; +} diff --git a/services/frontend/src/components/three/signal-field.tsx b/services/frontend/src/components/three/signal-field.tsx new file mode 100644 index 0000000..2db52d8 --- /dev/null +++ b/services/frontend/src/components/three/signal-field.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useRef } from "react"; +import * as THREE from "three"; +import { useThreeScene } from "./use-three-scene"; + +export interface SignalFieldProps { + /** 0..1 — scales particle pulse speed + opacity */ + activity?: number; + className?: string; +} + +/** + * Dashboard hero. A particle field whose idle rotation + breathing pulse + * reflects live activity. Warm signal-lime palette, additive glow, no harsh + * white. Pointer parallax via camera lerp. + */ +export function SignalField({ activity = 0.4, className }: SignalFieldProps) { + const ref = useRef(null); + const activityRef = useRef(activity); + activityRef.current = activity; + + useThreeScene(ref, { + setup: (ctx) => { + const w = ctx.width; + const h = ctx.height; + const area = w * h; + const count = Math.min(900, Math.max(220, Math.floor(area / 2000))); + + const positions = new Float32Array(count * 3); + const phases = new Float32Array(count); + const radius = 4.2; + for (let i = 0; i < count; i++) { + const r = radius * (0.25 + Math.random() * 0.75); + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + positions[i * 3] = r * Math.sin(phi) * Math.cos(theta); + positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta) * 0.6; + positions[i * 3 + 2] = r * Math.cos(phi); + phases[i] = Math.random() * Math.PI * 2; + } + + const geo = new THREE.BufferGeometry(); + geo.setAttribute("position", new THREE.BufferAttribute(positions, 3)); + + const mat = new THREE.PointsMaterial({ + color: new THREE.Color("oklch(0.82 0.18 125)"), + size: 0.045, + transparent: true, + opacity: 0.85, + blending: THREE.AdditiveBlending, + depthWrite: false, + }); + + const points = new THREE.Points(geo, mat); + ctx.scene.add(points); + + const key = { x: 0, y: 0 }; + const onMove = (e: PointerEvent) => { + const rect = ctx.container.getBoundingClientRect(); + key.x = ((e.clientX - rect.left) / rect.width - 0.5) * 2; + key.y = ((e.clientY - rect.top) / rect.height - 0.5) * 2; + }; + ctx.container.addEventListener("pointermove", onMove); + + (ctx as any)._key = key; + (ctx as any)._points = points; + (ctx as any)._phases = phases; + + return () => { + ctx.container.removeEventListener("pointermove", onMove); + }; + }, + onFrame: (ctx, t) => { + const points = (ctx as any)._points as THREE.Points; + const key = (ctx as any)._key as { x: number; y: number }; + const phases = (ctx as any)._phases as Float32Array; + const act = activityRef.current; + const pulse = 1 + Math.sin(t * (1.2 + act * 2.2)) * 0.08 * (0.5 + act); + points.scale.setScalar(pulse); + points.rotation.y = t * (0.05 + act * 0.12); + points.rotation.x = Math.sin(t * 0.2) * 0.1; + // parallax + ctx.camera.position.x += (key.x * 1.4 - ctx.camera.position.x) * 0.04; + ctx.camera.position.y += (-key.y * 1.0 - ctx.camera.position.y) * 0.04; + ctx.camera.lookAt(0, 0, 0); + }, + }); + + return
; +} diff --git a/services/frontend/src/components/three/static-fallback.tsx b/services/frontend/src/components/three/static-fallback.tsx new file mode 100644 index 0000000..e9ffc99 --- /dev/null +++ b/services/frontend/src/components/three/static-fallback.tsx @@ -0,0 +1,94 @@ +"use client"; + +export interface StaticFallbackProps { + variant?: "signal" | "orb"; + count?: number; + className?: string; +} + +/** + * 2D SVG silhouette used when WebGL is unavailable — so the hero still reads + * as a living visual, never blank. `variant="signal"` = drifting dot grid; + * `variant="orb"` = speaker orbs. + */ +export function StaticFallback({ + variant = "signal", + count = 60, + className, +}: StaticFallbackProps) { + if (variant === "orb") { + const orbs = Array.from({ length: count > 12 ? 12 : count }, (_, i) => { + const angle = (i / 12) * Math.PI * 2; + const r = 60; + return { + x: 100 + Math.cos(angle) * r, + y: 100 + Math.sin(angle) * r, + d: i * 0.3, + }; + }); + return ( + + + {orbs.map((o, i) => ( + + + + + ))} + + ); + } + + const dots = Array.from({ length: count }, (_, i) => ({ + x: (i * 53) % 200, + y: (i * 89) % 200, + r: 1.5 + ((i * 7) % 3), + })); + return ( + + + {dots.map((d, i) => ( + + + + ))} + + ); +} diff --git a/services/frontend/src/components/three/use-three-scene.ts b/services/frontend/src/components/three/use-three-scene.ts new file mode 100644 index 0000000..79f9620 --- /dev/null +++ b/services/frontend/src/components/three/use-three-scene.ts @@ -0,0 +1,135 @@ +import { useEffect, useRef } from "react"; +import * as THREE from "three"; + +export interface ThreeSceneOptions { + /** Called once after renderer/scene/camera are created. */ + setup: (ctx: ThreeSceneCtx) => (() => void) | void; + /** Optional per-frame callback. */ + onFrame?: (ctx: ThreeSceneCtx, t: number) => void; + background?: string; +} + +export interface ThreeSceneCtx { + renderer: THREE.WebGLRenderer; + scene: THREE.Scene; + camera: THREE.PerspectiveCamera; + container: HTMLDivElement; + width: number; + height: number; +} + +/** + * Shared Three.js lifecycle hook: + * - capped DPR [1, 1.75], high-performance hint + * - RAF loop paused when tab hidden + * - resize observer + * - full geometry/material/renderer dispose on unmount + * + * `setup` may return a cleanup fn (e.g. to remove its own listeners). + */ +export function useThreeScene( + containerRef: React.RefObject, + opts: ThreeSceneOptions, +) { + const optsRef = useRef(opts); + optsRef.current = opts; + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let disposed = false; + let cleanup: (() => void) | void; + let raf = 0; + let ctx: ThreeSceneCtx; + + const init = () => { + const width = container.clientWidth || 1; + const height = container.clientHeight || 1; + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: true, + powerPreference: "high-performance", + }); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.75)); + renderer.setSize(width, height); + container.appendChild(renderer.domElement); + renderer.domElement.style.display = "block"; + renderer.domElement.style.width = "100%"; + renderer.domElement.style.height = "100%"; + + const scene = new THREE.Scene(); + if (optsRef.current.background) { + scene.background = new THREE.Color(optsRef.current.background); + } + scene.fog = new THREE.FogExp2(0x000000, 0.06); + + const camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 100); + camera.position.set(0, 0, 6); + + ctx = { renderer, scene, camera, container, width, height }; + const c = optsRef.current.setup(ctx); + if (typeof c === "function") cleanup = c; + + const start = performance.now(); + const loop = () => { + if (disposed || document.hidden) { + raf = requestAnimationFrame(loop); + return; + } + const t = (performance.now() - start) / 1000; + optsRef.current.onFrame?.(ctx, t); + renderer.render(scene, camera); + raf = requestAnimationFrame(loop); + }; + raf = requestAnimationFrame(loop); + + const ro = new ResizeObserver(() => { + const w = container.clientWidth || 1; + const h = container.clientHeight || 1; + ctx.width = w; + ctx.height = h; + renderer.setSize(w, h); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + }); + ro.observe(container); + + const onVis = () => { + /* loop checks document.hidden */ + }; + document.addEventListener("visibilitychange", onVis); + + (ctx as any)._ro = ro; + (ctx as any)._onVis = onVis; + }; + + init(); + + return () => { + disposed = true; + cancelAnimationFrame(raf); + if (typeof cleanup === "function") cleanup(); + const c = ctx as any; + if (c?._ro) c._ro.disconnect(); + if (c?._onVis) document.removeEventListener("visibilitychange", c._onVis); + if (ctx) { + ctx.scene.traverse((obj) => { + const mesh = obj as THREE.Mesh; + if (mesh.geometry) mesh.geometry.dispose?.(); + const mat = mesh.material as + | THREE.Material + | THREE.Material[] + | undefined; + if (Array.isArray(mat)) mat.forEach((m) => m.dispose()); + else mat?.dispose?.(); + }); + ctx.renderer.dispose(); + if (ctx.renderer.domElement.parentNode === container) { + container.removeChild(ctx.renderer.domElement); + } + } + }; + }, [containerRef]); +} diff --git a/services/frontend/src/components/three/webgl-guard.tsx b/services/frontend/src/components/three/webgl-guard.tsx new file mode 100644 index 0000000..08c4898 --- /dev/null +++ b/services/frontend/src/components/three/webgl-guard.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { type ReactNode, useEffect, useRef } from "react"; + +export interface WebGLGuardProps { + children: ReactNode; + fallback: ReactNode; +} + +/** + * Detects WebGL support. If unavailable (old device / privacy browser / + * headless without GPU), renders `fallback` instead of the 3D scene so the + * page is never blank. + */ +export function WebGLGuard({ children, fallback }: WebGLGuardProps) { + const supported = useRef(null); + + if (supported.current === null) { + if (typeof window === "undefined") { + supported.current = false; + } else { + try { + const canvas = document.createElement("canvas"); + supported.current = !!( + window.WebGLRenderingContext && + (canvas.getContext("webgl2") || canvas.getContext("webgl")) + ); + } catch { + supported.current = false; + } + } + } + + return <>{supported.current ? children : fallback}; +} diff --git a/services/frontend/src/components/ui/accordion.tsx b/services/frontend/src/components/ui/accordion.tsx deleted file mode 100644 index 597ef3c..0000000 --- a/services/frontend/src/components/ui/accordion.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion" - -import { cn } from "@/lib/utils" -import { ChevronDownIcon, ChevronUpIcon } from "lucide-react" - -function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { - return ( - - ) -} - -function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { - return ( - - ) -} - -function AccordionTrigger({ - className, - children, - ...props -}: AccordionPrimitive.Trigger.Props) { - return ( - - - {children} - - - - - ) -} - -function AccordionContent({ - className, - children, - ...props -}: AccordionPrimitive.Panel.Props) { - return ( - -
- {children} -
-
- ) -} - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/services/frontend/src/components/ui/alert-dialog.tsx b/services/frontend/src/components/ui/alert-dialog.tsx deleted file mode 100644 index 05dce24..0000000 --- a/services/frontend/src/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,187 +0,0 @@ -"use client" - -import * as React from "react" -import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" - -function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { - return -} - -function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { - return ( - - ) -} - -function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { - return ( - - ) -} - -function AlertDialogOverlay({ - className, - ...props -}: AlertDialogPrimitive.Backdrop.Props) { - return ( - - ) -} - -function AlertDialogContent({ - className, - size = "default", - ...props -}: AlertDialogPrimitive.Popup.Props & { - size?: "default" | "sm" -}) { - return ( - - - - - ) -} - -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogMedia({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogAction({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function CarouselNext({ - className, - variant = "outline", - size = "icon-sm", - ...props -}: React.ComponentProps) { - const { orientation, scrollNext, canScrollNext } = useCarousel() - - return ( - - ) -} - -export { - type CarouselApi, - Carousel, - CarouselContent, - CarouselItem, - CarouselPrevious, - CarouselNext, - useCarousel, -} diff --git a/services/frontend/src/components/ui/chart.tsx b/services/frontend/src/components/ui/chart.tsx deleted file mode 100644 index 7c2dc84..0000000 --- a/services/frontend/src/components/ui/chart.tsx +++ /dev/null @@ -1,373 +0,0 @@ -"use client" - -import * as React from "react" -import * as RechartsPrimitive from "recharts" -import type { TooltipValueType } from "recharts" - -import { cn } from "@/lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -const INITIAL_DIMENSION = { width: 320, height: 200 } as const -type TooltipNameType = number | string - -export type ChartConfig = Record< - string, - { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -> - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a ") - } - - return context -} - -function ChartContainer({ - id, - className, - children, - config, - initialDimension = INITIAL_DIMENSION, - ...props -}: React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] - initialDimension?: { - width: number - height: number - } -}) { - const uniqueId = React.useId() - const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) -} - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme ?? config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( -