From 5b04a4637c113f867eb3aac3774f319ef036ccfb Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 13 Jun 2026 11:54:20 +0700 Subject: [PATCH] docs(frontend): add design tokens documentation --- services/frontend/DESIGN_TOKENS.md | 745 +++++++++++++++++++++++++++++ 1 file changed, 745 insertions(+) create mode 100644 services/frontend/DESIGN_TOKENS.md diff --git a/services/frontend/DESIGN_TOKENS.md b/services/frontend/DESIGN_TOKENS.md new file mode 100644 index 0000000..eabb86c --- /dev/null +++ b/services/frontend/DESIGN_TOKENS.md @@ -0,0 +1,745 @@ +# Design Tokens — Bete Frontend + +## Table of Contents + +1. [CSS Custom Properties (OKLCH)](#1-css-custom-properties-oklch) +2. [Z-Index Registry](#2-z-index-registry) +3. [Spacing & Layout Patterns](#3-spacing--layout-patterns) +4. [Typography](#4-typography) +5. [Animation Keyframes](#5-animation-keyframes) +6. [Animation Utility Classes](#6-animation-utility-classes) +7. [Framer Motion Presets](#7-framer-motion-presets) +8. [GSAP Page Transitions](#8-gsap-page-transitions) +9. [Card Pattern](#9-card-pattern) +10. [Stagger Animation Pattern](#10-stagger-animation-pattern) +11. [Component Patterns & Variants](#11-component-patterns--variants) +12. [Reduced-Motion Handling](#12-reduced-motion-handling) +13. [Known Issues & Technical Debt](#13-known-issues--technical-debt) + +--- + +## 1. CSS Custom Properties (OKLCH) + +Defined in `styles.css` on `:root`. All use the OKLCH color space (`lightness chroma hue`). Consumed via `oklch(var(--name))` in CSS and `oklch(var(--name))` in the Tailwind config (`tailwind.config.js`). + +### Surface Colors + +| Variable | OKLCH Value | Description | +|------------------------|--------------------------|------------------------------------| +| `--background` | `1 0 0` | Page background (white) | +| `--foreground` | `0.141 0.005 285.823` | Body text (near-black) | +| `--card` | `1 0 0` | Card surface (white) | +| `--card-foreground` | `0.141 0.005 285.823` | Card text (near-black) | +| `--border` | `0.92 0.004 286.32` | Default border | +| `--input` | `0.92 0.004 286.32` | Input border (same as `--border`) | + +### Interaction Colors + +| Variable | OKLCH Value | Description | +|------------------------|--------------------------|------------------------------------| +| `--primary` | `0.623 0.214 259.815` | Primary accent (blue) | +| `--primary-soft` | `0.92 0.04 259.815` | Soft primary background | +| `--primary-foreground` | `0.97 0.014 254.604` | Text on primary (near-white) | +| `--secondary` | `0.967 0.001 286.375` | Secondary surface (light gray) | +| `--secondary-foreground`| `0.21 0.006 285.885` | Text on secondary | +| `--muted` | `0.967 0.001 286.375` | Muted surface (same as secondary) | +| `--muted-foreground` | `0.552 0.016 285.938` | Muted text (medium gray) | +| `--accent` | `0.967 0.001 286.375` | Accent surface | +| `--accent-foreground` | `0.21 0.006 285.885` | Text on accent | +| `--destructive` | `0.577 0.245 27.325` | Destructive action (reddish) | +| `--destructive-foreground`| `0.97 0.014 254.604` | Text on destructive | + +### Semantic / Effect Colors + +| Variable | OKLCH Value | Description | +|--------------------------|-------------------------------|--------------------------------------| +| `--ring` | `0.623 0.214 259.815` | Focus ring (same as primary) | +| `--radius` | `1rem` | Border radius base | +| `--primary-glow` | `0.623 0.214 259.815 / 0.15` | Primary glow backdrop-filter orb | +| `--accent-glow` | `0.552 0.016 285.938 / 0.15` | Accent glow | +| `--card-shadow` | `0.92 0.004 286.32 / 0.3` | Card box-shadow color | + +### Tailwind Config Mapping + +All custom properties are wired into the Tailwind theme under `theme.extend.colors`: + +```js +colors: { + border: "oklch(var(--border))", + input: "oklch(var(--input))", + ring: "oklch(var(--ring))", + background: "oklch(var(--background))", + foreground: "oklch(var(--foreground))", + "primary-soft":"oklch(var(--primary-soft))", + "primary-glow":"oklch(var(--primary-glow))", + "accent-glow": "oklch(var(--accent-glow))", + primary: { DEFAULT: "oklch(var(--primary))", foreground: "oklch(var(--primary-foreground))" }, + secondary: { DEFAULT: "oklch(var(--secondary))", foreground: "oklch(var(--secondary-foreground))" }, + muted: { DEFAULT: "oklch(var(--muted))", foreground: "oklch(var(--muted-foreground))" }, + accent: { DEFAULT: "oklch(var(--accent))", foreground: "oklch(var(--accent-foreground))" }, + destructive: { DEFAULT: "oklch(var(--destructive))", foreground: "oklch(var(--destructive-foreground))" }, + card: { DEFAULT: "oklch(var(--card))", foreground: "oklch(var(--card-foreground))" }, +}, +borderRadius: { + lg: "var(--radius)", // 1rem + md: "calc(var(--radius) - 2px)", // 0.875rem + sm: "calc(var(--radius) - 4px)", // 0.75rem +}, +``` + +### Color vs. Variable Hardcoding + +**Prefer CSS variables**. The following are supposed to use CSS vars but are **hardcoded to Tailwind utility colors** — see [Known Issues §13.1](#131-hardcoded-utility-colors--no-css-var). + +--- + +## 2. Z-Index Registry + +All stacking contexts in the application, inventoried from `*.tsx` and `*.css` files. + +| Value | Owner | Context / Location | +|-----------|--------------------------|-------------------------------------------------| +| `-1` | `ParticleBackground.tsx` | Inline `style={{ zIndex: -1 }}` — decorative orbs sit behind all content | +| `10` | `Header.tsx` | Class `sticky top-0 z-10` — sticky page header | +| `50` | `Sidebar.tsx` line 110 | Class `relative z-50` — mascot chatbot toggle button | +| `50` | `MobileTabBar.tsx` | Class `fixed bottom-0 left-0 right-0 z-50` — mobile bottom nav | +| `50` | `toast.tsx` | Class `fixed bottom-4 right-4 z-50` — toast notification container | +| `[9999]` | `Sidebar.tsx` line 122 | Class `fixed bottom-[170px] left-[80px] z-[9999]` — MascotChatbot floating panel | + +### Stacking Order (bottom to top) + +1. **Layer -1**: Particle background orbs (`ParticleBackground`) +2. **Layer 0**: Main page content, sidebar, cards +3. **Layer 10**: Sticky header (`Header`) +4. **Layer 50**: Toast container, mobile tab bar, mascot toggle button +5. **Layer 9999**: Mascot chatbot floating panel + +**Note**: There is no established z-index scale. Values 20, 30, 40 are unused. The arbitrary `z-[9999]` for the chatbot is an outlier — future additions should use a defined scale (e.g., 10/20/30/40/50/100) rather than arbitrary large numbers. + +--- + +## 3. Spacing & Layout Patterns + +### Page Layout + +``` +DashboardLayout + ├── ParticleBackground (fixed inset-0) + ├── grid-pattern overlay (fixed inset-0, pointer-events-none) + │ + └── flex container (relative flex min-h-screen) + ├── Sidebar (shrink-0, w-16 collapsed / w-64 expanded, hidden on sm), user card 2-col, user detail layout | +| `gap-6` | Top-level panel sections (dashboard, live) | +| `gap-2` | Filter badges, button groups, message row indicators, toast container | +| `space-y-1` | Compact text+value pairs | +| `space-y-2` | Active speakers list, recording list, message metadata blocks | +| `space-y-3` | Message feed items, skeleton groups | +| `space-y-4` | Auth form, music panel | +| `space-y-6` | User profile detail sections | + +### Responsive Grid Breakpoints + +- `sm:grid-cols-2` — stat cards, user cards, recordings cards +- `lg:grid-cols-4` — stat summary cards +- `xl:grid-cols-3`, `2xl:grid-cols-4` — image grid +- `xl:grid-cols-[1fr_320px]` — live audio + sidebar layout +- `md:grid-cols-2` — voice connection guild/channel selects +- `md:grid-cols-3` — moderation queue stat cards + +--- + +## 4. Typography + +### Font Family + +```css +font-family: Poppins, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +``` + +Defined in `styles.css` and `tailwind.config.js` under `theme.extend.fontFamily.sans`. + +### Type Scale + +| Class | Used For | +|----------------|--------------------------------------------| +| `text-[10px]` | Badge text, filter labels, status chips | +| `text-[11px]` | Message timestamps, edited/deleted indicators, action hints | +| `text-[12px]` | AI analysis body text | +| `text-xs` | Captions, descriptions, metadata, secondary info, badge text | +| `text-sm` | Body text, card descriptions, message content, form labels | +| `text-base` | Card titles (sometimes), live audio heading | +| `text-lg` | Section headings, stat values | +| `text-xl` | Page title (`h1`) | +| `text-2xl` | Stat card values, moderation queue numbers | + +### Font Weights + +| Weight | Usage | +|-----------------|--------------------------------------------| +| `font-medium` | Sidebar items, timestamps, badge text, section labels, queue items | +| `font-semibold` | CardTitle, user names, tracking-tight headings | +| `font-bold` | Page titles, stat values, key numbers | + +### Tracking + +- `tracking-tight`: CardTitle, page `h1` +- `tracking-wider`: Image grid kind badges + +### Font Mono + +- `font-mono`: Channel IDs, user IDs + +--- + +## 5. Animation Keyframes + +### Defined in `styles.css` + +```css +@keyframes bar-pulse { + 0%, 100% { transform: scaleY(0.8); } + 50% { transform: scaleY(1.2); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +``` + +### Defined in `tailwind.config.js` only (`glowPulse`) + +```js +keyframes: { + glowPulse: { + "0%, 100%": { opacity: "0.4" }, + "50%": { opacity: "0.8" }, + }, +}, +``` + +**Note**: `glowPulse` is NOT defined in `styles.css`. It only exists in `tailwind.config.js` and is used by `ParticleBackground.tsx` via the class `animate-glow-pulse`. This works because Tailwind 4 uses the JS config for keyframes, but the CSS file is the canonical keyframe source. This is a **latent inconsistency** — see Known Issues §13.3. + +### Animation Timings Summary + +| Keyframe | Duration | Timing Function | Used In | +|----------------|----------|------------------------|--------------------------------| +| bar-pulse | 0.4s | ease-in-out | AudioVisualizer (via CSS class)| +| shimmer | 1.5s (CSS) / 2s (TW config) | ease-in-out | Skeleton loading components | +| fadeInUp | 0.5s | ease-out | Utility class `.animate-fade-in-up` | +| fadeIn | 0.3s | ease-out | Utility class `.animate-fade-in` | +| glowPulse | 3s | ease-in-out | ParticleBackground glow orbs | + +**Mismatch**: `shimmer` duration is `1.5s` in `styles.css` but `2s` in `tailwind.config.js`. The CSS class `.animate-shimmer` is used by `Skeleton.tsx`, so the CSS definition wins. + +--- + +## 6. Animation Utility Classes + +Defined in `styles.css` under `@layer utilities`: + +| Class | Animation | Purpose | +|----------------------|------------------------------|--------------------------------| +| `.animate-fade-in-up`| `fadeInUp 0.5s ease-out` | Entry animation for elements | +| `.animate-fade-in` | `fadeIn 0.3s ease-out` | Simple fade-in | +| `.animate-bar-pulse` | `bar-pulse 0.4s ease-in-out infinite` | Audio visualizer bars (transform-origin: bottom) | +| `.animate-shimmer` | `shimmer 1.5s ease-in-out infinite` | Skeleton loading placeholder (gradient sweep) | + +Tailwind config also registers `animate-glow-pulse` (`glowPulse 3s ease-in-out infinite`). + +**Usage**: `Skeleton` component uses `animate-shimmer`. Inline `animate-spin` (Tailwind built-in) is used for loading spinners on re-analyze buttons. + +--- + +## 7. Framer Motion Presets + +All defined in `shared/hooks/useFramerStagger.ts`. + +### `cardStagger` — Parent container variant +```ts +{ + initial: { opacity: 0 }, + animate: { + opacity: 1, + transition: { staggerChildren: 0.08, delayChildren: 0.1 }, + }, +} +``` + +### `cardItem` — Child item (fade + slide up) +```ts +{ + initial: { opacity: 0, y: 20 }, + animate: { + opacity: 1, y: 0, + transition: { duration: 0.4, ease: "easeOut" }, + }, +} +``` + +### `fadeSlideUp` — Single element (cubic-bezier) +```ts +{ + initial: { opacity: 0, y: 24 }, + animate: { + opacity: 1, y: 0, + transition: { duration: 0.5, ease: [0.25, 0.46, 0.45, 0.94] }, + }, + exit: { opacity: 0, y: -12, transition: { duration: 0.2 } }, +} +``` +Used by `Header.tsx` (key=activeTab) and `DashboardLayout.tsx` (main content area, key=activeTab). + +### `fadeIn` — Simple opacity +```ts +{ + initial: { opacity: 0 }, + animate: { opacity: 1, transition: { duration: 0.3 } }, + exit: { opacity: 0, transition: { duration: 0.2 } }, +} +``` + +### `scaleIn` — Badge/pill entrance +```ts +{ + initial: { opacity: 0, scale: 0.8 }, + animate: { opacity: 1, scale: 1, transition: { duration: 0.3, ease: "backOut" } }, +} +``` + +### `springUp` — Emphasized entrance +```ts +{ + initial: { opacity: 0, y: 30 }, + animate: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 200, damping: 20 } }, +} +``` + +### Usage Pattern + +```tsx + + ... + ... + +``` + +--- + +## 8. GSAP Page Transitions + +### `useGsapTransition` (`shared/hooks/useGsapTransition.ts`) + +A custom hook for tab-level page transitions, used by `AuthOverlay.tsx`. + +**Page enter animation**: +- Container: opacity 0→1, y 20→0, `duration: 0.4`, `ease: "power2.out"` +- Stagger children: querySelectorAll `[data-stagger]`, `duration: 0.3`, `stagger: 0.05` +- Reduced motion: all durations set to 0 + +**Page exit animation** (returns Promise): +- Container: opacity→0, y→20, `duration: 0.3`, `ease: "power2.in"` + +**Card hover helper** (`gsapCardHover`): +```ts +onMouseEnter: gsap.to(target, { y: -4, boxShadow: "0 8px 25px rgba(0,0,0,0.15)", duration: 0.2 }) +onMouseLeave: gsap.to(target, { y: 0, boxShadow: "0 2px 8px rgba(0,0,0,0.08)", duration: 0.2 }) +``` +(Exported but unused in current components — kept as utility.) + +--- + +## 9. Card Pattern + +### Base `Card` component (`shared/ui/card.tsx`) + +``` +rounded-xl border border-border bg-card text-card-foreground shadow-sm hover:shadow-md transition-shadow +``` + +### Composition + +| Sub-component | Classes | Notes | +|----------------|------------------------------|--------------------------------| +| `Card` | See above | `transition-shadow` for hover | +| `CardHeader` | `flex flex-col space-y-1.5 p-6` | | +| `CardTitle` | `font-semibold leading-none tracking-tight` | `h3` element | +| `CardDescription` | `text-sm text-muted-foreground` | `p` element | +| `CardContent` | `p-6 pt-0` | `pt-0` to collapse with header | +| `CardFooter` | `flex items-center p-6 pt-0` | | + +### Common Card Variants + +| Context | Additional Classes | +|------------------|-----------------------------------------------------| +| Stat card | `CardContent p-4` (compact) | +| Dashboard stat | `overflow-hidden` | +| Message card | `group hover:border-primary/30 hover:shadow-md` | +| Auth card | `w-full max-w-md border-primary/30 shadow-lg shadow-primary/10` | +| Error/empty card | `border-dashed border-destructive` (error state) | +| User card | `hover:ring-1 hover:ring-primary/30 cursor-pointer` | +| Media queue item | `rounded-lg border-l-2 border-l-primary border-border` | + +### Empty State Pattern + +```tsx +
+ +

