Files
GMW/design/system/15-theme-architecture.md
T
asepharyana d59b59a7a7 feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config
Frontend:
- migrate from Vite to Astro (astro.config.mjs, pages/, layouts/)
- add admin panel, settings page, command palette, error boundary
- refactor App.tsx, MascotChatbot, Sidebar, Header, DashboardLayout
- update API client, WebSocket, auth, dashboard features

Backend:
- add admin module and config routes
- refactor middlewares, Redis connection, WebSocket server/bridge
- add runtime config loader

Discord Gateway:
- refactor AI moderation: circuit breaker, concurrency limiter, fallback processor
- add media analysis client, Seaxng search, user profile learner
- add new drizzle migration

Shared:
- extend database schema, add new config fields
2026-07-02 00:02:41 +07:00

8.4 KiB

Theme Architecture — The Chameleon Engine

"The only constant in design is change — a theme system embraces it." — Unknown


🎯 Filosofi Theme

Sistem theme BETE dibangun di atas CSS Custom Properties:

  1. Separation of value from token — Nilai warna tidak pernah dirujuk langsung
  2. Single source of truth — Satu set CSS variables, dua tema (dark/light)
  3. Runtime switching — Tema bisa diganti tanpa reload
  4. Component-agnostic — Komponen tidak tahu tema apa yang aktif

🧬 Theme Architecture

CSS Custom Properties (oklch values)
        │
        ▼
┌─────────────────────────────────────┐
│         :root / [data-theme]        │  ← Tema didefinisikan di level root
│  --clr-surface-base: oklch(...)     │
│  --clr-primary: oklch(...)          │
│  --clr-text: oklch(...)             │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│     Tailwind Config Mapping         │  ← Map CSS vars ke Tailwind utilities
│  colors: {                          │
│    background: "oklch(var(--...))"  │
│  }                                  │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│        Component Styles             │  ← Komponen pakai Tailwind/CSS vars
│  <div className="bg-card" />        │
│  .card { background: var(--clr..) } │
└─────────────────────────────────────┘

🌗 Theme Definitions

Dark Theme (Default)

[data-theme="dark"] {
  /* Surfaces */
  --clr-surface-base:     oklch(0.11 0.010 286);
  --clr-surface-elevated: oklch(0.14 0.015 286);
  --clr-surface-overlay:  oklch(0.17 0.020 286);
  --clr-surface-sunken:   oklch(0.08 0.005 286);
  --clr-border:           oklch(0.22 0.020 286);

  /* Text */
  --clr-text:             oklch(0.95 0.005 286);
  --clr-text-secondary:   oklch(0.70 0.015 286);
  --clr-text-tertiary:    oklch(0.50 0.020 286);
  --clr-text-inverse:     oklch(0.11 0.010 286);

  /* Brand - brighter in dark */
  --clr-primary:          oklch(0.62 0.150 255);
  --clr-primary-bg:       oklch(0.25 0.060 255 / 0.20);
  --clr-primary-400:      oklch(0.62 0.150 255);
  --clr-primary-500:      oklch(0.55 0.175 255);
  --clr-primary-600:      oklch(0.47 0.160 255);

  /* Interactive */
  --clr-interactive-hover:   oklch(0.20 0.025 286);
  --clr-interactive-active:  oklch(0.24 0.030 286);
  --clr-interactive-selected: oklch(0.25 0.060 255 / 0.15);

  /* Shadows */
  --sh-card:    0 2px 8px rgba(0, 0, 0, 0.3);
  --sh-hover:   0 4px 16px rgba(0, 0, 0, 0.4);
  --sh-elevated: 0 8px 32px rgba(0, 0, 0, 0.5);
  --sh-modal:   0 16px 48px rgba(0, 0, 0, 0.6);

  /* Glass */
  --glass-bg:     oklch(0.15 0.015 286 / 0.60);
  --glass-border: oklch(0.25 0.030 286 / 0.20);
}

Light Theme

