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
This commit is contained in:
asepharyana
2026-07-02 00:02:41 +07:00
parent d5c22a3959
commit d59b59a7a7
91 changed files with 11165 additions and 674 deletions
+383
View File
@@ -0,0 +1,383 @@
# Color System — Éclat Spectral
> *"Color is the keyboard, the eyes are the harmonies, the soul is the piano with many strings."*
> — Wassily Kandinsky, diadaptasi untuk sistem desain modern.
---
## 🎯 Filosofi Warna
Sistem warna BETE dibangun di atas tiga fondasi:
1. **OKLCH** — Color space perceptually uniform untuk konsistensi antar device
2. **HCT (Hue-Chroma-Tone)** — Sistem warna Material You yang adaptif
3. **Semantic Tokens** — Abstraksi makna, bukan nilai literal
Setiap warna memiliki **lightness (L), chroma (C), hue (H)** yang independen. Ini memungkinkan:
- **Scale generation** — Variasi lightness dari 0100% dengan chroma yang sama
- **Theming** — Cukup ganti hue, seluruh tema bergeser
- **Accessibility** — Kontras dihitung dari lightness, cocok untuk WCAG AA/AAA
---
## 🎨 Primary Palette
### Brand Spectrum: "Aetherial Blue"
Rona utama IMPHNEN — biru yang dingin seperti es Amphoreus, namun hangat dalam interaksi.
```css
/* OKLCH Base — Primary */
--clr-primary-50: oklch(0.95 0.025 255);
--clr-primary-100: oklch(0.90 0.045 255);
--clr-primary-200: oklch(0.80 0.080 255);
--clr-primary-300: oklch(0.70 0.120 255);
--clr-primary-400: oklch(0.62 0.150 255);
--clr-primary-500: oklch(0.55 0.175 255); /* ★ Base primary */
--clr-primary-600: oklch(0.47 0.160 255);
--clr-primary-700: oklch(0.40 0.140 255);
--clr-primary-800: oklch(0.32 0.115 255);
--clr-primary-900: oklch(0.25 0.090 255);
--clr-primary-950: oklch(0.18 0.060 255);
```
**Deskripsi Hue 255°:** Biru jernih dengan sedikit cyan — warna langit senja di Amphoreus. Tidak terlalu agresif seperti biru korporat (#007bff), tidak terlalu playful seperti cyan (#00bcd4).
### Neutral Spectrum: "Glacial Scale"
Abu-abu yang tidak hangat (tidak kekuningan) dan tidak dingin (tidak kebiruan) — benar-benar netral.
```css
--clr-neutral-50: oklch(0.985 0.001 286);
--clr-neutral-100: oklch(0.970 0.001 286);
--clr-neutral-200: oklch(0.920 0.003 286);
--clr-neutral-300: oklch(0.870 0.005 286);
--clr-neutral-400: oklch(0.750 0.010 286);
--clr-neutral-500: oklch(0.620 0.015 286); /* ★ Base neutral */
--clr-neutral-600: oklch(0.500 0.020 286);
--clr-neutral-700: oklch(0.380 0.025 286);
--clr-neutral-800: oklch(0.260 0.030 286);
--clr-neutral-900: oklch(0.180 0.030 286);
--clr-neutral-950: oklch(0.110 0.025 286);
```
---
## 🌈 Extended Palette
### Accent Colors
| Palette | Hue | Chroma | Base (500) | Karakter |
|---------|-----|--------|------------|----------|
| **Ruby** (Destructive) | 25° | 0.165 | `oklch(0.55 0.165 25)` | Darah — urgensi, error |
| **Emerald** (Success) | 145° | 0.130 | `oklch(0.60 0.130 145)` | Pertumbuhan — sukses, aman |
| **Amber** (Warning) | 75° | 0.120 | `oklch(0.70 0.120 75)` | Matahari — peringatan, atensi |
| **Amethyst** (Premium) | 285° | 0.100 | `oklch(0.55 0.100 285)` | Mewah — fitur premium, VIP |
| **Rose** (AI / Feminine) | 350° | 0.110 | `oklch(0.60 0.110 350)` | Kecerdasan — AI analysis, insight |
| **Cyan** (Info) | 200° | 0.120 | `oklch(0.65 0.120 200)` | Informasi — tooltip, hint |
Setiap palette memiliki scale 50950 mengikuti pola primary. Contoh:
```css
--clr-ruby-500: oklch(0.55 0.165 25);
--clr-ruby-600: oklch(0.47 0.150 25);
--clr-emerald-500: oklch(0.60 0.130 145);
--clr-amber-500: oklch(0.70 0.120 75);
```
### Semantic Surface Colors — Dark Theme
```css
/* Base surfaces */
--clr-surface-base: oklch(0.11 0.010 286); /* Darkest bg */
--clr-surface-elevated: oklch(0.14 0.015 286); /* Card surface */
--clr-surface-overlay: oklch(0.17 0.020 286); /* Modal/dropdown */
--clr-surface-sunken: oklch(0.08 0.005 286); /* Input bg */
/* Interactive states */
--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); /* Primary tint */
```
### Semantic Surface Colors — Light Theme
```css
--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-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);
```
---
## 📐 Text Colors & Accessibility
### Foreground Scale
```css
--clr-text-primary: oklch(0.95 0.005 286); /* High emphasis — body */
--clr-text-secondary: oklch(0.70 0.015 286); /* Medium emphasis — metadata */
--clr-text-tertiary: oklch(0.50 0.020 286); /* Low emphasis — placeholder */
--clr-text-disabled: oklch(0.35 0.020 286); /* Disabled state */
--clr-text-inverse: oklch(0.11 0.010 286); /* On colored backgrounds */
/* On-brand backgrounds */
--clr-text-on-primary: oklch(0.97 0.005 286); /* Text on primary bg */
--clr-text-on-destructive: oklch(0.97 0.005 286); /* Text on destructive bg */
```
### Kontras Minimum
| Level | Rasio | Usage | Elemen |
|-------|-------|-------|--------|
| **AA** | 4.5:1 | Body text normal | `--clr-text-primary` di atas surface |
| **AA Large** | 3:1 | Teks ≥18px/≥14px bold | Heading, label |
| **AAA** | 7:1 | Teks penting | Legal, alert, critical info |
### Verifikasi Kontras
```typescript
// utils/contrast.ts
function meetsWCAGAA(foreground: OklchColor, background: OklchColor): boolean {
const contrast = relativeLuminance(background) / relativeLuminance(foreground);
return contrast >= 4.5;
}
// Helper function untuk mendapatkan lightness aman
function accessibleLightness(hue: number, chroma: number, bgLightness: number): number {
// Mencari lightness minimum yang memenuhi 4.5:1
for (let l = 1.0; l > 0; l -= 0.01) {
if (getContrastRatio(l, chroma, hue, bgLightness) >= 4.5) return l;
}
return 0.5;
}
```
---
## 🪞 Glass & Frosted Effects
Estetika glassmorphism menggunakan **opacity + backdrop-blur**:
```css
/* Glass card — frosted glass */
--glass-bg: oklch(0.15 0.015 286 / 0.60);
--glass-border: oklch(0.25 0.030 286 / 0.20);
--glass-blur: 16px;
--glass-shadow: 0 8px 32px oklch(0 0 0 / 0.25);
/* Glass strong — modal/dialog */
--glass-strong-bg: oklch(0.18 0.020 286 / 0.85);
--glass-strong-blur: 24px;
/* Glass subtle — sidebar */
--glass-subtle-bg: oklch(var(--clr-surface-base) / 0.50);
--glass-subtle-blur: 8px;
```
### Menggabungkan di CSS:
```css
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
}
```
---
## 🌟 Glow & Light Effects
### Glow Tokens
```css
/* Primary glow — untuk elemen interaktif, loading state */
--glow-primary: 0 0 20px oklch(0.55 0.175 255 / 0.3);
--glow-primary-soft: 0 0 12px oklch(0.55 0.175 255 / 0.15);
/* Success glow — untuk badge, notifikasi sukses */
--glow-success: 0 0 16px oklch(0.60 0.130 145 / 0.25);
/* Destructive glow — untuk alert error */
--glow-error: 0 0 16px oklch(0.55 0.165 25 / 0.25);
/* AI glow — untuk analysis badge, pulsing indicator */
--glow-ai: 0 0 20px oklch(0.60 0.110 350 / 0.25);
```
### Pulse Animation dengan Glow
```css
@keyframes glow-pulse {
0%, 100% {
box-shadow: var(--glow-primary-soft);
}
50% {
box-shadow: var(--glow-primary);
}
}
.ai-analysis-badge {
animation: glow-pulse 2s ease-in-out infinite;
}
```
---
## 🌗 Theme Tokens
### Dark Mode (Default)
```css
[data-theme="dark"] {
/* Base */
--clr-base: var(--clr-surface-base);
--clr-elevated: var(--clr-surface-elevated);
--clr-overlay: var(--clr-surface-overlay);
--clr-sunken: var(--clr-surface-sunken);
/* Text */
--clr-text: var(--clr-text-primary);
--clr-text-muted: var(--clr-text-secondary);
/* Brand adjustments for dark */
--clr-primary: var(--clr-primary-400); /* Lebih terang di dark */
--clr-primary-bg: oklch(0.25 0.060 255 / 0.20);
}
```
### Light Mode
```css
[data-theme="light"] {
--clr-base: oklch(0.97 0.002 286);
--clr-elevated: oklch(1.00 0.000 286);
--clr-overlay: oklch(0.95 0.003 286);
--clr-sunken: oklch(0.92 0.004 286);
--clr-text: oklch(0.11 0.010 286);
--clr-text-muted: oklch(0.50 0.020 286);
--clr-primary: var(--clr-primary-500); /* Standard di light */
--clr-primary-bg: oklch(0.90 0.060 255 / 0.25);
}
```
---
## 📊 Moderation Severity Colors
Sistem moderasi menggunakan gradasi keparahan dari aman hingga kritis:
```css
--clr-severity-safe: oklch(0.60 0.130 145); /* Emerald — aman */
--clr-severity-low: oklch(0.70 0.120 75); /* Amber — rendah */
--clr-severity-medium: oklch(0.65 0.150 50); /* Orange — sedang */
--clr-severity-high: oklch(0.60 0.150 30); /* Red-orange — tinggi */
--clr-severity-critical: oklch(0.55 0.165 25); /* Ruby — kritis */
```
### Background Variations (untuk chip/badge)
```css
--clr-severity-safe-bg: oklch(0.60 0.130 145 / 0.15);
--clr-severity-low-bg: oklch(0.70 0.120 75 / 0.15);
--clr-severity-medium-bg: oklch(0.65 0.150 50 / 0.15);
--clr-severity-high-bg: oklch(0.60 0.150 30 / 0.15);
--clr-severity-critical-bg: oklch(0.55 0.165 25 / 0.15);
```
---
## 🧪 Color Usage Decision Tree
```
Butuh warna untuk...
├── Surface / Background → Gunakan --clr-surface-* (base/elevated/overlay)
├── Text → Gunakan --clr-text-* (primary/secondary/tertiary)
├── Interactive element →
│ ├── Button utama → --clr-primary, --clr-primary-hover
│ ├── Button danger → --clr-ruby-500
│ ├── Link → --clr-primary-400 (dark) / --clr-primary-600 (light)
│ └── Input focus → --clr-primary ring
├── Status indicator →
│ ├── Success → --clr-emerald-* atau --clr-severity-safe
│ ├── Warning → --clr-amber-* atau --clr-severity-low
│ ├── Error → --clr-ruby-* atau --clr-severity-critical
│ └── Info → --clr-cyan-*
├── Data visualization → Gunakan palette chart (lihat patterns/07)
└── Moderation badge → Gunakan --clr-severity-*
```
---
## ⚠️ Anti-Patterns (Yang Tidak Boleh Dilakukan)
### ❌ Hardcoded HEX/RGB
```css
/* ❌ JANGAN — tidak akan terpengaruh theme switching */
.notification-success {
background: #d4edda;
color: #155724;
}
/* ✅ Gunakan token semantic */
.notification-success {
background: var(--clr-emerald-100);
color: var(--clr-emerald-800);
}
```
### ❌ Langsung pakai Tailwind utility colors
```tsx
{/* ❌ JANGAN — hardcoded ke skema tertentu */}
<Badge className="bg-emerald-100 text-emerald-700" />
{/* ✅ Gunakan semantic variant */}
<Badge variant="success" />
```
### ❌ Mengabaikan kontras
```css
/* ❌ JANGAN — teks abu-abu di atas abu-abu */
.metadata {
color: oklch(0.65 0.015 286); /* L=0.65 */
background: oklch(0.70 0.010 286); /* L=0.70 — rasio ~1.1:1! */
}
/* ✅ Minimum kontras 3:1 untuk secondary text */
.metadata {
color: oklch(0.50 0.020 286); /* L=0.50 */
background: var(--clr-surface-base); /* L=0.11 */
/* Rasio ~6:1 — aman */
}
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [OKLCH Color Picker](https://oklch.com/) | Visualisasi OKLCH color space |
| [WCAG Contrast Checker](https://webaim.org/resources/contrastchecker/) | Verifikasi kontras |
| [Material HCT](https://material.io/blog/science-of-color-design) | Hue-Chroma-Tone system |
| [Tailwind CSS OKLCH](https://tailwindcss.com/docs/colors#using-custom-colors) | Implementasi OKLCH |
---
*"Warna adalah ingatan yang tak pernah pudar — dibiaskan melalui prisma es Amphoreus."* ❄️🩵
+371
View File
@@ -0,0 +1,371 @@
# Typography — The Voice of Glass
> *"Typography is the craft of endowing human language with a durable visual form."*
> — Robert Bringhurst, *The Elements of Typographic Style*
---
## 🎯 Filosofi Tipografi
Tipografi BETE dibangun di atas tiga pilar:
1. **Hierarki melalui weight & size** — Bukan sekadar memperbesar heading, tapi memberi bobot makna
2. **Ritme vertikal yang konsisten** — Setiap elemen teks berada dalam grid ritme 4px
3. **Keterbacaan sebagai prioritas utama** — Sebelum estetika, sebelum gaya
---
## 📐 Type Scale: Fluid Modular Scale
Kita menggunakan **modular scale** 1.25 (major third) yang **fluid** — menyesuaikan antara viewport.
```css
/* Font size scale — fluid, minor third (1.125) hingga major third (1.25) */
--fs-xs: clamp(0.69rem, 0.69rem + 0.01vw, 0.75rem); /* 1112px */
--fs-sm: clamp(0.81rem, 0.81rem + 0.02vw, 0.88rem); /* 1314px */
--fs-base: clamp(0.94rem, 0.94rem + 0.03vw, 1.00rem); /* 1516px ★ */
--fs-md: clamp(1.06rem, 1.06rem + 0.04vw, 1.13rem); /* 1718px */
--fs-lg: clamp(1.19rem, 1.19rem + 0.06vw, 1.25rem); /* 1920px */
--fs-xl: clamp(1.31rem, 1.31rem + 0.08vw, 1.50rem); /* 2124px */
--fs-2xl: clamp(1.50rem, 1.50rem + 0.12vw, 1.88rem); /* 2430px */
--fs-3xl: clamp(1.69rem, 1.69rem + 0.18vw, 2.25rem); /* 2736px */
--fs-4xl: clamp(1.88rem, 1.88rem + 0.26vw, 2.81rem); /* 3045px */
--fs-5xl: clamp(2.25rem, 2.25rem + 0.38vw, 3.50rem); /* 3656px */
--fs-6xl: clamp(2.50rem, 2.50rem + 0.50vw, 4.00rem); /* 4064px */
```
> **Mengapa clamp()?** Font size yang terlalu besar di mobile dan terlalu kecil di desktop adalah masalah UX klasik. Dengan `clamp()`, kita dapatkan ukuran yang optimal di setiap viewport tanpa media query.
### Line Height
```css
--lh-tight: 1.15; /* Heading besar, display text */
--lh-normal: 1.50; /* Body text, paragraphs */
--lh-relaxed: 1.65; /* Long-form reading */
--lh-compact: 1.25; /* UI labels, badges, small text */
```
### Font Weight Tokens
```css
--fw-light: 300;
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--fw-extrabold: 800;
```
---
## 🔤 Font Family
### Primary: "Outfit" — Modern Geometric Sans
```css
--ff-sans: 'Outfit', system-ui, -apple-system, sans-serif;
--ff-display: 'Outfit', system-ui, -apple-system, sans-serif;
```
Mengapa **Outfit** menggantikan Poppins?
- **Geometric precision** — Bentuk huruf yang bersih, cocok untuk UI modern
- **Low x-height** — Memberi kesan elegan dan lega
- **Variable font support** — Satu file untuk semua weight, performa lebih baik
- **Open-source** — SIL Open Font License
### Monospace: "JetBrains Mono"
```css
--ff-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
```
Untuk: ID, timestamp, kode, data teknis, metrik.
### Loading Strategy
```html
<!-- Variable font — cukup satu file untuk seluruh weight -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300..800&family=JetBrains+Mono:wght@400..700&display=swap" rel="stylesheet" />
```
```css
/* Fallback font stack dengan @font-face untuk cache lokal */
@font-face {
font-family: 'Outfit Fallback';
src: local('Segoe UI'), local('Roboto'), local('Helvetica Neue');
size-adjust: 95%; /* Mengurangi layout shift (CLS) */
ascent-override: 90%;
}
```
---
## 📋 Type Styles — The Complete Catalog
### Display / Hero
```css
.display-1 {
font-family: var(--ff-display);
font-size: var(--fs-6xl);
font-weight: var(--fw-extrabold);
line-height: var(--lh-tight);
letter-spacing: -0.03em;
}
.display-2 {
font-family: var(--ff-display);
font-size: var(--fs-5xl);
font-weight: var(--fw-bold);
line-height: var(--lh-tight);
letter-spacing: -0.02em;
}
```
### Headings
```css
.h1 {
font-family: var(--ff-sans);
font-size: var(--fs-4xl);
font-weight: var(--fw-bold);
line-height: var(--lh-tight);
letter-spacing: -0.02em;
}
.h2 {
font-family: var(--ff-sans);
font-size: var(--fs-3xl);
font-weight: var(--fw-semibold);
line-height: var(--lh-tight);
letter-spacing: -0.015em;
}
.h3 {
font-family: var(--ff-sans);
font-size: var(--fs-2xl);
font-weight: var(--fw-semibold);
line-height: var(--lh-tight);
letter-spacing: -0.01em;
}
.h4 {
font-family: var(--ff-sans);
font-size: var(--fs-xl);
font-weight: var(--fw-medium);
line-height: var(--lh-normal);
letter-spacing: -0.005em;
}
```
### Body
```css
.body-large {
font-family: var(--ff-sans);
font-size: var(--fs-md);
font-weight: var(--fw-regular);
line-height: var(--lh-relaxed);
}
.body {
font-family: var(--ff-sans);
font-size: var(--fs-base);
font-weight: var(--fw-regular);
line-height: var(--lh-normal);
}
.body-small {
font-family: var(--ff-sans);
font-size: var(--fs-sm);
font-weight: var(--fw-regular);
line-height: var(--lh-normal);
}
.body-compact {
font-family: var(--ff-sans);
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
line-height: var(--lh-compact);
}
```
### UI / Label
```css
.label {
font-family: var(--ff-sans);
font-size: var(--fs-xs);
font-weight: var(--fw-semibold);
line-height: var(--lh-compact);
letter-spacing: 0.06em; /* UPPERCASE labels get wider tracking */
text-transform: uppercase;
}
.caption {
font-family: var(--ff-sans);
font-size: var(--fs-xs);
font-weight: var(--fw-regular);
line-height: var(--lh-normal);
color: var(--clr-text-secondary);
}
.mono {
font-family: var(--ff-mono);
font-size: var(--fs-sm);
font-weight: var(--fw-regular);
line-height: var(--lh-normal);
}
.badge {
font-family: var(--ff-sans);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
line-height: 1;
}
```
---
## 📊 Type Table — Mapping ke Penggunaan
| Token | Penggunaan | Contoh |
|-------|-----------|--------|
| `.display-1` | Halaman kosong, 404, hero section | "Nothing to see here" |
| `.display-2` | Empty state utama | "No messages yet" |
| `.h1` | Judul halaman | "Dashboard", "Messages" |
| `.h2` | Judul section panel | "Voice Connections", "Analytics" |
| `.h3` | Judul card | Nama user, channel |
| `.h4` | Sub-section, tab content | "Active Speakers", "Filters" |
| `.body` | Paragraf, konten utama | Pesan teks, deskripsi |
| `.body-small` | Metadata, secondary info | Timestamp, username |
| `.body-compact` | Dense lists | Daftar items compact |
| `.label` | Form label, section header | "CHANNEL", "USERNAME" |
| `.caption` | Hint, footnote, helper | "Click to expand" |
| `.mono` | ID, kode, data teknis | "Channel #12345" |
| `.badge` | Chip, status indicator | "AI Analysis", "Flagged" |
---
## 🎭 Rich Text & Emphatic Styles
```css
/* Links */
a, .link {
color: var(--clr-primary-400);
text-decoration: none;
transition: opacity var(--dur-fast) var(--ease-out);
}
a:hover, .link:hover {
opacity: 0.8;
text-decoration: underline;
text-underline-offset: 2px;
}
/* Code inline */
code, .code-inline {
font-family: var(--ff-mono);
font-size: 0.9em;
padding: 0.125em 0.375em;
background: var(--clr-surface-sunken);
border-radius: var(--rd-xs);
}
/* Truncation */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Multi-line truncation */
.text-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.text-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
```
---
## 📏 Vertical Rhythm
Menggunakan sistem baseline 4px (bukan 8px default) untuk tipografi:
```css
:root {
--baseline: 4px;
}
/* Heading margin */
.h1 { margin-bottom: calc(var(--baseline) * 4); } /* 16px */
.h2 { margin-bottom: calc(var(--baseline) * 3); } /* 12px */
.h3 { margin-bottom: calc(var(--baseline) * 3); } /* 12px */
.h4 { margin-bottom: calc(var(--baseline) * 2); } /* 8px */
/* Paragraph spacing */
p, .body {
margin-bottom: calc(var(--baseline) * 4); /* 16px */
}
p + p {
margin-top: calc(var(--baseline) * 2); /* 8px — reduced between consecutive paragraphs */
}
```
---
## ⚠️ Anti-Patterns Tipografi
### ❌ Ukuran absolut tanpa fluid
```css
/* ❌ JANGAN — title 32px di mobile terlalu besar */
.page-title { font-size: 32px; }
/* ✅ clamp menyesuaikan viewport */
.page-title { font-size: var(--fs-4xl); }
```
### ❌ Line height terlalu kecil untuk body text
```css
/* ❌ JANGAN — crowded, sulit dibaca */
.body-text { font-size: 16px; line-height: 1.2; }
/* ✅ Line height yang cukup untuk readability */
.body-text { font-size: var(--fs-base); line-height: var(--lh-normal); }
```
### ❌ Terlalu banyak type scale
```css
/* ❌ JANGAN — 29px, 28px, 27px, 26px adalah noise */
.custom-1 { font-size: 29px; }
.custom-2 { font-size: 28px; }
/* ✅ Gunakan scale yang terdefinisi */
.custom { font-size: var(--fs-4xl); }
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Type Scale Calculator](https://typescale.com/) | Modular scale generation |
| [Outfit on Google Fonts](https://fonts.google.com/specimen/Outfit) | Font spesifikasi |
| [Utopia.fyi](https://utopia.fyi/) | Fluid type scale calculator |
| [JetBrains Mono](https://www.jetbrains.com/lp/mono/) | Font monospace |
---
*"Huruf adalah jejak ingatan yang tak kasatmata — ia berbicara tanpa suara."* ❄️🩵
+349
View File
@@ -0,0 +1,349 @@
# Spatial System — The Architecture of Void
> *"Space is the breath of art."*
> — Frank Lloyd Wright, arsitek organik.
---
## 🎯 Filosofi Spasial
Ruang dalam BETE bukan sekadar "tempat kosong" — ia adalah **medium komunikasi visual**. Jarak antarelemen menyampaikan hubungan semantik:
- **Dekat** → Elemen terkait secara konseptual
- **Berjarak** → Elemen independen atau batch berbeda
- **Terpisah jauh** → Section baru, hierarki turun
Kita menggunakan **4px baseline grid** untuk semua keputusan spasial.
---
## 📐 Grid System: 8px × 4px Hybrid
BETE menggunakan sistem **8px untuk layout kasar**, **4px untuk fine-tuning**.
```css
:root {
/* Base grid unit */
--grid-unit: 4px;
/* Spacing scale — exponential */
--sp-0: 0px;
--sp-0.5: calc(var(--grid-unit) * 1); /* 4px — micro spacing */
--sp-1: calc(var(--grid-unit) * 2); /* 8px — tight spacing */
--sp-2: calc(var(--grid-unit) * 3); /* 12px — compact spacing */
--sp-3: calc(var(--grid-unit) * 4); /* 16px — base spacing ★ */
--sp-4: calc(var(--grid-unit) * 6); /* 24px — relaxed spacing */
--sp-5: calc(var(--grid-unit) * 8); /* 32px — section spacing */
--sp-6: calc(var(--grid-unit) * 12); /* 48px — panel spacing */
--sp-7: calc(var(--grid-unit) * 16); /* 64px — page spacing */
--sp-8: calc(var(--grid-unit) * 24); /* 96px — hero spacing */
}
```
### Logic di Balik Scale
| Token | px | Konteks |
|-------|----|---------|
| `--sp-0.5` | 4px | Ikon-padding, badge spacing, dot indicators |
| `--sp-1` | 8px | Avatar-text gap, icon-button padding |
| `--sp-2` | 12px | Button padding, chip spacing, input padding |
| `--sp-3` | 16px | **Base unit** — card-padding, section margin, form gap |
| `--sp-4` | 24px | Card gap, panel padding, modal padding |
| `--sp-5` | 32px | Content area padding, desktop sidebar width |
| `--sp-6` | 48px | Page section gap, dashboards grid gap |
| `--sp-7` | 64px | Page padding desktop, hero spacing |
| `--sp-8` | 96px | Empty state height, large break sections |
---
## 🔲 Layout Components
### Page Layout (Desktop)
```
┌─────────────────────────────────────────────────────┐
│ ┌── Header ──┐ │ 56px
│ └────────────┘ │
│ ┌─ Sidebar ─┐ ┌────────── Main Content ──────────┐│
│ │ │ │ ┌─ Page Title ─────────────────┐ ││
│ │ icon grid │ │ │ Section Heading │ ││
│ │ ──────── │ │ └─────────────────────────────┘ ││
│ │ nav-1 │ │ ││
│ │ nav-2 │ │ ┌────── Grid Area ─────────────┐ ││
│ │ nav-3 │ │ │ ┌── Card ──┐ ┌── Card ──┐ │ ││
│ │ │ │ │ │ │ │ │ │ ││
│ │ ──────── │ │ │ └──────────┘ └──────────┘ │ ││
│ │ mascot │ │ │ ┌── Card ──┐ ┌── Card ──┐ │ ││
│ │ │ │ │ │ │ │ │ │ ││
│ └────────────┘ │ │ └──────────┘ └──────────┘ │ ││
│ │ └───────────────────────────────┘ ││
│ w-64/ │ flex-1 ││
│ w-16(icon) │ ││
└─────────────────────────────────────────────────────┘
```
### Vue 3 Component Mapping
```tsx
// DashboardLayout.vue
<template>
<div class="page-layout">
<Sidebar :collapsed="sidebarCollapsed" />
<main class="main-area">
<Header />
<div class="content-area">
<slot />
</div>
</main>
</div>
</template>
```
```css
.page-layout {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
min-height: 100vh;
}
.sidebar {
grid-row: 1 / -1;
width: 256px; /* w-64 */
transition: width var(--dur-normal) var(--ease-out-quint);
}
.sidebar.collapsed {
width: 64px; /* w-16 */
}
.main-area {
display: flex;
flex-direction: column;
min-width: 0; /* Prevent grid blowout */
overflow: hidden;
}
.content-area {
flex: 1;
overflow-y: auto;
padding: var(--sp-5);
}
```
---
## 🔳 Border Radius Scale
```css
--rd-none: 0px;
--rd-xs: 4px; /* Checkbox, toggle, small indicators */
--rd-sm: 6px; /* Input, button small, badges */
--rd-md: 8px; /* Button default, card, modal */
--rd-lg: 12px; /* Card elevated, sheets, panels */
--rd-xl: 16px; /* Dialog, bottom sheet */
--rd-2xl: 20px; /* Full-width cards on mobile */
--rd-full: 9999px; /* Pill, chip, avatar */
```
### Radius Decision Tree
```
Elemen interaktif?
├── Ya ─→ butuh affordance visual?
│ ├── Ya, utama (button, card clickable) → --rd-md (8px)
│ └── Tidak (chip, tag, avatar) → --rd-full (pill)
└── Tidak → container?
├── Dialog/modal → --rd-xl (16px)
├── Card dalam grid → --rd-lg (12px)
└── Sheet/panel → --rd-xl atau --rd-2xl
```
---
## 🥞 Z-Index Registry (Formal)
```css
:root {
--z-base: 0;
--z-dropdown: 10;
--z-sticky: 20;
--z-header: 30;
--z-sidebar: 40;
--z-overlay: 50; /* Mobile sidebar, backdrop */
--z-modal: 60; /* Dialog, confirm */
--z-popover: 70; /* Tooltip, popover, dropdown menu */
--z-toast: 80; /* Toast notification */
--z-mascot: 100; /* Mascot chatbot — highest */
}
/* Implementation */
.header { z-index: var(--z-header); }
.sidebar { z-index: var(--z-sidebar); }
.modal { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); }
.mascot-chat { z-index: var(--z-mascot); }
```
### Stacking Order
```
Layer Value Elemen
─────────────────────────────────────────────
Background -1 ParticleBackground
Base 0 Layout, cards, text
Dropdown 10 Select options, context menu
Sticky 20 Sticky section headers
Header 30 Sticky page header
Sidebar 40 Desktop sidebar
Overlay 50 Backdrop, mobile drawer
Modal 60 Confirm dialog, modal
Popover 70 Tooltip, dropdown
Toast 80 Toast notifications
Mascot 100 Chatbot floating panel
```
---
## 📱 Responsive Breakpoints
```css
/* CSS Custom Properties for breakpoints */
:root {
--bp-sm: 640px;
--bp-md: 768px;
--bp-lg: 1024px;
--bp-xl: 1280px;
--bp-2xl: 1536px;
}
/* Container max-width */
--container-sm: 640px;
--container-md: 768px;
--container-lg: 1024px;
--container-xl: 1280px;
```
### Layout Behavior per Breakpoint
| Breakpoint | Sidebar | Content Padding | Grid Columns |
|-----------|---------|-----------------|--------------|
| `<640px` | Bottom tab (56px) | `--sp-3` (16px) | 1 |
| `640768px` | Bottom tab | `--sp-4` (24px) | 1 |
| `7681024px` | Icon-only (64px) | `--sp-4` (24px) | 12 |
| `10241280px` | Full (256px) | `--sp-5` (32px) | 23 |
| `1280px+` | Full (256px) | `--sp-5` (32px) | 24 |
---
## 📦 Common Layout Patterns
### Card Grid
```css
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--sp-4);
}
```
### Two-Column Detail
```css
.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sp-4);
}
@media (max-width: 768px) {
.two-column {
grid-template-columns: 1fr;
}
}
```
### Sidebar + Content (Live Panel)
```css
.live-layout {
display: grid;
grid-template-columns: 1fr 320px; /* Content 1fr, sidebar fixed */
gap: var(--sp-4);
}
@media (max-width: 1024px) {
.live-layout {
grid-template-columns: 1fr; /* Stack on smaller screens */
}
}
.live-sidebar {
position: sticky;
top: calc(56px + var(--sp-4)); /* Below header */
max-height: calc(100vh - 56px - var(--sp-4) * 2);
overflow-y: auto;
}
```
---
## 🧠 Container Queries (Modern Approach)
Untuk komponen yang reusable di berbagai konteks:
```css
.card-grid-component {
container-type: inline-size;
container-name: card-grid;
}
@container card-grid (max-width: 400px) {
.card-item {
grid-template-columns: 1fr; /* Single column di container kecil */
}
}
@container card-grid (min-width: 401px) {
.card-item {
grid-template-columns: 1fr 1fr;
}
}
```
---
## ⚠️ Anti-Patterns Spasial
### ❌ Margin collapse tanpa sengaja
```css
/* ❌ JANGAN — flex gap jauh lebih aman */
.card + .card { margin-top: 16px; } /* Rawan collapse */
/* ✅ Gunakan gap */
.card-grid { display: flex; flex-direction: column; gap: var(--sp-4); }
```
### ❌ Padding tidak konsisten
```css
/* ❌ JANGAN — setiap file punya padding sendiri */
.page-a { padding: 20px; }
.page-b { padding: 24px; }
/* ✅ Gunakan spacing token */
.page { padding: var(--sp-5); }
```
### ❌ Grid blowout (min-width tanpa min-width: 0)
```css
/* ❌ JANGAN — grid item dengan teks panjang mendorong layout */
.grid-item { overflow: visible; }
/* ✅ Cegah blowout */
.grid-item { min-width: 0; overflow: hidden; }
```
---
*"Ruang adalah kanvas tempat ingatan menari — setiap piksel memiliki tempatnya."* ❄️🩵
+552
View File
@@ -0,0 +1,552 @@
# Motion System — The Dance of Glass
> *"Animation is not about making things move. It's about making things *believe*."*
> — Richard Williams, *The Animator's Survival Kit*
---
## 🎯 Filosofi Gerak
Animasi di BETE bukan sekadar efek visual — ia adalah **bahasa kinetik** yang mengkomunikasikan relasi spasial antar elemen:
| Gerakan | Makna |
|---------|-------|
| **Slide from right** | Elemen baru datang dari "luar" — panel, drawer |
| **Fade in + scale** | Muncul dari "dalam" — modal, dialog |
| **Slide up** | Konten baru melanjutkan alur vertikal |
| **Scale + glow** | Sedang diproses — loading, analysis |
| **Spring bounce (subtle)** | Konfirmasi sukses — centang, badge |
---
## ⏱️ Timing & Easing — The Physics Engine
### Duration Tokens
```css
:root {
--dur-instant: 0ms;
--dur-fast: 150ms; /* Hover, active state, toggle */
--dur-normal: 250ms; /* Default transition, card hover */
--dur-slow: 350ms; /* Panel enter/exit, page transition */
--dur-glacial: 500ms; /* Modal, drawer slide, emphasis */
}
```
### Easing Curves
BETE menggunakan tiga easing curve utama, semuanya **custom cubic-bezier**:
```css
:root {
/* Standard ease-out — untuk sebagian besar interaksi */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
/* Ease-out quint — untuk elemen yang "mendarat" */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
/* Ease-in-out — untuk transisi dua arah (accordion, collapse) */
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}
```
### Perbandingan dengan CSS Default
| Nama | Bezier | Karakter |
|------|--------|----------|
| `ease` (default) | `(0.25, 0.1, 0.25, 1)` | Lambat mulai, lambat akhir |
| `ease-out` | `(0, 0, 0.58, 1)` | Cepat mulai |
| `ease-in-out` | `(0.42, 0, 0.58, 1)` | Simetris |
| **`--ease-out`** | `(0.16, 1, 0.3, 1)` | Natural, "berat" di akhir |
| **`--ease-out-quint`** | `(0.22, 1, 0.36, 1)` | Landing yang tegas |
---
## 🏃 Micro-interactions
### Hover State
```css
/* Card hover — subtle lift + shadow deepen */
.card {
transition:
transform var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--sh-hover);
}
/* Button hover — scale up subtly */
.button {
transition: transform var(--dur-fast) var(--ease-out);
}
.button:hover {
transform: scale(1.02);
}
.button:active {
transform: scale(0.98);
}
```
### Active/Press State
```css
.button:active {
transform: scale(0.96);
transition-duration: var(--dur-fast);
}
/* Button ripple effect */
.button.ripple {
position: relative;
overflow: hidden;
}
.button.ripple::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle, rgba(255,255,255,0.3) 10%, transparent 10%);
background-position: center;
background-repeat: no-repeat;
background-size: 1000% 1000%;
opacity: 0;
transition: none;
}
.button.ripple:active::after {
background-size: 0% 0%;
opacity: 1;
transition: background-size 0.4s, opacity 0.4s;
}
```
### Focus Ring
```css
.button:focus-visible {
outline: 2px solid var(--clr-primary-400);
outline-offset: 2px;
}
/* Smooth ring transition */
.input {
transition:
border-color var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
}
.input:focus {
border-color: var(--clr-primary);
box-shadow: 0 0 0 3px var(--clr-primary-bg);
}
```
---
## 🎬 Page & Panel Transitions
### Tab Switch — Choreographed Sequence
```css
/* Stagger container */
.page-transition-enter {
opacity: 0;
}
.page-transition-enter-active {
opacity: 1;
transition: opacity var(--dur-slow) var(--ease-out);
}
/* Children stagger — via Framer Motion / GSAP timeline */
@keyframes stagger-enter {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.stagger-item {
animation: stagger-enter var(--dur-slow) var(--ease-out) both;
}
.stagger-item:nth-child(1) { animation-delay: 40ms; }
.stagger-item:nth-child(2) { animation-delay: 80ms; }
.stagger-item:nth-child(3) { animation-delay: 120ms; }
.stagger-item:nth-child(4) { animation-delay: 160ms; }
.stagger-item:nth-child(5) { animation-delay: 200ms; }
/* ... formula: delay = index * 40ms */
```
### Vue Transition Mode
```vue
<template>
<Transition
name="page"
mode="out-in"
@before-leave="beforeLeave"
@after-enter="afterEnter"
>
<component :is="currentTab" :key="currentTab" />
</Transition>
</template>
<style>
.page-leave-active {
transition: opacity var(--dur-normal) var(--ease-in-out),
transform var(--dur-normal) var(--ease-in-out);
}
.page-leave-to {
opacity: 0;
transform: translateY(-8px);
}
.page-enter-active {
transition: opacity var(--dur-slow) var(--ease-out),
transform var(--dur-slow) var(--ease-out);
}
.page-enter-from {
opacity: 0;
transform: translateY(12px);
}
</style>
```
---
## 🌟 Component-Specific Animations
### Sidebar Expand/Collapse
```css
.sidebar {
width: var(--sidebar-width, 256px);
transition: width var(--dur-slow) var(--ease-out-quint);
}
.sidebar.collapsed {
--sidebar-width: 64px;
}
/* Nav items — icon slides, text fades */
.sidebar-nav-item {
display: flex;
align-items: center;
gap: 12px;
overflow: hidden;
}
.sidebar-nav-item .label {
transition: opacity var(--dur-normal) var(--ease-out),
width var(--dur-normal) var(--ease-out);
white-space: nowrap;
}
.sidebar.collapsed .sidebar-nav-item .label {
opacity: 0;
width: 0;
padding: 0;
}
```
### Toast Notification
```css
.toast-enter-active {
animation: toast-slide-in var(--dur-slow) var(--ease-out-quint);
}
.toast-leave-active {
animation: toast-slide-out var(--dur-normal) var(--ease-in-out);
}
@keyframes toast-slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-slide-out {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
```
### Modal / Dialog
```css
.modal-overlay-enter-active {
transition: opacity var(--dur-normal) var(--ease-out);
}
.modal-overlay-enter-from { opacity: 0; }
.modal-content-enter-active {
animation: modal-scale-in var(--dur-slow) var(--ease-out-quint);
}
@keyframes modal-scale-in {
from {
transform: scale(0.92) translateY(8px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
```
---
## 📊 Special Effects
### Audio Visualizer Bars
```css
.visualizer-bar {
animation: bar-pulse 0.4s ease-in-out infinite;
transform-origin: bottom;
}
.visualizer-bar:nth-child(1) { animation-delay: 0ms; }
.visualizer-bar:nth-child(2) { animation-delay: 75ms; }
.visualizer-bar:nth-child(3) { animation-delay: 150ms; }
.visualizer-bar:nth-child(4) { animation-delay: 225ms; }
@keyframes bar-pulse {
0%, 100% { transform: scaleY(0.8); }
50% { transform: scaleY(1.2); }
}
```
### Skeleton Loading (Shimmer)
```css
.skeleton {
background: linear-gradient(
90deg,
var(--clr-surface-sunken) 25%,
var(--clr-surface-elevated) 50%,
var(--clr-surface-sunken) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
```
### AI Analysis Pulse (Glow)
```css
.ai-badge {
animation: ai-pulse 2s ease-in-out infinite;
}
@keyframes ai-pulse {
0%, 100% {
box-shadow: 0 0 4px var(--clr-rose-500 / 0.2);
}
50% {
box-shadow: 0 0 12px var(--clr-rose-500 / 0.4);
}
}
```
### Ghost Particle (Background)
```css
.particle {
position: fixed;
border-radius: 50%;
pointer-events: none;
animation: float var(--dur-float, 8s) ease-in-out infinite;
animation-delay: var(--delay, 0s);
}
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0) scale(1);
opacity: 0.3;
}
25% {
transform: translateY(-20px) translateX(10px) scale(1.1);
opacity: 0.6;
}
50% {
transform: translateY(-40px) translateX(-5px) scale(0.9);
opacity: 0.4;
}
75% {
transform: translateY(-20px) translateX(15px) scale(1.05);
opacity: 0.5;
}
}
```
---
## 🎯 Motion Decision Tree
```
Elemen apa yang dianimasikan?
├── Hover/Interaksi → dur-fast (150ms) + --ease-out
│ ├── Card → translateY(-2px) + shadow deepen
│ ├── Button → scale(1.02) / scale(0.98)
│ ├── Link → opacity/underline
│ └── Icon → rotate/color
├── Masuk ke halaman → dur-slow (350ms) + stagger
│ ├── Halaman baru → fade + slideY(12px)
│ ├── List items → stagger (40ms per item)
│ └── Modal → scale(0.92→1) + fade overlay
├── Keluar dari halaman → dur-normal (250ms) + ease-in-out
│ ├── Halaman → fade + slideY(-8px)
│ ├── Toast → slideX(100%)
│ └── Modal → scale(→0.95) + fade overlay
├── Loading → infinite loop
│ ├── Skeleton → shimmer 1.5s
│ ├── Spinner → spin
│ └── AI Analysis → glow-pulse 2s
└── State change → dur-normal (250ms) + --ease-out
├── Sidebar → width transition
├── Accordion → height transition
└── Badge → scale(0.8→1)
```
---
## ♿ Reduced Motion
```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;
}
/* But allow opacity transitions for basic UX */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease !important;
}
}
```
### Programmatic Check
```typescript
// hooks/useReducedMotion.ts
export function useReducedMotion(): boolean {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}
```
```vue
// Dalam komponen
<script setup>
const reducedMotion = useReducedMotion();
</script>
<template>
<Transition :duration="reducedMotion ? 0 : 350">
...
</Transition>
</template>
```
---
## ⚠️ Anti-Patterns Animasi
### ❌ Durasi terlalu lama
```css
/* ❌ JANGAN — 1 detik terasa lambat */
.sidebar { transition: width 1s ease; }
/* ✅ 250350ms adalah sweet spot UI */
.sidebar { transition: width var(--dur-slow) var(--ease-out-quint); }
```
### ❌ Semua bergerak bersamaan (tanpa stagger)
```css
/* ❌ JANGAN — tidak graceful */
.card { animation: fadeIn 0.3s ease; }
/* ✅ Stagger menciptakan gelombang natural */
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 40ms; }
```
### ❌ Easing yang salah untuk konteks
```css
/* ❌ JANGAN — ease-in untuk enter terasa lambat di awal */
.modal { animation: scaleIn 0.3s ease-in; }
/* ✅ ease-out untuk enter — cepat mulai, soft berhenti */
.modal { animation: scaleIn 0.3s var(--ease-out-quint); }
```
### ❌ Mengabaikan reduced motion
```css
/* ❌ JANGAN — tidak accessible */
.particle { animation: float 8s infinite; }
/* ✅ Diberhentikan untuk reduced motion */
@media (prefers-reduced-motion: reduce) {
.particle { display: none; }
}
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Easing Functions Cheat Sheet](https://easings.net/) | Visualisasi easing curves |
| [Material Motion](https://m2.material.io/design/motion/) | Sistem motion Google |
| [GSAP](https://gsap.com/) | Production-grade animation library |
| [AnimXYZ](https://animxyz.com/) | Utility-first CSS animations |
---
*"Gerak adalah bahasa ingatan yang tak terucap — setiap transisi adalah cerita."* ❄️🩵
+313
View File
@@ -0,0 +1,313 @@
# Component Architecture — The Glass Library
> *"Design is not just what it looks like and feels like. Design is how it works."*
> — Steve Jobs
---
## 🎯 Filosofi Komponen
Setiap komponen di BETE adalah **self-contained glass panel** yang:
1. **Satu tanggung jawab** — Satu komponen, satu fungsi
2. **State-driven** — Visual merepresentasikan state, bukan sebaliknya
3. **Composable** — Bisa digabung seperti LEGO
4. **Theme-aware** — Menggunakan CSS variables, bukan hardcoded values
5. **Accessible** — Keyboard, screen reader, reduced motion
---
## 📐 Component Taxonomy
```
┌─────────────────────────────────────────────────────────────┐
│ COMPONENT MAP │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─── ATOMS ──────────────────────────────────────────┐ │
│ │ Button │ Badge │ Input │ Label │ Icon │ │
│ │ Avatar │ Skeleton│ Spinner │ Divider│ Tooltip │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─── MOLECULES ───────────────────────────────────────┐ │
│ │ Card │ Tabs │ Select │ │
│ │ Toast │ Modal │ Dropdown │ │
│ │ Pagination │ Breadcrumb │ SearchBar │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─── ORGANISMS ───────────────────────────────────────┐ │
│ │ MessageCard │ VoiceCard │ AnalyticsChart │ │
│ │ ActiveSpeaker │ NowPlaying │ ImageGrid │ │
│ │ MascotChatbot │ AudioViz │ StatsCard │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─── TEMPLATES ───────────────────────────────────────┐ │
│ │ Sidebar │ Header │ DashboardLayout │ │
│ │ AuthForm │ LivePanel │ MessagesPanel │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 🧱 Atomic Components (Atoms)
### Button
```tsx
interface ButtonProps {
variant: 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost';
size: 'sm' | 'default' | 'lg' | 'icon';
loading?: boolean;
disabled?: boolean;
icon?: ReactNode;
children?: ReactNode;
}
```
```css
.button {
--btn-bg: var(--clr-primary);
--btn-color: var(--clr-text-on-primary);
--btn-border: transparent;
--btn-hover-bg: var(--clr-primary-600);
--btn-active-transform: scale(0.97);
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--sp-1);
border-radius: var(--rd-md);
font-family: var(--ff-sans);
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
line-height: var(--lh-compact);
white-space: nowrap;
cursor: pointer;
background: var(--btn-bg);
color: var(--btn-color);
border: 1px solid var(--btn-border);
transition:
transform var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
padding: var(--sp-1) var(--sp-3);
height: 40px;
}
.button--sm { height: 32px; padding: var(--sp-0-5) var(--sp-2); }
.button--lg { height: 48px; padding: var(--sp-2) var(--sp-4); }
.button--icon { height: 40px; width: 40px; padding: 0; }
.button--secondary { --btn-bg: var(--clr-interactive-hover); --btn-color: var(--clr-text); }
.button--destructive { --btn-bg: var(--clr-ruby-500); --btn-color: white; }
.button--outline { --btn-bg: transparent; --btn-color: var(--clr-text); --btn-border: var(--clr-border); }
.button--ghost { --btn-bg: transparent; --btn-color: var(--clr-text); }
.button:hover { background: var(--btn-hover-bg); }
.button:active { transform: var(--btn-active-transform); }
.button:disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
.button:focus-visible { outline: 2px solid var(--clr-primary); outline-offset: 2px; }
```
### Badge
```tsx
interface BadgeProps {
variant: 'default' | 'secondary' | 'success' | 'warning' | 'destructive' | 'outline' | 'severity';
severity?: 'safe' | 'low' | 'medium' | 'high' | 'critical';
size?: 'sm' | 'default';
dot?: boolean;
children: ReactNode;
}
```
---
## ⛓️ Molecular Components (Molecules)
### Card
```tsx
interface CardProps {
variant?: 'default' | 'elevated' | 'glass' | 'interactive';
padding?: 'sm' | 'default' | 'lg' | 'none';
hover?: boolean;
as?: 'div' | 'button' | 'a';
onClick?: () => void;
children: ReactNode;
}
// Sub-components
Card.Header flex-col gap-1.5
Card.Title h3, font-semibold
Card.Description p, text-sm, text-muted
Card.Content main area with padding
Card.Footer flex items-center
```
### Modal / Dialog
```tsx
interface ModalProps {
open: boolean;
onClose: () => void;
title?: string;
description?: string;
size?: 'sm' | 'default' | 'lg' | 'full';
closeOnOverlay?: boolean;
children: ReactNode;
}
```
### Toast
```tsx
interface Toast {
id: string;
type: 'info' | 'success' | 'error' | 'warning';
title: string;
description?: string;
duration?: number; // Auto-dismiss ms, 0 = persistent
action?: { label: string; onClick: () => void; };
}
function useToast(): {
toast: (t: Omit<Toast, 'id'>) => string;
dismiss: (id: string) => void;
toasts: Toast[];
}
```
---
## 🧬 Organism Components
### MessageCard
```tsx
interface MessageCardProps {
message: {
id: string;
content: string;
author: { id: string; name: string; avatar: string; };
timestamp: number;
channel?: { id: string; name: string; };
attachments?: Attachment[];
aiAnalysis?: {
status: 'pending' | 'analyzing' | 'complete' | 'error';
severity?: 'safe' | 'low' | 'medium' | 'high' | 'critical';
categories?: string[];
summary?: string;
};
isEdited?: boolean;
isDeleted?: boolean;
};
onReanalyze?: (messageId: string) => Promise<void>;
}
```
### AudioVisualizer — Canvas-based
```tsx
interface AudioVisualizerProps {
frequencies: Uint8Array;
barCount?: number; // Default 48
gradient?: [string, string];
height?: number; // Default 32
mirrored?: boolean;
}
```
---
## 🎯 State Management per Komponen
Setiap komponen mengelola state visual:
```tsx
// 1. Normal
<div className="card">...</div>
// 2. Hover
<div className="card card--interactive">...</div>
// 3. Active/Focus
<div className="card card--interactive" aria-pressed="true">...</div>
// 4. Disabled
<div className="card opacity-50 pointer-events-none">...</div>
// ± Loading (data-dependent)
<div className="card"><Skeleton className="h-4 w-3/4" /></div>
// ± Error (data-dependent)
<div className="card border-destructive">
<p className="text-destructive">Failed to load</p>
</div>
// ± Empty (data-dependent)
<div className="card">
<EmptyState icon={MessageSquare} message="No messages" />
</div>
```
---
## ⚠️ Component Anti-Patterns
### ❌ Prop Drilling Berlebihan
```tsx
// ❌ JANGAN — props turun 4 level
<Dashboard user={user} messages={messages} settings={settings} />
// ✅ Gunakan context atau komposisi
<Dashboard>
<MessageFeed>
<MessageCard />
</MessageFeed>
</Dashboard>
```
### ❌ Komponen terlalu besar
```tsx
// ❌ JANGAN — 400+ baris
function LivePanel() { /* 400 lines */ }
// ✅ Bagi ke sub-komponen
function LivePanel() {
return (
<div>
<VoiceControls />
<ActiveSpeakers />
<NowPlaying />
<RecordingsList />
</div>
);
}
```
### ❌ Conditional terlalu kompleks
```tsx
// ❌ JANGAN — ternary bersarang
return isError ? <Error /> : isLoading ? <Loading /> : isEmpty ? <Empty /> : <Content />;
// ✅ State machine pattern
const state = getComponentState({ isLoading, isError, isEmpty, data });
return <ComponentStateMachine state={state} />;
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Atomic Design (Brad Frost)](https://atomicdesign.bradfrost.com/) | Atom-molecule-organism |
| [Radix UI](https://www.radix-ui.com/) | Headless UI primitives |
| [shadcn/ui](https://ui.shadcn.com/) | Component pattern reference |
---
*"Komponen adalah kristal ingatan — setiap bagian kecil menyatu membentuk keindahan yang utuh."* ❄️🩵