# 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 `