Files
GMW/services/frontend/DESIGN_TOKENS.md
T
asepharyana 81a004d250 feat(frontend): IMPHNEN design deep integration — full 5-layer redesign
Layer 0 — Foundation:
- Dark mode palette (30+ CSS var pairs in [data-theme='dark'])
- CSS-first theme engine in styles.css (@theme + CSS vars)
- UseTheme hook with light/dark/system support + localStorage persistence
- Fixed animation keyframes (shimmer 1.5s canonical, glowPulse moved to CSS)
- Smooth theme switch transitions via .theme-transitioning class

Layer 1 — Shared UI Components:
- Badge: success/warning variants now use CSS vars (bg-success-soft text-success)
- Toast: rewritten with CSS vars, z-index fixed (40), repositioned top-right
- Skeleton: added variant prop (rounded/circular/rectangular)
- EmptyState: new component with icon + title + description + action
- Button: added tertiary variant + icon-sm size
- Card: added elevated/bordered variants
- Input: added soft variant

Layer 2 — Layout & Navigation:
- TabStrip: new horizontal tab navigation with spring underline indicator (z-30)
- Sidebar: expanded by default (w-64), brand assets always visible
- Header: simplified brand bar, removed redundant page titles, added ThemeToggle
- MobileTabBar: enhanced with spring dot indicator + glass bg + safe area
- DashboardLayout: integrated TabStrip between Header and content
- ParticleBackground: lazy render, skips on mobile/reduced-motion

Layer 3 — Feature Components:
- MessageCard: 30+ hardcoded hex replacements → semantic CSS vars
- MessagesPanel: stat badges use Badge component variants
- DashboardStats: StatCard with variant system (primary/success/warning/destructive)
- UserSummaryList/UserProfileDetail/ChannelProfileDetail: all hardcoded colors → CSS vars
- AudioVisualizer: reads --primary CSS var at paint time
- ActiveSpeakers/RecordingsSubPanel: hardcoded colors → CSS vars

Layer 4 — Polish:
- EmptyState integrated across messages/dashboard/live panels
- Theme toggle wired in Header + App root
- Hover state audit for consistency
- Entry animations verified (cardStagger/cardItem pattern in all panels)

Resolves DESIGN_TOKENS.md §13.x issues: hardcoded colors, shimmer mismatch,
glow-pulse fragmentation, z-index collisions, toast positioning.
2026-07-02 05:58:36 +07:00

754 lines
33 KiB
Markdown

# 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 <md)
└── main (flex-1, min-w-0)
├── Header (sticky top-0 z-10, px-4 md:px-8)
│ └── flex items-center justify-between
│ ├── h1 + subtitle (left)
│ └── WS/Voice badges (right)
│ (under md) MobileTabBar (fixed bottom-0 z-50)
└── main content (flex-1 overflow-auto, p-4 md:p-6 lg:p-8)
```
### Common Padding Values
| Context | Class Pattern | Notes |
|------------------|-----------------------------|------------------------------|
| Card container | `p-6` | CardContent / CardHeader |
| Card content | `p-6 pt-0` (or `p-4`) | `pt-0` when following header |
| Card footer | `p-6 pt-0` + `flex` | |
| Page content | `p-4 md:p-6 lg:p-8` | Responsive scaling |
| Header | `px-4 py-4 md:px-8` | |
| Sidebar | `px-2` (nav items) | |
| Sub-panels | `p-4` | Music, Screen, etc. |
| Mascot chat | `p-4` (messages), `p-3` (input) | |
### Common Gap Values
| Pattern | Usage |
|----------------|--------------------------------------------|
| `gap-3` | Sidebar nav items, message card avatar+text, recording items, feed groups |
| `gap-4` | Stat card grid (>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
<motion.div variants={cardStagger} initial="initial" animate="animate">
<motion.div variants={cardItem}>...</motion.div>
<motion.div variants={cardItem}>...</motion.div>
</motion.div>
```
---
## 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
<div className="flex flex-col items-center gap-4 py-12">
<MascotImage size="md" className="opacity-60" />
<p className="text-sm text-muted-foreground">No data to display</p>
</div>
```
Wrapped in `EmptyStateMascot` component. Also used via standalone icon pattern:
```tsx
<div className="flex flex-col items-center gap-4 py-20 text-muted-foreground">
<BarChart3 className="h-10 w-10" />
<p className="text-sm">No data available yet.</p>
</div>
```
### Skeleton Loading Pattern
```tsx
<Skeleton className="rounded-lg bg-muted animate-shimmer" />
```
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 — RESOLVED in redesign (all components migrated to CSS vars)
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 — RESOLVED in T02 (moved to styles.css)
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 — RESOLVED in T02 (CSS canonical at 1.5s)
`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 — RESOLVED (z-index registry established in layout)
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 — RESOLVED in T06 (toast now z-40, positioned top-right)
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.
---
## 14. Dark Mode
Dark mode is driven by `[data-theme="dark"]` CSS custom property overrides in `styles.css`.
The theme is managed by `useTheme()` hook (`shared/hooks/useTheme.ts`).
See the full palette in `docs/superpowers/specs/2026-07-02-bete-frontend-redesign-design.md`.
---
## 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` |