No data to display

+
+``` +Wrapped in `EmptyStateMascot` component. Also used via standalone icon pattern: +```tsx +
+ +

No data available yet.

+
+``` + +### Skeleton Loading Pattern + +```tsx + +``` +Used in `.animate-shimmer` containers matching real card layout. See `MessageCardSkeleton`, `StatsSkeleton`, `UserListSkeleton`, `DetailSkeleton`, `ActiveSpeakersSkeleton`. + +--- + +## 10. Stagger Animation Pattern + +### Framer Motion (primary, used in panels) + +Applied as `cardStagger` / `cardItem` pair on every major panel: + +| Panel | File | +|----------------------|----------------------------------| +| LivePanel | `features/live/index.tsx` | +| MessagesPanel | `features/messages/index.tsx` | +| DashboardStatsContent| `features/dashboard/components/DashboardStats.tsx` | +| UserSummaryList | `features/dashboard/components/UserSummaryList.tsx` | +| UserProfileDetail | `features/dashboard/components/UserProfileDetail.tsx` | +| MessageFeed | `features/messages/components/MessageFeed.tsx` | + +**Timing**: stagger 80ms, delayChildren 100ms, item duration 400ms. + +### GSAP (used in auth flow) + +`useGsapTransition` with `[data-stagger]` attributes. Stagger 50ms, duration 300ms. + +--- + +## 11. Component Patterns & Variants + +### Button (`shared/ui/button.tsx`) + +**Variants**: `default`, `secondary`, `destructive`, `outline`, `ghost` +**Sizes**: `default` (h-10), `sm` (h-9), `lg` (h-11), `icon` (h-10 w-10) + +Base classes: +``` +inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium +transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring +active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 +``` + +| Variant | Background | +|--------------|---------------------------------------| +| default | `bg-primary text-primary-foreground shadow-sm hover:bg-primary/90` | +| secondary | `bg-secondary text-secondary-foreground hover:bg-secondary/80` | +| destructive | `bg-destructive text-destructive-foreground hover:bg-destructive/90` | +| outline | `border border-input bg-background hover:bg-accent hover:text-accent-foreground` | +| ghost | `hover:bg-accent hover:text-accent-foreground` | + +### Badge (`shared/ui/badge.tsx`) + +**Variants**: `default`, `secondary`, `destructive`, `outline`, `success`, `warning` + +Base classes: +``` +inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors +``` + +| Variant | Classes (uses CSS vars where possible) | +|------------|------------------------------------------------------| +| default | `bg-primary text-primary-foreground` | +| secondary | `bg-muted text-muted-foreground` | +| destructive| `bg-destructive/15 text-destructive` | +| outline | `border-border text-foreground` | +| success | `bg-emerald-100 text-emerald-700` **[HARDCODED]** | +| warning | `bg-amber-100 text-amber-700` **[HARDCODED]** | + +### Input (`shared/ui/input.tsx`) + +``` +flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground +ring-offset-background placeholder:text-muted-foreground +focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 +disabled:cursor-not-allowed disabled:opacity-50 +``` + +### Select (`shared/ui/select.tsx`) + +Same visual treatment as Input (`flex h-10 w-full rounded-lg border border-input bg-background ...`). + +### Skeleton (`shared/ui/skeleton.tsx`) + +``` +rounded-lg bg-muted animate-shimmer +``` + +### Tabs (`shared/ui/tabs.tsx`) + +Wraps Radix `TabsPrimitive`: + +| Component | Key Classes | +|---------------|-----------------------------------------------------------| +| `TabsList` | `inline-flex h-10 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground` | +| `TabsTrigger` | `inline-flex items-center justify-center whitespace-nowrap rounded-lg px-3 py-1.5 text-sm font-medium transition-all ... data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm` | +| `TabsContent` | `mt-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring` | + +### Toast (`shared/ui/toast.tsx`) + +Context-based system with `addToast` / `removeToast`. Auto-dismiss after 4s. + +Container: `fixed bottom-4 right-4 z-50 flex flex-col gap-2` + +Type styles: + +| Type | Border-left class | Icon color | +|---------|----------------------------------|--------------------| +| info | `border-l-primary` | `text-primary` | +| success | `border-l-emerald-500` | `text-emerald-500` | +| error | `border-l-destructive` | `text-destructive` | +| warning | `border-l-amber-500` | `text-amber-500` | + +**Note**: `emerald-500` and `amber-500` are hardcoded Tailwind utilities — see Known Issues. + +### Glass Card (utility, `styles.css`) + +```css +.glass-card { + @apply bg-white/70 backdrop-blur-sm border border-[oklch(0.92_0.004_286.32)] rounded-xl; +} +``` + +### Grid Pattern (utility, `styles.css`) + +```css +.grid-pattern { + background-image: + linear-gradient(oklch(0.92 0.004 286.32 / 0.3) 1px, transparent 1px), + linear-gradient(90deg, oklch(0.92 0.004 286.32 / 0.3) 1px, transparent 1px); + background-size: 40px 40px; +} +``` +Applied in `DashboardLayout.tsx` at `opacity-[0.03]`. + +### Gradient Text + +```css +.gradient-text { + @apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400; +} +``` +Used in `Header.tsx` for the "IMPHNEN" brand text. + +--- + +## 12. Reduced-Motion Handling + +Three layers of protection: + +### 1. CSS Level (`styles.css`) + +```css +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} +``` + +### 2. GSAP Hook (`useGsapTransition`) + +```ts +function prefersReducedMotion(): boolean { + if (typeof window === "undefined") return false; + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} +``` +Passes `instant` flag → all durations set to 0, stagger set to 0. + +### 3. Particle Background (`ParticleBackground.tsx`) + +```tsx +const [reducedMotion, setReducedMotion] = useState(false); +useEffect(() => { + const mq = window.matchMedia("(prefers-reduced-motion: reduce)"); + setReducedMotion(mq.matches); + // listener for changes... +}, []); +if (reducedMotion) return null; // Entire particle layer removed +``` + +### Coverage Gap + +Framer Motion `AnimatePresence` and `motion` components (used extensively in MessageFeed, LivePanel, DashboardStats, UserSummaryList, UserProfileDetail, MascotChatbot) do **not** check `prefers-reduced-motion`. They rely solely on the CSS override. While the CSS `!important` rules do suppress Framer Motion animations (since they run via inline styles that get overridden), this is a fragile approach — some Framer Motion `transition` properties (spring physics, stagger delays) may not be fully neutralized by the CSS blanket rule. + +--- + +## 13. Known Issues & Technical Debt + +### 13.1 Hardcoded Utility Colors — Not Using CSS Variables + +Several components bypass the OKLCH custom property system and use Tailwind's built-in `emerald-*`, `amber-*`, `red-*`, `blue-*`, `orange-*`, `yellow-*`, `violet-*`, `cyan-*`, `purple-*`, `sky-*`, `pink-*` utility classes directly. These will not respond to theme changes. + +**Locations**: + +| File(s) | Hardcoded Colors Used | +|-------------------------------------------|--------------------------------------------| +| `shared/ui/badge.tsx` | `bg-emerald-100 text-emerald-700` (success variant), `bg-amber-100 text-amber-700` (warning variant) | +| `shared/ui/toast.tsx` | `border-l-emerald-500`, `text-emerald-500`, `border-l-amber-500`, `text-amber-500` | +| `shared/ui/button.tsx` | None (uses CSS vars correctly) | +| `features/dashboard/components/DashboardStats.tsx` | `text-emerald-500`, `bg-emerald-100`, `text-blue-500`, `bg-blue-100`, `text-violet-500`, `bg-violet-100`, `text-emerald-600`, `bg-cyan-100`, `text-cyan-500`, `text-amber-500`, `bg-amber-100` | +| `features/dashboard/components/UserSummaryList.tsx` | `bg-emerald-100 text-emerald-700`, `bg-amber-100 text-amber-700`, `bg-red-100 text-red-700` | +| `features/dashboard/components/UserProfileDetail.tsx` | `bg-red-100 text-red-700`, `bg-emerald-100 text-emerald-700`, `bg-amber-100 text-amber-700`, `text-emerald-600` | +| `features/messages/components/MessageCard.tsx` | `bg-red-100 text-red-700 border-red-200`, `bg-orange-100 text-orange-700 border-orange-200`, `bg-yellow-100 text-yellow-700 border-yellow-200`, `bg-blue-100 text-blue-700 border-blue-200`, `bg-emerald-50/40`, `border-l-emerald-400`, `bg-pink-50/40`, `border-l-pink-400`, `text-pink-600`, `text-pink-600/70` | +| `features/messages/components/MessagesPanel.tsx` | `bg-emerald-100 text-emerald-700 border-emerald-200`, `bg-orange-100 text-orange-700 border-orange-200`, `bg-red-100 text-red-700 border-red-200`, `text-emerald-600` | +| `features/messages/components/ImageGrid.tsx` | `bg-purple-100 text-purple-700 border-purple-200` | +| `features/live/components/ActiveSpeakers.tsx` | `text-emerald-700` | +| `features/live/components/RecordingsSubPanel.tsx` | `border-sky-200 bg-white` | +| `features/live/components/NowPlaying.tsx` | (uses Badge `success`/`warning` variants, which are hardcoded) | +| `widgets/Header.tsx` | `bg-emerald-400`, `bg-red-400`, `bg-gray-400`, `text-emerald-400`, `text-red-400` | + +**Fix**: Replace with CSS variable-based tokens, e.g.: +- `emerald-100` → `oklch(var(--success-bg))` (would need new custom property) +- `text-emerald-700` → `oklch(var(--success-fg))` +- `border-emerald-200` → `oklch(var(--success-border))` + +### 13.2 `formatTimeAgo` Forces Re-render on Every Tick + +In `features/messages/components/MessageCard.tsx`: + +```ts +function formatTimeAgo(ts: number): string { + const seconds = Math.floor((Date.now() - ts) / 1000); + // ... +} +``` + +`Date.now()` is called on every render, not via a periodic timer. This means the "X s ago" / "X m ago" labels are only accurate at the moment of render and do not update reactively. No `useEffect` interval keeps them fresh. Once rendered, a "5s ago" label will stay stale until a parent re-render. This is acceptable for a message feed that re-renders on new data, but inaccurate for pinned/static views. + +**Fix**: Either (a) accept staleness (current behavior, simplest), (b) add a `useEffect` interval that forces re-render every ~30s, or (c) use `useSyncExternalStore` with a global ticker. + +### 13.3 `AudioVisualizer.tsx` — Hardcoded IMPHNEN Gradient + +```ts +const gradient = ctx.createLinearGradient(0, 0, 0, height); +gradient.addColorStop(0, "#23a1eb"); +gradient.addColorStop(1, "#3eb0f2"); +``` + +These hex values (IMPHNEN blue `#23a1eb` → `#3eb0f2`) are hardcoded and do not reference the OKLCH `--primary` CSS variable. A theme change would not affect the visualizer. + +**Fix**: Read CSS custom property at paint time: +```ts +const primaryColor = getComputedStyle(document.documentElement) + .getPropertyValue("--primary").trim(); +// Convert OKLCH to hex or use Canvas oklch() if available +``` + +### 13.4 `glow-pulse` Keyframe Fragmentation + +The `glowPulse` keyframes are defined in `tailwind.config.js` but NOT in `styles.css`. The class `animate-glow-pulse` is referenced by `ParticleBackground.tsx`. Under Tailwind 4's CSS-first configuration, keyframes should live in the stylesheet. The config-only definition works but is inconsistent with `bar-pulse`, `shimmer`, `fadeInUp`, and `fadeIn` which are in `styles.css`. + +**Fix**: Move `@keyframes glowPulse { ... }` into `styles.css`. + +### 13.5 `shimmer` Duration Mismatch + +`styles.css`: `animation: shimmer 1.5s ease-in-out infinite` +`tailwind.config.js`: `shimmer: "shimmer 2s linear infinite"` (also uses `linear` vs. `ease-in-out`) + +The CSS class `.animate-shimmer` (used by `Skeleton.tsx`) references the CSS-based keyframe, so `1.5s ease-in-out` is the actual runtime value. The Tailwind config value is dead unless used via `animate-shimmer` as a Tailwind class name — which maps to `shimmer 2s linear infinite`. + +**Fix**: Align both sources. Pick one canonical definition. + +### 13.6 Z-Index Scale Not Formalized + +No defined z-index scale or Sass/CSS variables. Values jump from 50 to 9999. The chatbot's `z-[9999]` is brittle — any overlay/modal added later risks overlap issues. + +**Fix**: Define z-index custom properties: `--z-header: 10`, `--z-overlay: 50`, `--z-modal: 100`, `--z-toast: 50`, etc. + +### 13.7 Toast Container z-index Collision + +Toast container (`z-50`) and MobileTabBar (`z-50`) at same z-index. On mobile, toasts could be partially hidden behind the tab bar since toasts use `bottom-4` and the tab bar is `bottom-0`. In practice the toast gap prevents overlap, but this is fragile. + +### 13.8 Retro `bg-white` Usage + +`RecordingsSubPanel.tsx` uses `bg-white` (line 86) instead of `bg-card`. This will not respect a dark theme if one is added. + +--- + +## Appendix: File Map + +| Token / Concern | Canonical Source | +|---------------------------|----------------------------------------------| +| CSS custom properties | `styles.css` | +| Tailwind theme extension | `tailwind.config.js` | +| Framer Motion variants | `shared/hooks/useFramerStagger.ts` | +| GSAP transition hook | `shared/hooks/useGsapTransition.ts` | +| `cn()` utility | `shared/lib/utils.ts` | +| Card component | `shared/ui/card.tsx` | +| Button component | `shared/ui/button.tsx` | +| Badge component | `shared/ui/badge.tsx` | +| Input component | `shared/ui/input.tsx` | +| Select component | `shared/ui/select.tsx` | +| Skeleton component | `shared/ui/skeleton.tsx` | +| Tabs component | `shared/ui/tabs.tsx` | +| Toast component | `shared/ui/toast.tsx` | +| ScrollArea component | `shared/ui/scroll-area.tsx` | +| ParticleBackground | `widgets/particles/ParticleBackground.tsx` | +| DashboardLayout | `widgets/DashboardLayout.tsx` |