[data-theme="light"] {
  /* Surfaces */
  --clr-surface-base:     oklch(0.97 0.002 286);
  --clr-surface-elevated: oklch(1.00 0.000 286);
  --clr-surface-overlay:  oklch(0.95 0.003 286);
  --clr-surface-sunken:   oklch(0.92 0.004 286);
  --clr-border:           oklch(0.87 0.005 286);

  /* Text */
  --clr-text:             oklch(0.11 0.010 286);
  --clr-text-secondary:   oklch(0.50 0.020 286);
  --clr-text-tertiary:    oklch(0.70 0.025 286);
  --clr-text-inverse:     oklch(0.97 0.005 286);

  /* Brand - standard in light */
  --clr-primary:          oklch(0.55 0.175 255);
  --clr-primary-bg:       oklch(0.90 0.060 255 / 0.25);
  --clr-primary-400:      oklch(0.55 0.175 255);
  --clr-primary-500:      oklch(0.47 0.160 255);
  --clr-primary-600:      oklch(0.40 0.140 255);

  /* Interactive */
  --clr-interactive-hover:   oklch(0.90 0.005 286);
  --clr-interactive-active:  oklch(0.85 0.008 286);
  --clr-interactive-selected: oklch(0.90 0.060 255 / 0.3);

  /* Shadows — lighter in light theme */
  --sh-card:    0 2px 8px rgba(0, 0, 0, 0.08);
  --sh-hover:   0 4px 16px rgba(0, 0, 0, 0.12);
  --sh-elevated: 0 8px 24px rgba(0, 0, 0, 0.08);
  --sh-modal:   0 16px 48px rgba(0, 0, 0, 0.12);

  /* Glass — lighter opacity */
  --glass-bg:     oklch(0.97 0.002 286 / 0.50);
  --glass-border: oklch(0.87 0.005 286 / 0.30);
}

🔄 Theme Switching

React Implementation

// hooks/useTheme.ts
type Theme = 'light' | 'dark';

function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    // 1. Check localStorage
    const stored = localStorage.getItem('theme');
    if (stored === 'light' || stored === 'dark') return stored;

    // 2. Check system preference
    return window.matchMedia('(prefers-color-scheme: light)').matches
      ? 'light'
      : 'dark';
  });

  useEffect(() => {
    // Apply theme to document
    document.documentElement.setAttribute('data-theme', theme);
    localStorage.setItem('theme', theme);

    // Toggle Tailwind dark class
    document.documentElement.classList.toggle('dark', theme === 'dark');
  }, [theme]);

  const toggle = useCallback(() => {
    setTheme(prev => prev === 'dark' ? 'light' : 'dark');
  }, []);

  return { theme, setTheme, toggle } as const;
}

Scream-Free Architecture

Theme switching tidak perlu re-render seluruh komponen. Karena CSS variables diubah di :root, browser secara otomatis me-repain semua elemen yang menggunakan var tersebut.


🎯 Token Mapping Rules

Design Token CSS Variable Tailwind Mapping
Page background --clr-surface-base bg-background
Card surface --clr-surface-elevated bg-card
Body text --clr-text text-foreground
Secondary text --clr-text-secondary text-muted-foreground
Primary button --clr-primary bg-primary
Primary text on button --clr-text-on-primary text-primary-foreground
Border --clr-border border-border
Card shadow --sh-card shadow-sm

🎨 System Theme (prefers-color-scheme)

/* Default: dark */
:root { /* dark variables */ }

/* System light */
@media (prefers-color-scheme: light) {
  :root { /* light variables */ }
}

/* Manual override via data-theme */
[data-theme="dark"] { /* dark variables */ }
[data-theme="light"] { /* light variables */ }

Priority:

  1. data-theme attribute (manual override) — highest
  2. prefers-color-scheme (system) — medium
  3. Default (dark) — fallback

📦 Theme-aware Component Pattern

// Komponen tidak perlu tahu theme — cukup pakai CSS vars
function ThemeAwareCard() {
  return (
    <div className="rounded-xl border border-border bg-card text-card-foreground shadow-sm">
      {/* Konten — styling otomatis berubah sesuai theme */}
    </div>
  );
}

Dark-mode Specific Adjustments

/* Hanya untuk theme dark */
[data-theme="dark"] .particle-orbs {
  opacity: 0.6;
}

/* Hanya untuk theme light */
[data-theme="light"] .particle-orbs {
  opacity: 0.3;
}

⚠️ Theme Anti-Patterns

Color hardcoding

/* ❌ JANGAN — tidak akan berubah saat theme switch */
.card { background: #1e1e2e; }

/* ✅ CSS variable — otomatis mengikuti theme */
.card { background: var(--clr-surface-elevated); }

Theme-specific logic in components

// ❌ JANGAN — komponen tahu soal theme
function Card() {
  const { theme } = useTheme();
  return <div className={theme === 'dark' ? 'bg-gray-800' : 'bg-white'} />;
}

// ✅ Komponen tidak perlu tahu — CSS vars handle semua
function Card() {
  return <div className="bg-card" />;
}

🔗 Referensi

Sumber Konsep
CSS Custom Properties CSS vars
prefers-color-scheme System theme
OKLCH in CSS Color space

"Tema adalah kulit yang berganti — esensi tetap sama, wajah yang baru." ❄️🩵