diff --git a/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md b/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md new file mode 100644 index 0000000..67d30f9 --- /dev/null +++ b/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md @@ -0,0 +1,418 @@ +# GMW Frontend — Greenfield Rebuild (Visual-System Overhaul + Custom UI + Motion/3D) + +> **For Hermes:** Execute with `subagent-driven-development` (one fresh subagent per task, two-stage review). Each task is 1–3 min, atomic, independently verifiable, committed after each. Reuse `src/lib/api/*`, `src/lib/ws/*`, `src/lib/types/*`, hooks verbatim. Never invent endpoints. + +**Goal:** Rebuild the GMW Discord-automod dashboard frontend from scratch — drop shadcn/ui + glass/teal/purple aesthetic entirely, replace with a custom, distinctive design system where every page has its own visual metaphor (no uniform bordered-card grid), and push the presentation layer with **Framer Motion (`motion`) choreography + signature Three.js scenes**. Re-integrate to the EXISTING backend API + WebSocket contract (do NOT touch the backend). + +**Architecture:** Next.js 16 App Router (SSR) + React 19 + TS strict + Tailwind v4. Keep the *plumbing* (data contract), rebuild the *skin + primitives + motion*. Each route = one self-contained `page.tsx` (server fetch + client view in the same file via a `"use client"` sibling export). Custom SVG charts (no recharts). Custom micro-primitives (no shadcn/base-ui). New token system in `globals.css`. **Motion:** `motion/react` app-wide for page transitions, spring micro-interactions, layout animation. **3D:** raw `three` (no react-three-fiber — leaner) in exactly TWO signature scenes, lazy-loaded client-only with graceful fallback. Deployed unchanged via existing flake + `gmw-proxy` nginx (`:4009` → Next `:4017`). + +**Tech Stack:** next@16, react@19, tailwindcss@4 (`@import "tailwindcss"`), `next/font/google` (Bricolage Grotesque + Inter + JetBrains Mono), `swr` (data revalidation), `lucide-react` (icons only), `motion` (Framer Motion successor — `motion/react`), `three` + `@types/three` (signature scenes only), `clsx` + `tailwind-merge`. **Removed:** `@shadcn/react`, `@base-ui/react`, `recharts`, `shadcn` CLI, `cmdk`, `sonner`, `react-day-picker`, `embla-carousel-react`, `react-resizable-panels`, `input-otp`, all 50 `components/ui/*`. + +--- + +## 0. Design System (the creative core — read before coding) + +**Persona:** Controlled UX Designer + a tactical "ops console" voice. Material honesty: hierarchy via **scale/weight/tonal blocks**, NOT borders/shadows. Per `frontend-design` skill principle — spend the boldness in ONE signature place per page, keep the rest disciplined. Motion is choreography, not confetti: **one orchestrated moment per page**, everything else quiet. + +### Palette (warm, signal-driven — NO teal/cyan/purple/blue gradients) +Light mode (`:root`): +``` +--canvas: oklch(0.96 0.012 80) /* warm off-white, not cream */ +--surface: oklch(0.92 0.014 80) /* tonal block, replaces bordered card */ +--surface-2: oklch(0.88 0.016 80) +--ink: oklch(0.22 0.02 70) /* primary text */ +--ink-soft: oklch(0.46 0.02 70) /* secondary text */ +--hairline: oklch(0.22 0.02 70 / 0.10) /* structural rules ONLY, sparse */ +--signal: oklch(0.78 0.17 125) /* lime — OK / live / primary accent */ +--signal-ink: oklch(0.20 0.03 70) /* text ON signal */ +--amber: oklch(0.80 0.15 70) /* WARN */ +--vermilion: oklch(0.62 0.21 25) /* FLAGGED / destructive */ +--ring: var(--signal) +``` +Dark mode (`.dark`, default theme per `next-themes`): +``` +--canvas: oklch(0.13 0.015 70) /* warm charcoal, not blue-black */ +--surface: oklch(0.18 0.02 70) +--surface-2: oklch(0.23 0.022 70) +--ink: oklch(0.93 0.01 75) +--ink-soft: oklch(0.62 0.02 75) +--hairline: oklch(1 0 0 / 0.09) +--signal: oklch(0.88 0.18 125) +--signal-ink: oklch(0.18 0.03 70) +--amber: oklch(0.85 0.15 70) +--vermilion: oklch(0.68 0.21 25) +``` +Three semantic signals reused everywhere: **lime = OK/live, amber = warn, vermilion = flag/danger**. This kills the purple-accent + teal-primary monotony. + +### Typography (3 roles, deliberate pairing — not "Inter everywhere") +- **Display:** `Bricolage Grotesque` (700–800) — characterful grotesque for headers/big numbers. +- **Body/UI:** `Inter` (400–600). +- **Data/label:** `JetBrains Mono` (500/700) — all stats, timestamps, channel IDs, metrics. +Load all three via `next/font/google` with CSS variables (keep current `--font-inter`/`--font-jetbrains-mono` names + add `--font-display`). + +### Layout & signature +- **No `card` with border.** Use tonal `--surface` blocks with generous radius (`--r: 14px`) and internal padding; separate blocks with whitespace + sparse hairlines only where structurally meaningful. +- **Signature element = "scan-tick":** a 1px animated pulse line (CSS keyframe `scan`) that marks every live/section header — NOT a card outline. Global canvas carries a faint warm dot-grid texture (low opacity) instead of the current bluish dotted radial-gradient. +- **Nav = left "spine":** vertical rail of icon nodes joined by a hairline; active node gets a `--signal` dot + label reveal. Collapses to a bottom tab-bar < 768px (CSS only, no JS sidebar primitive). +- **Header = "status bar":** connection state (WS dot), guild selector, live clock — mono font, reads like an instrument readout. + +## 0.1 Motion & 3D Layer (the "lebih kreatif" addition) + +### Motion rules (from `motion/react`) +- **Page transitions:** one shared `RouteTransition` in `(dashboard)/layout.tsx` — `AnimatePresence mode="popLayout"` + `motion.div key={pathname}` (fade + 8px rise + slight blur-out, ~220ms, `easeOut`). Consistent everywhere, zero per-page boilerplate. +- **Enter choreography (per page, ONE signature moment):** staggered rise-in for the hero/ticker group using `staggerChildren` variants; afterwards, quiet springs for hover/tap (`scale: 1.03` on interactive blocks, `whileTap` on buttons). +- **Layout animation:** `layout` prop on list items (messages rows, recording rows, queue) so add/remove/filter reflows smoothly; `layoutId` for shared-element transitions (ticker → detail modal on dashboard). +- **Live pulse:** `motion` drives the severity ticks / speaker rings with springs, not CSS `transition` alone. +- **`useReducedMotion()`** (from `motion/react`) gates ALL heavy motion; CSS `@media (prefers-reduced-motion: reduce)` additionally kills `scan`/`spin-disc` keyframes. Accessibility floor, non-negotiable. +- **No scroll-jacking, no marquee loops, no per-element confetti.** One moment per page. (`frontend-design` skill: "Satu momen orkestrasi biasanya lebih mengena daripada efek tersebar.") + +### 3D rules (raw `three`, no R3F — lean bundle) +- Exactly **two** scenes, chosen because they carry real data meaning: **Dashboard hero** (`SignalField` — a particle field whose pulse density reflects live activity) and **Voice page** (`OrbField` — speakers as glowing orbs whose height/ring radius reacts to who is speaking). Everything else stays 2D/motion. +- **Lazy + client-only:** `next/dynamic(() => import("./SignalField"), { ssr: false, loading: () => })`. Three ships in its own chunk, loaded only on those two routes. +- **WebGL guard:** if `!window.WebGLRenderingContext` or context creation fails → render the static SVG/CSS fallback (a stylized 2D version of the same visual). Never blank. +- **Perf guardrails:** `dpr: [1, 1.75]`, `powerPreference: "high-performance"`, `antialias: true`; RAF loop paused on `document.hidden`; `dispose()` geometries/materials on unmount; particle count capped by `navigator.hardwareConcurrency` + viewport (`Math.min(900, w*h/2000)`). +- **Style:** warm palette ONLY — signal-lime particles, amber/vermilion for flag/warn states; fog + soft additive blending for glow (no harsh white lights, no metallic PBR). +- **Interactivity:** subtle pointer parallax (camera lerp toward cursor) + gentle idle rotation. No drag/drop, no raycasting menus. + +### Per-page metaphor (kills monotony — each page feels different) +| Route | Metaphor | Signature visual | Motion / 3D | +|---|---|---|---| +| `/dashboard` | Live ops overview | **3D signal particle field** hero + asymmetric ticker blocks + radial moderation gauge | **3D SignalField** (reacts to activity), staggered ticker rise-in, gauge draws on mount | +| `/messages` | Transcript | Left channel **timeline spine**; right = flowing message entries with left **severity tick** (no bordered cards); search = command palette | `layout` on message rows, spring severity ticks, palette types in | +| `/voice` | Stage | **3D orb field** of speakers + equalizer rings; activity = horizontal **session ribbon** | **3D OrbField** (speaking → orb rises + ring pulses), session ribbon draws sequentially | +| `/media` | Turntable | Rotating **disc** now-playing; queue = borderless list | CSS 3D disc spin (spring on play/pause), queue `layout` reflow, progress bar springs | +| `/recordings` | Tape library | Rows with **waveform thumbnail** (custom SVG from duration) | Waveform bars spring on hover; new upload animates in (AnimatePresence) | +| `/moderation` | Security log | Vertical **event flow** with status nodes (dot + line), not a table of cards | Nodes pulse on live action; timeline draws in sequence | +| `/analysis` | Query console | Terminal-style search panel | Typing cursor + results stagger | + +--- + +## 1. What to KEEP (reuse verbatim — correct + integrates to backend) +- `src/lib/api/server.ts` — 11 server fetchers (`getDashboardStats`, `getActivity`, `getMediaStatus`, `getConfig`, `getModerationStats/Actions`, `getGuilds`, `getVoiceStatus`, `getRecordings`, `getMessages`). **No change.** +- `src/lib/api/client.ts` — `apiRequest` + `ApiError`. **No change.** +- `src/lib/ws/*` — `connection.ts`, `context.tsx`, `types.ts` (17 typed events: `message_created/updated/deleted/analyzed`, `voice_*`, `media_state`, `voice_pcm_data` binary). **No change.** +- `src/lib/types/*` — all interfaces. **No change.** +- `src/lib/format.ts`, `src/lib/utils.ts` (`cn`). **No change.** +- `src/lib/navigation.ts` — `navItems`. Keep but extend icons/labels if needed. +- Hooks: `src/hooks/*` (use-dashboard, use-media, use-messages, use-voice, use-moderation, use-recordings, use-guilds, use-config, use-chatbot-user, use-action, use-mobile), `src/lib/hooks/use-mounted.ts`. **Reuse** (verify no bad imports into deleted barrels). +- Feature logic kept but **reskinned**: `src/components/media/music-player.tsx`, `src/components/voice/*`, `src/components/chatbot/*`, `src/components/messages/*`, `src/components/recordings/*`, `src/components/moderation/moderation-section.tsx`, `src/components/analysis/search-panel.tsx`, `src/components/dashboard/*` (charts → rewritten as custom SVG). + +## 2. What to DELETE +- `src/components/ui/*` (all 50 shadcn primitives). +- `components.json`, `@shadcn/react` + `@base-ui/react` + `shadcn` deps. +- `recharts` (replace with custom SVG chart helpers in `src/components/charts/`). +- `src/app/globals.css` → rewrite (no `@import "shadcn/tailwind.css"`, no `--color-primary` teal, no `.glass*`, no `.text-gradient`/`.gradient-border` teal/purple, no bluish body texture). +- `src/components/layout/app-sidebar.tsx` (shadcn Sidebar) → replace with custom `Spine` nav. +- All `page.tsx`+`view.tsx` pairs → merge into single `page.tsx` per route. + +## 3. What to BUILD (new) +- New `globals.css` (tokens above + utilities + keyframes `scan`, `eq`, `fade-up`, `spin-disc`). +- `src/components/primitives/` — minimal custom: `Button`, `Input`, `Select`, `Dialog`, `Tooltip`, `Badge`, `Progress`, `Avatar`, `Skeleton`, `Toast`, `Sheet`. +- `src/components/motion/` — `RouteTransition.tsx`, `Stagger.tsx`, `variants.ts`. +- `src/components/three/` — `SignalField.tsx`, `OrbField.tsx`, `WebGLGuard.tsx`, `StaticFallback.tsx`, `useThreeScene.ts`. +- `src/components/charts/` — `Sparkline`, `AreaActivity`, `RadialGauge`, `SessionRibbon`, `Waveform`. +- `src/components/layout/` — `Spine.tsx`, `StatusBar.tsx`, `ThemeToggle.tsx` (reskin). +- 7 merged `page.tsx` files (one per route) implementing the metaphors above. +- `src/app/layout.tsx` — root (fonts + ThemeProvider + Toaster), `src/app/(dashboard)/layout.tsx` — providers + Spine + StatusBar + RouteTransition + MiniPlayer + Chatbot, `src/app/page.tsx` → redirect `/dashboard`. + +--- + +## 4. Target File & Folder Structure (authoritative) + +Everything below `services/frontend/src/` is the new tree. `REWRITE` replaces existing; `NEW` creates; `DELETE` removes. The `app/` route tree collapses `page.tsx`+`view.tsx` into single `page.tsx` files containing BOTH server fetch (default export) and client view (`"use client"` named export in same file). + +``` +services/frontend/ +├─ package.json REWRITE (drop shadcn/base-ui/recharts; add motion, three, @types/three) +├─ pnpm-workspace.yaml REWRITE (onlyBuiltDependencies: keep build list minimal) +├─ components.json DELETE (shadcn registry config — no longer used) +├─ next.config.ts KEEP (output: standalone, trailingSlash, images.unoptimized) +├─ tsconfig.json KEEP (paths "@/*" → src/*, strict) +├─ postcss.config.mjs KEEP (@tailwindcss/postcss) +├─ biome.json KEEP +└─ src/ + ├─ app/ + │ ├─ layout.tsx REWRITE (3 fonts + ThemeProvider + custom Toaster; rm sonner) + │ ├─ globals.css REWRITE (new token system §0; rm glass/teal/purple) + │ ├─ page.tsx KEEP (redirect → /dashboard/) + │ └─ (dashboard)/ + │ ├─ layout.tsx REWRITE (providers + Spine + StatusBar + RouteTransition + MiniPlayer + Chatbot; rm shadcn Sidebar) + │ ├─ dashboard/ page.tsx REWRITE (server fetch + client; 3D SignalField hero) + │ ├─ messages/ page.tsx REWRITE (server seeds + ; spine + severity ticks) + │ ├─ voice/ page.tsx REWRITE (server seeds + ; 3D OrbField hero) + │ ├─ media/ page.tsx REWRITE (server seeds + ; turntable disc) + │ ├─ recordings/ page.tsx REWRITE (server seeds + ; waveform rows) + │ ├─ moderation/ page.tsx REWRITE (server seeds + ; event-flow) + │ └─ analysis/ page.tsx REWRITE (client ; query console) + ├─ components/ + │ ├─ ui/ DELETE (all 50 shadcn primitives) + │ ├─ primitives/ NEW (Button, Input, Select, Dialog, Tooltip, Badge, Progress, Avatar, Skeleton, Toast, Sheet, index.ts) + │ ├─ motion/ NEW (variants.ts, Stagger.tsx, RouteTransition.tsx) + │ ├─ three/ NEW (WebGLGuard, SignalField, OrbField, StaticFallback, useThreeScene) + │ ├─ charts/ NEW (Sparkline, AreaActivity, RadialGauge, SessionRibbon, Waveform) + │ ├─ layout/ REWRITE (Spine NEW, StatusBar NEW, ThemeToggle REWRITE, app-sidebar DELETE) + │ ├─ dashboard/ REWRITE (stat-card DELETE; activity-chart/hourly/top-channels/moderation-donut/users/channels/reactions REWRITE) + │ ├─ messages/ REWRITE (message-card DELETE; message-list/detail/detail-view/ai-status-badge/ai-analysis-panel/attachments-grid/lightbox/search-overlay REWRITE) + │ ├─ voice/ REWRITE (voice-connection-card/connection-card/microphone-card DELETE; speaker-waveform/active-speakers-panel/activity-timeline/mic-control/listen-control REWRITE) + │ ├─ media/ REWRITE (music-player, mini-player REWRITE) + │ ├─ recordings/ REWRITE (recording-card DELETE; recording-player REWRITE) + │ ├─ moderation/ REWRITE (moderation-section REWRITE) + │ ├─ analysis/ REWRITE (search-panel REWRITE) + │ ├─ chatbot/ REWRITE (chatbot-container, chat-panel REWRITE; chatbot-context, index KEEP) + │ └─ shared/ REWRITE (empty-state, error-state, loading-skeleton, error-boundary, guild-selector REWRITE; index KEEP) + ├─ hooks/ KEEP (verify no bad imports) + ├─ lib/ + │ ├─ api/ KEEP (server.ts, client.ts, index.ts) + │ ├─ ws/ KEEP (connection.ts, context.tsx, types.ts, ws-hook.ts) + │ ├─ types/ KEEP (all interfaces) + │ ├─ hooks/ KEEP (use-media-player.tsx, use-mounted.ts) + │ ├─ audio/ KEEP (voice PCM decode helpers if present) + │ ├─ format.ts KEEP + │ ├─ utils.ts KEEP (cn) + │ └─ navigation.ts KEEP + └─ (public assets) KEEP +``` + +### 4.1 Single-file page pattern (mandatory) +```tsx +// server component (default export) — runs on the server, fetches initial data +import { getX, getY } from "@/lib/api/server"; +import { XView } from "./page"; // self-import of the named client export + +export default async function Page() { + const [a, b] = await Promise.allSettled([getX(), getY()]); + return ; +} + +// client component (named export) — hydrated, takes initialData as SWR fallback +"use client"; +export function XView({ initialA, initialB }: Props) { + const { data } = useX(initialA); // SWR fallbackData = initialA +} +``` +Self-referencing the named export keeps the file single-artifact while satisfying Next's RSC boundary (default = server, named = client). Tabs live inside `XView`. + +### 4.2 Import rules (lint gate) +- No `@/components/ui/*` (deleted) — all UI via `@/components/primitives`. +- `three` only imported inside `src/components/three/*`; pages import those via `next/dynamic({ ssr: false })`. +- `motion` imported from `motion/react` only. +- All data: `@/lib/api/server` (server) / `@/lib/api/client` (client) — never invented endpoints. + +--- + +## TASKS (granular — every file is its own task) + +### PHASE 0 — Dependency surgery +- **T0.1** Edit `package.json`: remove `dependencies["@base-ui/react"]`. +- **T0.2** Remove `dependencies["@shadcn/react"]`. +- **T0.3** Remove `dependencies["shadcn"]`. +- **T0.4** Remove `dependencies["recharts"]`. +- **T0.5** Remove `dependencies["cmdk"]`. +- **T0.6** Remove `dependencies["sonner"]`. +- **T0.7** Remove `dependencies["react-day-picker"]`. +- **T0.8** Remove `dependencies["embla-carousel-react"]`. +- **T0.9** Remove `dependencies["react-resizable-panels"]`. +- **T0.10** Remove `dependencies["input-otp"]`. +- **T0.11** Add `dependencies["motion"]: "^12.0.0"`, `dependencies["three"]: "^0.180.0"`, `devDependencies["@types/three"]: "^0.180.0"`. +- **T0.12** `rm -f pnpm-lock.yaml && pnpm install` (regenerate lockfile). +- **T0.13** Verify `pnpm ls recharts @shadcn/react @base-ui/react` → empty; `pnpm ls motion three @types/three` → present. +- **T0.14** `cat pnpm-workspace.yaml`: confirm `onlyBuiltDependencies` keeps needed native builds, no broken shadcn postinstall. + +### PHASE 1 — Design tokens (globals.css) +- **T1.1** Rewrite `@theme { }` head: light `:root` palette from §0 (canvas/surface/ink/ink-soft/hairline/signal/signal-ink/amber/vermilion/ring). +- **T1.2** Add radius tokens `--r: 14px`, `--r-panel: 12px`, `--r-control: 8px`, `--r-pill: 9999px`. +- **T1.3** Add `--font-display` token; keep `--font-sans`/`--font-mono`. +- **T1.4** Add `.dark { }` override block with §0 dark values (warm charcoal). +- **T1.5** Replace `@layer base body` bg: warm dot-grid `radial-gradient(oklch(0.45 0.03 70 / 0.05) 1px, transparent 1px)` + faint warm glow; remove old bluish radial layers. +- **T1.6** Retint scrollbar thumb to warm `oklch(0.4 0.02 70 / 0.2)`; keep `::selection` signal-tinted. +- **T1.7** Delete `.glass`, `.glass-elevated`, `.glass-intense`, `.dark .glass-intense` utilities. +- **T1.8** Delete `.text-gradient` and `.gradient-border`. +- **T1.9** Add `.surface` utility (bg var(--surface), radius var(--r), padding). +- **T1.10** Add `.scan-tick` (1px animated pulse line, keyframe `scan`). +- **T1.11** Add `.ticker`, `.pill`, `.mono` utilities. +- **T1.12** Add keyframes `scan`, `eq`, `fade-up`, `spin-disc` (keep used existing ones if still referenced). +- **T1.13** Add `@media (prefers-reduced-motion: reduce)` kill switch for scan/eq/spin-disc/pulse-ring/shimmer. +- **T1.14** Remove `@import "shadcn/tailwind.css";` (line 3); verify nothing else depends on shadcn CSS vars. +- **T1.15** Verify `grep -c "0.52 0.17 215\|0.55 0.2 280" src/app/globals.css` → `0`. +- **T1.16** `pnpm biome check src/app/globals.css` → no errors. + +### PHASE 2 — Root layout + fonts +- **T2.1** In `layout.tsx` add `Bricolage_Grotesque` (`variable: "--font-display"`, subsets `["latin"]`, `display: "swap"`). +- **T2.2** Apply `inter.variable`, `jetbrainsMono.variable`, `bricolage.variable` to ``. +- **T2.3** Remove `import { Toaster } from "@/components/ui/sonner"`. +- **T2.4** Comment out `` temporarily (re-enabled after T3.10). +- **T2.5** Keep `suppressHydrationWarning`, `ThemeProvider` (defaultTheme dark, enableSystem false). +- **T2.6** `npx tsc --noEmit` (fonts only; rest may still error until primitives exist). + +### PHASE 3 — Custom primitives (replace 50 shadcn ui) +- **T3.1** `primitives/Button.tsx`: `motion.button`, variants `primary`/`ghost`/`danger`, `cn()` merge, `cursor-pointer`, `focus-visible:ring-2 ring-signal`, `whileTap` scale 0.97 gated by `useReducedMotion()`. +- **T3.2** `primitives/Input.tsx`: native ``, `bg-surface`, `rounded-[var(--r-control)]`, `mono` prop. +- **T3.3** `primitives/Select.tsx`: native `` styled, `bg-surface`. +- **T3.4** `primitives/Dialog.tsx`: native `` + `showModal()`, warm `::backdrop`, `AnimatePresence`, `onClose`. +- **T3.5** `primitives/Tooltip.tsx`: CSS group-hover popover. +- **T3.6** `primitives/Badge.tsx`: tonal pill, `variant` → `bg-{tone}/15 text-{tone}` (signal/amber/vermilion/neutral). +- **T3.7** `primitives/Progress.tsx`: SVG track + `motion` fill, `value`/`max`, signal color. +- **T3.8** `primitives/Avatar.tsx`: `` + initials fallback, signal bg, size prop. +- **T3.9** `primitives/Skeleton.tsx`: shimmer block (signal-tinted), `aria-hidden`. +- **T3.10** `primitives/Toast.tsx`: `ToastProvider` context + portal, `useToast()`, motion slide-in, auto-dismiss. +- **T3.11** `primitives/Sheet.tsx`: mobile drawer (`translate-x` spring), overlay, `open`/`onClose`. +- **T3.12** `primitives/index.ts` re-export all 11. +- **T3.13** Re-enable `` in `layout.tsx` (T2.4). +- **T3.14** Verify `npx tsc --noEmit` on primitives; `grep -rl "@/components/ui/" src/components/primitives` → empty. + +### PHASE 4 — Motion foundation +- **T4.1** `motion/variants.ts`: export `spring`, `ease`, `fadeUp`, `stagger` (per §0.1). +- **T4.2** `motion/Stagger.tsx`: `StaggerGroup` + `StaggerItem` (`"use client"`). +- **T4.3** `motion/RouteTransition.tsx`: `"use client"`, `usePathname`, `AnimatePresence mode="popLayout"`, reduced-motion fallback to plain ``. +- **T4.4** Verify `npx tsc --noEmit` on motion; `motion/react` import resolves. + +### PHASE 5 — Custom SVG charts (replace recharts) +- **T5.1** `charts/Sparkline.tsx`: `` polyline from `points:number[]`, signal stroke, no axes. +- **T5.2** `charts/AreaActivity.tsx`: filled `` area, low-opacity signal gradient, `pathLength` draw gated by reduced-motion. +- **T5.3** `charts/RadialGauge.tsx`: `` arc `stroke-dasharray`, center mono label. +- **T5.4** `charts/SessionRibbon.tsx`: horizontal segments per speaker duration. +- **T5.5** `charts/Waveform.tsx`: bars from deterministic seed, spring scaleY on hover. +- **T5.6** Verify `npx tsc --noEmit` on charts; no `recharts` import. + +### PHASE 6 — Three.js foundation (lazy, guarded) +- **T6.1** `three/WebGLGuard.tsx`: `"use client"`, detect webgl2/webgl, render `children` or `fallback`. +- **T6.2** `three/useThreeScene.ts`: shared hook — renderer init (`dpr:[1,1.75]`, `powerPreference`), RAF with `document.hidden` pause, `dispose()` on unmount, resize observer. +- **T6.3** `three/SignalField.tsx`: `Points` BufferGeometry (~min(900, w*h/2000)), additive blend, signal-lime, fog, idle rotation + sine drift, `activity` prop, pointer parallax. Uses `useThreeScene`. +- **T6.4** `three/OrbField.tsx`: per-speaker `Sphere`, y-scale + ring lerp to speaking, tones signal/idle/vermilion. +- **T6.5** `three/StaticFallback.tsx`: 2D SVG/CSS silhouette for both scenes. +- **T6.6** Verify `grep -rl "from \"three\"" src | grep -v "components/three"` → empty; `npx tsc --noEmit` on three. + +### PHASE 7 — Layout shell +- **T7.1** `layout/Spine.tsx`: `"use client"`, vertical rail from `navItems`, icon node + hairline, active signal dot + label reveal (motion spring), `max-md:` bottom tab-bar. +- **T7.2** `layout/StatusBar.tsx`: `"use client"`, page title + WS status dot (motion pulse) + `GuildSelector` + live clock (mono) + `ThemeToggle`. +- **T7.3** `layout/ThemeToggle.tsx`: restyle, keep `next-themes` logic, motion icon swap. +- **T7.4** Rewrite `(dashboard)/layout.tsx`: keep SWRConfig/WsProvider/MediaPlayerProvider/ChatbotProvider + sync functions verbatim; swap `AppSidebar`→`Spine`, header→`StatusBar`, wrap children in `RouteTransition`; remove `SidebarInset`/`SidebarTrigger`/`Separator`; keep MiniPlayer+ChatbotContainer. +- **T7.5** Delete `layout/app-sidebar.tsx`. +- **T7.6** Verify `grep -rl "components/ui/sidebar\|app-sidebar" src` → empty; `npx tsc --noEmit`. + +### PHASE 8 — Dashboard page + components +- **T8.1** Rewrite `dashboard/page.tsx`: default async `getDashboardStats`+`getActivity` → ``; named `"use client"` view with useStats/useActivity, 3D hero + tickers + tabs. +- **T8.2** Add `WebGLGuard`+`SignalField` hero with `activity` ratio; overlay headline (Bricolage) + ``. +- **T8.3** Build asymmetric ticker row with `StaggerGroup` + 4 `.surface` blocks (mono number + label + ``); inline (replaces stat-card). +- **T8.4** Delete `dashboard/stat-card.tsx`. +- **T8.5** Reskin `dashboard/activity-chart.tsx` → `charts/AreaActivity` (daily). +- **T8.6** Reskin `dashboard/hourly-activity-chart.tsx` → `charts/AreaActivity` (hourly). +- **T8.7** Reskin `dashboard/top-channels-chart.tsx` → `charts/` + `.surface`. +- **T8.8** Reskin `dashboard/moderation-donut.tsx` → `charts/RadialGauge`. +- **T8.9** Reskin `dashboard/users-section.tsx` → `.surface`. +- **T8.10** Reskin `dashboard/channels-section.tsx` → `.surface`. +- **T8.11** Reskin `dashboard/reactions-section.tsx` → `.surface`. +- **T8.12** Verify `grep -rl "components/ui/card" src/app/\(dashboard\)/dashboard src/components/dashboard` → empty; `npx tsc --noEmit`. + +### PHASE 9 — Messages page + components +- **T9.1** Rewrite `messages/page.tsx`: default `getMessages(guildId)`(+channels) → ``; client spine + entries. +- **T9.2** Delete `messages/message-card.tsx`. +- **T9.3** Rewrite `messages/message-list.tsx`: left timeline spine + right severity-tick entries (`surface` + `border-l-2` lime/amber/vermilion, motion spring tick), `layout` reflow. +- **T9.4** Rewrite `messages/message-detail.tsx` → `.surface`. +- **T9.5** Rewrite `messages/message-detail-view.tsx` → `.surface` pane. +- **T9.6** Rewrite `messages/ai-status-badge.tsx` → `primitives/Badge`. +- **T9.7** Rewrite `messages/ai-analysis-panel.tsx` → `.surface`. +- **T9.8** Rewrite `messages/attachments-grid.tsx` → `.surface` grid. +- **T9.9** Rewrite `messages/lightbox.tsx` → `primitives/Dialog`. +- **T9.10** Rewrite `messages/search-overlay.tsx` → console palette, type-in animation, `primitives/Dialog`. +- **T9.11** Verify no `components/ui/card` in messages tree; `npx tsc --noEmit`. + +### PHASE 10 — Voice page + components +- **T10.1** Rewrite `voice/page.tsx`: default `getVoiceStatus()` → ``; client `WebGLGuard`+`OrbField` hero + ribbon + tabs. +- **T10.2** Delete `voice/voice-connection-card.tsx`, `connection-card.tsx`, `microphone-card.tsx`. +- **T10.3** Rewrite `voice/speaker-waveform.tsx` → SVG ring / eq bars. +- **T10.4** Rewrite `voice/active-speakers-panel.tsx` → `.surface`. +- **T10.5** Rewrite `voice/activity-timeline.tsx` → `charts/SessionRibbon`. +- **T10.6** Rewrite `voice/mic-control.tsx` → `primitives/Button`. +- **T10.7** Rewrite `voice/listen-control.tsx` → `primitives/Button`. +- **T10.8** Verify `npx tsc --noEmit`; no `components/ui/card` in voice tree. + +### PHASE 11 — Media page + components +- **T11.1** Rewrite `media/page.tsx`: default `getMediaStatus()` → ``; client turntable disc + transport + queue. +- **T11.2** Rewrite `media/music-player.tsx`: CSS-3D disc (spin-disc, pause when not playing, spring on play/pause), mono meta, `primitives/Button` transport, `.surface` queue rows with `layout`. +- **T11.3** Rewrite `media/mini-player.tsx` → compact `.surface`. +- **T11.4** Verify `npx tsc --noEmit`; no `components/ui/card` in media tree. + +### PHASE 12 — Recordings page + components +- **T12.1** Rewrite `recordings/page.tsx`: default `getRecordings(50)` → ``; client rows `AnimatePresence`+`layout`, live `voice_recording_uploaded` prepend. +- **T12.2** Delete `recordings/recording-card.tsx`. +- **T12.3** Rewrite `recordings/recording-player.tsx` → `.surface` row + `charts/Waveform` + `primitives/Button`/`Dialog` play/delete. +- **T12.4** Verify `npx tsc --noEmit`. + +### PHASE 13 — Moderation + Analysis pages +- **T13.1** Rewrite `moderation/page.tsx`: default `getModerationStats/Actions` → ``; client vertical event-flow, live actions pulse-in. +- **T13.2** Rewrite `moderation/moderation-section.tsx` → event-flow, no Card/table. +- **T13.3** Rewrite `analysis/page.tsx`: client `` terminal console (`primitives/Input` mono + blinking caret, `.surface` results staggered). +- **T13.4** Rewrite `analysis/search-panel.tsx` → terminal style. +- **T13.5** Verify `npx tsc --noEmit`. + +### PHASE 14 — Chatbot + shared + final cleanup +- **T14.1** Rewrite `chatbot/chatbot-container.tsx` → `.surface`, keep drag/minimize. +- **T14.2** Rewrite `chatbot/chat-panel.tsx` → bubbles via `AnimatePresence`, `primitives/*`. +- **T14.3** Keep `chatbot/chatbot-context.tsx` + `index.ts`. +- **T14.4** Rewrite `shared/empty-state.tsx` → tonal. +- **T14.5** Rewrite `shared/error-state.tsx`. +- **T14.6** Rewrite `shared/loading-skeleton.tsx` → `primitives/Skeleton`. +- **T14.7** Rewrite `shared/error-boundary.tsx`. +- **T14.8** Rewrite `shared/guild-selector.tsx` → `primitives/Select`. +- **T14.9** `rm -rf src/components/ui && rm -f components.json`. +- **T14.10** `grep -rn "components/ui/\|@shadcn\|@base-ui\|recharts" src` → MUST be empty. +- **T14.11** `grep -rl "from \"recharts\"\|@base-ui\|@shadcn" src` → empty (double-check). +- **T14.12** Verify `npx tsc --noEmit` across whole `src`. + +### PHASE 15 — Build + lint gate +- **T15.1** `cd services/frontend && npx tsc --noEmit` → 0 errors. +- **T15.2** `pnpm biome check` → fix all issues (no `any` in new files). +- **T15.3** `pnpm build` (standalone) → success, emits `.next/standalone/server.js`. +- **T15.4** Inspect `.next/static/chunks/` for three-heavy chunk loaded only on dashboard/voice; confirm NOT in `/dashboard/` initial SSR HTML. +- **T15.5** Confirm `pnpm-lock.yaml` present (reproducible flake install). +- **T15.6** `grep -c "0.52 0.17 215\|0.55 0.2 280" .next/static/css/*.css` → 0. + +### PHASE 16 — Local runtime smoke test (no prod) +- **T16.1** Start local standalone: `GMW_BACKEND_URL=http://127.0.0.1:4001 PORT=4017 node .next/standalone/server.js &`. +- **T16.2** `curl -s -o /dev/null -w "%{http_code}"` for all 7 routes → 200. +- **T16.3** `curl /dashboard/ | grep -o "Bricolage\|signal\|surface"` → present. +- **T16.4** `curl /_next/static/css/*.css | grep "0.52 0.17 215\|0.55 0.2 280"` → empty. +- **T16.5** Headless browser `/dashboard/`+`/voice/` WebGL on: no console errors, `` present, `` NOT rendered. +- **T16.6** Same pages WebGL off: `` renders, no crash. +- **T16.7** Kill local server. Do NOT touch prod unit. +- **T16.8** Confirm 7 routes 200 + no console errors in T16.5/16.6. + +### PHASE 17 — Flake + staging deploy +- **T17.1** Inspect `flake.nix` frontend drv: `filterSource` ignores `out/.next/node_modules`; `pnpm-lock.yaml` included. +- **T17.2** `nix build .#gmw-frontend --impure --sandbox-off` → succeeds. +- **T17.3** `nix copy` frontend drv to VPS into staging profile (test port e.g. 4217). +- **T17.4** Create/adjust staging systemd unit with `PORT=4217` exported BEFORE `node server.js` (LIDM PORT bug). +- **T17.5** `sudo systemctl restart gmw-frontend-staging`; `curl` staging → 200. +- **T17.6** Browser-check staging `/dashboard/`+`/voice/` (3D visible, fallback test). +- **T17.7** Verify staging CSS has no old teal/purple; new design renders. + +### PHASE 18 — Production swap (CONFIRM WITH USER FIRST) +- **T18.1** STOP — send staging screenshots/URL; await explicit approval before touching prod. +- **T18.2** On approval: `nix-env --profile /nix/var/nix/profiles/gmw-frontend --set `. +- **T18.3** Confirm `gmw-frontend.service` exports `PORT=4017` before exec. +- **T18.4** `sudo systemctl restart gmw-frontend`. +- **T18.5** `curl` all 7 routes on `https://imphnen.asepharyana.my.id` → 200. +- **T18.6** Browser verify prod: new design, old teal gone, 3D scenes render. +- **T18.7** `journalctl -u gmw-frontend -f` 5 min; confirm WS reconnect + live features. +- **T18.8** Notify user with before/after notes; keep rollback plan (`nix-env --set ; systemctl restart`). + +--- + +## Risks / Trade-offs +- **Scope:** 7 pages + charts + primitives + motion + 2 three scenes + layout. Big but mechanical; each task is isolated (~90 atomic tasks). +- **Bundle weight:** `three` adds ~150KB gz but ONLY on dashboard/voice routes (lazy chunk, `ssr:false`). `motion` ~35KB gz app-wide — acceptable. +- **WebGL compatibility:** covered by `WebGLGuard` + static fallback. Old devices / strict privacy browsers never blank. +- **Motion excess:** risk of "AI-generated" scattered animation. Guard: one signature moment per page, shared variants, reduced-motion gates. +- **Feature regressions:** Voice PCM playback, media transport, chatbot drag — logic preserved, only skin changes. Smoke test (T16) catches SSR breaks; live WS/3D needs real backend (staging T17). +- **Removed deps:** dropping `recharts`/`sonner`/`cmdk` means rewriting charts + toasts + search palette — accounted for in Phases 3/5/9. +- **Next standalone PORT bug:** `server.js` may not read `PORT` — ensure unit exports `PORT=4017` before exec (T17.4/T18.3). +- **three + React 19:** raw three avoids R3F compat surface; lifecycle (dispose + RAF) handled in T6.2. +- **next-themes:** keep (light/dark toggle); default dark. + +## Open questions (answer before T18) +- Q1: Deploy to prod now or staging-only first? (Recommend staging + screenshot review.) +- Q2: Keep `react-day-picker`/`embla` if any page still needs them? (Plan assumes no — verify in T14.10 grep.) +- Q3: Any brand name/wordmark change from "Discord Automod"? (Keep "Bete" identity unless told.) +- Q4: 3D depth — full 3D scenes on dashboard+voice as specced, or also a 3D accent on media (disc)? (Default: dashboard+voice only; media disc stays CSS 3D.) diff --git a/services/frontend/nav-debug.cjs b/services/frontend/nav-debug.cjs new file mode 100644 index 0000000..4e154e1 --- /dev/null +++ b/services/frontend/nav-debug.cjs @@ -0,0 +1,48 @@ +const puppeteer = require("puppeteer-core"); +(async () => { + const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] }); + const page = await browser.newPage(); + page.on("pageerror", (e) => console.log("PAGEERROR:", e.message)); + await page.goto("http://127.0.0.1:4017/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 }); + await new Promise((r) => setTimeout(r, 2500)); + + // Find every tag and log its href + event listeners + const anchors = await page.$$eval("a", els => els.map(a => ({ + href: a.getAttribute("href"), + hasClick: a.hasAttribute("onClick"), + outer: a.outerHTML.substring(0, 200) + }))); + console.log("ALL ANCHORS:", JSON.stringify(anchors)); + + // Inject error catches + await page.evaluate(() => { + window.__clicks = []; + window.__events = []; + document.addEventListener("click", (e) => window.__clicks.push({ target: e.target.tagName, dp: e.defaultPrevented, url: location.href }), true); + document.addEventListener("click", (e) => { + const t = e.target.closest("a"); + if (t) { + window.__events.push({ targetTag: t.tagName, href: t.getAttribute("href"), dp: e.defaultPrevented }); + } + }, false); + }); + + // Click the Voice nav anchor + await page.evaluate(() => { + const v = Array.from(document.querySelectorAll('a[aria-label]')).find(a=>a.getAttribute('aria-label')==='Voice'); + if (v) v.click(); + }); + await new Promise((r) => setTimeout(r, 3000)); + + console.log("URL after click:", page.url()); + console.log("history.length:", await page.evaluate(() => history.length)); + console.log("CLICKS (capture phase):", JSON.stringify(await page.evaluate(() => window.__clicks))); + console.log("CLICKS (bubble phase):", JSON.stringify(await page.evaluate(() => window.__clicks))); + + // Force navigate and confirm destination + await page.evaluate(() => { window.location.href = "/voice/"; }); + await new Promise((r) => setTimeout(r, 2000)); + console.log("FORCED to /voice/ ->", page.url()); + + await browser.close(); +})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); }); \ No newline at end of file diff --git a/services/frontend/nav-test.cjs b/services/frontend/nav-test.cjs new file mode 100644 index 0000000..f8a2293 --- /dev/null +++ b/services/frontend/nav-test.cjs @@ -0,0 +1,27 @@ +const puppeteer = require("puppeteer-core"); + +(async () => { + const browser = await puppeteer.launch({ + executablePath: "/usr/bin/google-chrome", + headless: true, + args: ["--no-sandbox", "--disable-setuid-sandbox"], + }); + const page = await browser.newPage(); + const errs = []; + page.on("pageerror", (e) => errs.push("PAGEERROR: " + e.message)); + await page.goto("http://localhost:3000/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 }); + await new Promise((r) => setTimeout(r, 2500)); + + const info = await page.evaluate(() => { + const v = Array.from(document.querySelectorAll('a[aria-label]')).find((a) => a.getAttribute("aria-label") === "Voice"); + const r = v.getBoundingClientRect(); + return { cx: r.x + r.width / 2, cy: r.y + r.height / 2 }; + }); + await page.mouse.click(info.cx, info.cy); + for (const t of [500, 1000, 2000, 3500]) { + await new Promise((r) => setTimeout(r, t === 500 ? 500 : t - (t === 1000 ? 500 : t === 2000 ? 1000 : 2000))); + console.log(`url @${t}ms:`, page.url()); + } + console.log("ERRORS:", errs.slice(0, 10).join(" | ") || "none"); + await browser.close(); +})().catch((e) => { console.error("SCRIPT FAIL:", e); process.exit(1); }); diff --git a/services/frontend/nav-test2.cjs b/services/frontend/nav-test2.cjs new file mode 100644 index 0000000..026e784 --- /dev/null +++ b/services/frontend/nav-test2.cjs @@ -0,0 +1,18 @@ +const puppeteer = require("puppeteer-core"); +(async () => { + const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] }); + const page = await browser.newPage(); + const errs = []; + page.on("pageerror", (e) => errs.push("PAGEERR: " + e.message)); + page.on("console", (m) => { if (m.type() === "error" || m.type()==="warning") errs.push("CONS["+m.type()+"]: " + m.text().slice(0,100)); }); + await page.goto("http://127.0.0.1:4019/voice/", { waitUntil: "domcontentloaded", timeout: 30000 }); + await new Promise((r) => setTimeout(r, 4000)); + const snap = await page.evaluate(() => ({ + url: location.href, + hasVoice: !!Array.from(document.querySelectorAll("*")).find(e => e.textContent && e.textContent.includes("Live speakers")), + hasPicker: !!Array.from(document.querySelectorAll("select"))[0] || false, + })); + console.log("DIRECT /voice/ load:", JSON.stringify(snap)); + console.log("ERRORS:", errs.slice(0,15).join("\n") || "none"); + await browser.close(); +})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); }); \ No newline at end of file diff --git a/services/frontend/nav-test4019.cjs b/services/frontend/nav-test4019.cjs new file mode 100644 index 0000000..a887742 --- /dev/null +++ b/services/frontend/nav-test4019.cjs @@ -0,0 +1,26 @@ +const puppeteer = require("puppeteer-core"); +(async () => { + const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: true, args: ["--no-sandbox","--disable-setuid-sandbox"] }); + const page = await browser.newPage(); + const net = []; + page.on("request", (r) => { const u = r.url(); if (u.includes("_rsc") || u.includes("/voice")) net.push("REQ " + r.method() + " " + u); }); + page.on("response", (r) => { const u = r.url(); if (u.includes("_rsc") || u.includes("/voice")) net.push("RES " + r.status() + " " + u); }); + page.on("pageerror", (e) => console.log("PAGEERR:", e.message)); + await page.goto("http://127.0.0.1:4019/dashboard/", { waitUntil: "domcontentloaded", timeout: 30000 }); + await new Promise((r) => setTimeout(r, 2500)); + + const info = await page.evaluate(() => { + const b = document.querySelector('button[aria-label="Voice"]'); + const a = document.querySelector('a[aria-label="Voice"]'); + return { buttonPresent: !!b, aPresent: !!a }; + }); + console.log("NAV button present:", info.buttonPresent, "| a present:", info.aPresent); + + await page.evaluate(() => { + const b = document.querySelector('button[aria-label="Voice"]'); + if (b) b.click(); + }); + await new Promise((r) => setTimeout(r, 3000)); + console.log("URL after click:", page.url(), "| history:", await page.evaluate(() => history.length)); + await browser.close(); +})().catch((e) => { console.error("FAIL:", e.message); process.exit(1); }); \ No newline at end of file diff --git a/services/frontend/package.json b/services/frontend/package.json index c4ff444..59a86eb 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -29,6 +29,7 @@ "@types/react-dom": "^19", "@types/three": "^0.180.0", "babel-plugin-react-compiler": "1.0.0", + "puppeteer-core": "^25.7.0", "tailwindcss": "^4", "typescript": "^5" } diff --git a/services/frontend/pnpm-lock.yaml b/services/frontend/pnpm-lock.yaml index 9f5b918..40f0bd3 100644 --- a/services/frontend/pnpm-lock.yaml +++ b/services/frontend/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + puppeteer-core: + specifier: ^25.7.0 + version: 25.7.0 tailwindcss: specifier: ^4 version: 4.3.3 @@ -427,6 +430,19 @@ packages: cpu: [x64] os: [win32] + '@puppeteer/browsers@3.2.0': + resolution: {integrity: sha512-LlBrE8oqGfU7b1Nk2d5Q1SbuPhZxTj0cJEMDPEws28OjNMELlflekmPPuf4FnK03x0ZRjKaYwJElUcKK4kyqJA==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + proxy-agent: '>=8.0.1' + yauzl: ^2.10.0 || ^3.4.0 + peerDependenciesMeta: + proxy-agent: + optional: true + yauzl: + optional: true + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -548,6 +564,14 @@ packages: '@webgpu/types@0.1.71': resolution: {integrity: sha512-mMy8/ODcKhab808co15eW+yN+HgXoQxRQHTiBV9Mrvl1r0ufnid7YOcI+gi4eUWSWl9ezD6TW2KXccrL8HCh2A==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + babel-plugin-react-compiler@1.0.0: resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} @@ -564,9 +588,19 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + chromium-bidi@17.0.2: + resolution: {integrity: sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==} + engines: {node: '>=20.19.0 <22.0.0 || >=22.12.0'} + peerDependencies: + devtools-protocol: '*' + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -594,9 +628,15 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devtools-protocol@0.0.1666840: + resolution: {integrity: sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==} + electron-to-chromium@1.5.398: resolution: {integrity: sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + enhanced-resolve@5.24.4: resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} @@ -626,6 +666,14 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -734,6 +782,13 @@ packages: meshoptimizer@0.22.0: resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + modern-tar@0.8.4: + resolution: {integrity: sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==} + engines: {node: '>=18.0.0'} + motion-dom@12.43.0: resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} @@ -804,6 +859,10 @@ packages: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} + puppeteer-core@25.7.0: + resolution: {integrity: sha512-wgBBj7dU5ceGyoT2PCrJpkYOhxPY8mDOmcSKZP92Cj5GgqQ3kv/UxzawnOLxiYuupe4bDf/yiQm3bzs/1nI0rQ==} + engines: {node: '>=22.12.0'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -833,6 +892,18 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -867,6 +938,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typed-query-selector@2.12.2: + resolution: {integrity: sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -886,9 +960,43 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + webdriver-bidi-protocol@0.4.2: + resolution: {integrity: sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -1190,6 +1298,11 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.12': optional: true + '@puppeteer/browsers@3.2.0': + dependencies: + modern-tar: 0.8.4 + yargs: 18.1.0 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -1293,6 +1406,10 @@ snapshots: '@webgpu/types@0.1.71': {} + ansi-regex@6.3.0: {} + + ansi-styles@6.2.3: {} + babel-plugin-react-compiler@1.0.0: dependencies: '@babel/types': 7.29.7 @@ -1310,8 +1427,20 @@ snapshots: caniuse-lite@1.0.30001806: {} + chromium-bidi@17.0.2(devtools-protocol@0.0.1666840): + dependencies: + devtools-protocol: 0.0.1666840 + mitt: 3.0.1 + zod: 3.25.76 + client-only@0.0.1: {} + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + clsx@2.1.1: {} convert-source-map@2.0.0: @@ -1328,16 +1457,19 @@ snapshots: detect-libc@2.1.2: {} + devtools-protocol@0.0.1666840: {} + electron-to-chromium@1.5.398: optional: true + emoji-regex@10.6.0: {} + enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 - escalade@3.2.0: - optional: true + escalade@3.2.0: {} fflate@0.8.3: {} @@ -1353,6 +1485,10 @@ snapshots: gensync@1.0.0-beta.2: optional: true + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + graceful-fs@4.2.11: {} jiti@2.7.0: {} @@ -1430,6 +1566,10 @@ snapshots: meshoptimizer@0.22.0: {} + mitt@3.0.1: {} + + modern-tar@0.8.4: {} + motion-dom@12.43.0: dependencies: motion-utils: 12.39.0 @@ -1496,6 +1636,20 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + puppeteer-core@25.7.0: + dependencies: + '@puppeteer/browsers': 3.2.0 + chromium-bidi: 17.0.2(devtools-protocol@0.0.1666840) + devtools-protocol: 0.0.1666840 + typed-query-selector: 2.12.2 + webdriver-bidi-protocol: 0.4.2 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - proxy-agent + - utf-8-validate + - yauzl + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -1545,6 +1699,21 @@ snapshots: source-map-js@1.2.1: {} + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.4): dependencies: client-only: 0.0.1 @@ -1568,6 +1737,8 @@ snapshots: tslib@2.8.1: {} + typed-query-selector@2.12.2: {} + typescript@5.9.3: {} undici-types@6.21.0: {} @@ -1583,5 +1754,30 @@ snapshots: dependencies: react: 19.2.4 + webdriver-bidi-protocol@0.4.2: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + ws@8.21.3: {} + + y18n@5.0.8: {} + yallist@3.1.1: optional: true + + yargs-parser@22.0.0: {} + + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + zod@3.25.76: {} diff --git a/services/frontend/src/components/ambient/ambient-canvas.tsx b/services/frontend/src/components/ambient/ambient-canvas.tsx index 36be34d..c71bc23 100644 --- a/services/frontend/src/components/ambient/ambient-canvas.tsx +++ b/services/frontend/src/components/ambient/ambient-canvas.tsx @@ -221,7 +221,7 @@ export function AmbientCanvas({ >; +}) { + const router = useRouter(); + return ( + router.push(href)} + className={cn( + "group relative flex size-11 items-center justify-center rounded-[13px] transition-all", + active + ? "bg-signal/15 text-signal" + : "text-ink-faint hover:bg-white/5 hover:text-ink-soft", + )} + > + {active && ( + + )} + + + ); +} export function NavRail() { const pathname = usePathname(); + const path = pathname ?? "/"; + return ( - - G - + label="Dashboard" + active={path === "/dashboard" || path === "/dashboard/"} + Icon={LayoutDashboard} + /> - {navItems.map((item) => { - const active = isActivePath(pathname, item.matchPrefix); - const Icon = item.icon; - return ( - - - {active && ( - - )} - - - - ); - })} + {navItems.map((item) => ( + + ))} ); } +