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
+151
View File
@@ -0,0 +1,151 @@
# Interaction Patterns — The Language of Touch
> *"Every interaction is a conversation between the user and the system."*
> — Don Norman
---
## 🎯 Filosofi Interaksi
Interaksi di BETE adalah **dialog** yang:
1. **Predictable** — Pengguna tahu yang akan terjadi
2. **Forgiving** — Kesalahan mudah diperbaiki (undo, confirm)
3. **Feedback-rich** — Setiap aksi mendapat respons visual
4. **Efficient** — Pengguna mahir bisa bergerak cepat (keyboard)
---
## 🔄 Interaction Feedback Matrix
| Elemen | Hover | Click | Focus | Disabled |
|--------|-------|-------|-------|----------|
| Button | scale(1.02) + bg shift | scale(0.97) | ring-2 | opacity-50 |
| Card | translateY(-2px) + shadow | — | ring-2 | opacity-50 |
| Link | underline + opacity 0.8 | color shift | ring-2 | opacity-40 |
| Input | border highlight | — | ring + border color | opacity-50 |
| Toggle | cursor pointer | slide + color | ring-2 | opacity-50 |
### Timing Reference
| Interaksi | Durasi | Easing |
|-----------|--------|--------|
| Hover in | 150ms | ease-out |
| Hover out | 200ms | ease-out |
| Click press | 100ms | ease-out |
| Click release | 150ms | ease-out |
| Focus ring | 200ms | ease-out |
| Tooltip show (after 300ms) | 200ms | ease-out |
| Tooltip hide | 150ms | ease-out |
---
## 🎪 Interaction Pattern Catalog
### Pattern 1: Progressive Disclosure
Informasi kompleks diungkap bertahap:
```tsx
<CollapsibleSection title="Advanced Filters" defaultOpen={false}>
<FilterGroup label="Severity">
<Checkbox label="Safe" />
<Checkbox label="Low" />
<Checkbox label="High" />
</FilterGroup>
</CollapsibleSection>
```
**Rules:** Chevron rotate 180° saat open. Jangan nested > 2 level.
### Pattern 2: Optimistic UI
Untuk aksi yang hampir pasti berhasil:
```tsx
async function handleDelete(messageId: string) {
// 1. Update UI optimistis
setMessages(prev => prev.filter(m => m.id !== messageId));
addToast({
type: 'info', title: 'Message deleted',
action: { label: 'Undo', onClick: handleUndo }
});
try {
await api.deleteMessage(messageId);
} catch {
// Rollback
setMessages(prev => [...prev, deletedMessage]);
addToast({ type: 'error', title: 'Failed to delete' });
}
}
```
### Pattern 3: Infinite Scroll vs Pagination
| Context | Pattern | Rationale |
|---------|---------|-----------|
| Message feed | Infinite scroll | Real-time, chronological |
| User list | Pagination | Bisa dicari, difilter |
| Recordings | Infinite scroll | Timeline-based |
| Analytics | Pagination | Butuh konteks halaman |
### Pattern 4: Keyboard Shortcuts
```tsx
const SHORTCUTS = {
'ctrl+k': 'Open command palette',
'ctrl+1': 'Switch to Live tab',
'ctrl+2': 'Switch to Messages tab',
'ctrl+3': 'Switch to Settings tab',
'escape': 'Close modal/panel',
'?': 'Show keyboard shortcuts',
};
```
---
## 🔔 Notification Priority System
| Priority | Style | Duration | Stack |
|----------|-------|----------|-------|
| info | Blue border | 4s auto | Queue |
| success | Green border | 4s auto | Queue |
| warning | Amber border | Persistent | Stack |
| error | Red border | Persistent | Stack + glow |
---
## 🖱️ Cursor Mapping
```css
.clickable { cursor: pointer; }
.draggable { cursor: grab; }
.dragging { cursor: grabbing; }
.disabled { cursor: not-allowed; }
.text-select { cursor: text; }
.launch { cursor: pointer; }
.copy { cursor: copy; }
```
---
## ♿ Interaction Accessibility
1. Semua interaktif reachable via Tab
2. Focus order = visual order (DOM order)
3. Hover-only → ada keyboard alternative
4. Touch targets min 44x44px (WCAG 2.5.5)
5. Undo untuk destructive actions
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Material Interaction](https://m3.material.io/foundations/interaction) | Google patterns |
| [NN Group](https://www.nngroup.com/) | UX research |
| [Inclusive Components](https://inclusive-components.design/) | Accessible patterns |
---
*"Setiap sentuhan adalah dialog — interaksi adalah bahasa yang tak terucapkan."* ❄️🩵
+171
View File
@@ -0,0 +1,171 @@
# Data Visualization — Painting with Numbers
> *"The greatest value of a picture is when it forces us to notice what we never expected to see."*
> — John Tukey
---
## 🎯 Filosofi Data Visual
Data visualisasi di BETE adalah **cerita** tentang data yang:
1. **Jujur** — Tidak memanipulasi sumbu atau skala
2. **Kontekstual** — Setiap angka punya pembanding
3. **Hierarkis** — Overview dulu, detail kemudian
---
## 🎨 Chart Color Palette
```css
:root {
/* Sequential (single hue) */
--chart-blue-1: oklch(0.85 0.060 255);
--chart-blue-2: oklch(0.70 0.100 255);
--chart-blue-3: oklch(0.55 0.150 255);
--chart-blue-4: oklch(0.40 0.150 255);
--chart-blue-5: oklch(0.30 0.120 255);
/* Categorical */
--chart-cat-1: oklch(0.55 0.175 255); /* Blue */
--chart-cat-2: oklch(0.60 0.130 145); /* Green */
--chart-cat-3: oklch(0.65 0.150 50); /* Orange */
--chart-cat-4: oklch(0.55 0.165 25); /* Red */
--chart-cat-5: oklch(0.50 0.100 285); /* Purple */
--chart-cat-6: oklch(0.65 0.120 200); /* Cyan */
--chart-cat-7: oklch(0.60 0.110 350); /* Pink */
--chart-cat-8: oklch(0.70 0.100 85); /* Yellow */
}
```
---
## 📊 Chart Types & Usage
### 1. Stat Card (KPI)
```tsx
interface StatCardProps {
label: string;
value: number | string;
trend?: { direction: 'up' | 'down' | 'flat'; percentage: number; period: string; };
icon: ReactNode;
color?: 'primary' | 'success' | 'warning' | 'destructive';
}
```
**Layout:**
```
┌─────────────────────┐
│ [icon] Label │
│ 1,234 ▲ 12.3% │
│ vs last wk │
└─────────────────────┘
```
### 2. Line Chart (Trend)
**Use:** Message volume per day, moderation per hour
**Rules:** Y-axis dari 0. Gradient subtle below line. Hover tooltip.
### 3. Bar Chart (Comparison)
**Use:** Top channels, severity distribution
**Rules:** Horizontal untuk >5 kategori. Max 20 bars.
### 4. Donut Chart (Composition)
**Use:** Message type, severity breakdown
**Rules:** Max 6 segmen. <3% collaps ke "Other". Center = total.
### 5. Heatmap Calendar (Activity)
**Use:** User activity by day/hour
**Rules:** Sumbu X = hari, Y = jam. Satu warna accent.
---
## 📐 Chart Styling Tokens
```css
.chart-container {
--chart-padding: var(--sp-4);
--chart-label-size: var(--fs-xs);
--chart-tick-count: 5;
--chart-grid-opacity: 0.1;
--chart-line-width: 2px;
}
.chart-tooltip {
background: var(--clr-surface-overlay);
backdrop-filter: blur(8px);
border: 1px solid var(--clr-border);
border-radius: var(--rd-md);
padding: var(--sp-2) var(--sp-3);
font-size: var(--fs-sm);
box-shadow: var(--sh-elevated);
}
```
---
## 🔍 Drill-down Pattern
```tsx
function MessageTrendChart() {
const [granularity, setGranularity] = useState<'daily' | 'hourly' | '15min'>('daily');
const handlePointClick = (date: Date) => {
if (granularity === 'daily') setGranularity('hourly');
else if (granularity === 'hourly') setGranularity('15min');
};
return (
<ChartCard title="Message Volume"
onBack={granularity !== 'daily' ? () => setGranularity('daily') : undefined}>
<LineChart data={data} granularity={granularity} onClick={handlePointClick} />
</ChartCard>
);
}
```
---
## ⚠️ Anti-Patterns
### ❌ Truncated Y-axis
```tsx
// ❌ Y axis mulai dari 50, memperbesar perbedaan
const options = { yAxis: { min: 50 } };
// ✅ Mulai dari 0
const options = { yAxis: { min: 0 } };
```
### ❌ Terlalu banyak warna
```tsx
// ❌ JANGAN — setiap bar beda warna
<Bar data={data} fill={['#ff0000', '#00ff00', '#0000ff', ...]} />
// ✅ Sequential scale
<Bar data={data} colorScale="sequential" />
```
### ❌ 3D charts — mendistorsi persepsi
```tsx
// ❌ JANGAN
<PieChart><Pie data={data} style={{ filter: 'drop-shadow(...)' }} /></PieChart>
// ✅ 2D
<PieChart><Pie data={data} /></PieChart>
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Recharts](https://recharts.org/) | React chart library |
| [Chartability](https://chartability.github.io/) | Accessible charts |
| [Tufte](https://www.edwardtufte.com/tufte/) | Minimalist chart design |
---
*"Angka adalah ingatan yang terukur — setiap titik data adalah kisah yang menanti."* ❄️🩵
+110
View File
@@ -0,0 +1,110 @@
# Moderation UI Patterns — The Watchful Eye
> *"With great power comes great responsibility."*
> — Adapted for content moderation interfaces.
---
## 🎯 Filosofi UI Moderasi
1. **At-a-glance severity** — Warna & label yang langsung terbaca
2. **Context-rich** — Setiap keputusan disertai konteks
3. **Non-destructive by default** — Flag dulu, action kemudian
4. **Audit trail** — Setiap aksi tercatat
---
## 🏷️ Severity Scale
```css
.severity--safe { background: oklch(0.60 0.130 145 / 0.15); color: oklch(0.60 0.130 145); }
.severity--low { background: oklch(0.70 0.120 75 / 0.15); color: oklch(0.70 0.120 75); }
.severity--medium { background: oklch(0.65 0.150 50 / 0.15); color: oklch(0.65 0.150 50); }
.severity--high { background: oklch(0.60 0.150 30 / 0.15); color: oklch(0.60 0.150 30); }
.severity--critical { background: oklch(0.55 0.165 25 / 0.15); color: oklch(0.55 0.165 25); }
```
| Severity | Warna | Ikon | Action |
|----------|-------|------|--------|
| Safe | Emerald | ✅ Shield | None |
| Low | Amber | ⚠️ | Review |
| Medium | Orange | 🔶 | Alert + review |
| High | Red-Orange | 🚫 | Notify + action |
| Critical | Ruby | 🔴 | Immediate |
---
## 📋 Moderation Queue
```tsx
interface ModerationQueueItem {
id: string;
message: { preview: string; author: { name: string; }; timestamp: number; channel: string; };
analysis: { severity: Severity; categories: string[]; confidence: number; summary: string; };
status: 'pending' | 'reviewed' | 'actioned' | 'dismissed';
}
```
**Layout per item:**
```
┌──────────────────────────────────────────────────────┐
│ 🔴 CRITICAL │ [User]: "message preview..." │
│ 🏷️ toxicity, │ in #general · 2m ago │
│ harassment │ [Review] [Dismiss] [Action] │
└──────────────────────────────────────────────────────┘
```
### Filter Bar
```
Severity: [All] [Safe] [Low] [Medium] [High] [Critical]
Channel: [#general ▼]
Date: [Last 24h ▼]
Search: [.................. 🔍]
```
---
## 🎯 Action Confirmation
| Action | Confirm | Duration | Undo |
|--------|---------|----------|------|
| Dismiss | No | 2s toast | Yes (5s) |
| Warn | No | 3s toast | No |
| Delete | Yes (modal) | 4s toast | No |
| Ban | Yes (modal + reason) | — | Manual |
---
## 📊 Moderation Metrics
| Metric | Format | Frequency |
|--------|--------|-----------|
| Messages analyzed | Number | Real-time |
| Flag rate | % | Hourly |
| Response time | ms avg | Real-time |
| False positive rate | % | Daily |
| Queue depth | Number | Real-time |
---
## ⚠️ Anti-Patterns
### ❌ Ambiguous severity
```tsx
// ❌ Warna tanpa label
<div className="bg-red-200">...</div>
// ✅ Color + icon + text
<SeverityBadge severity="critical" />
```
### ❌ One-click destructive
```tsx
// ❌ Delete tanpa konfirmasi
<Button onClick={handleDelete}>Delete</Button>
// ✅ Confirm dialog
<ConfirmDialog variant="destructive" ... />
```
---
*"Mata waspada adalah penjaga ingatan — setiap flag adalah catatan sejarah."* ❄️🩵
+307
View File
@@ -0,0 +1,307 @@
# State Machines — The Flow of Data
> *"All happy families are alike; each unhappy family is unhappy in its own way."*
> — Tolstoy, adapted for component states.
---
## 🎯 Filosofi State Machine
Setiap komponen data-driven di BETE memiliki **4 state fundamental**:
```
IDLE → LOADING → SUCCESS
↘ ERROR
↘ EMPTY (conditional, jika data.length === 0)
```
State machine memastikan **tidak ada kondisi yang terlewat** — setiap kemungkinan state visual memiliki representasi.
---
## 🎮 The Quad-State Pattern
```tsx
type DataState<T> =
| { status: 'idle' }
| { status: 'loading'; progress?: number }
| { status: 'success'; data: T; timestamp: number }
| { status: 'error'; error: Error; retryCount?: number }
| { status: 'empty'; message?: string };
```
### Generic State Machine Hook
```tsx
// shared/hooks/useDataState.ts
function useDataState<T>(
fetcher: () => Promise<T>,
options?: {
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
retry?: number;
cacheKey?: string;
}
): {
state: DataState<T>;
execute: () => Promise<void>;
reset: () => void;
retry: () => Promise<void>;
setData: (data: T) => void;
} {
const [state, setState] = useState<DataState<T>>({ status: 'idle' });
const execute = useCallback(async () => {
setState({ status: 'loading' });
try {
const data = await fetcher();
if (Array.isArray(data) && data.length === 0) {
setState({ status: 'empty', message: 'No data available' });
} else {
setState({ status: 'success', data, timestamp: Date.now() });
options?.onSuccess?.(data);
}
} catch (error) {
setState({ status: 'error', error: error as Error });
options?.onError?.(error as Error);
}
}, [fetcher]);
return { state, execute, reset, retry: execute, setData };
}
```
### Component Rendering
```tsx
function DataPanel() {
const { state, execute, retry } = useDataState(fetchMessages);
useEffect(() => { execute(); }, []);
switch (state.status) {
case 'idle':
case 'loading':
return <LoadingSkeleton />;
case 'error':
return (
<ErrorState
message={state.error.message}
onRetry={retry}
retryCount={state.retryCount}
/>
);
case 'empty':
return <EmptyState message={state.message ?? 'Nothing here'} />;
case 'success':
return <DataView data={state.data} />;
}
}
```
---
## 🖼️ Visual Representations
### Loading State
```tsx
interface LoadingSkeletonProps {
variant?: 'card' | 'list' | 'detail' | 'table' | 'chart';
count?: number; // Jumlah skeleton items
}
/* Contoh variant 'card' */
function CardSkeleton() {
return (
<div className="card animate-shimmer" aria-busy="true" aria-label="Loading...">
<Skeleton className="h-4 w-3/4 mb-3" />
<Skeleton className="h-3 w-1/2 mb-2" />
<Skeleton className="h-3 w-full" />
</div>
);
}
```
### Error State
```tsx
interface ErrorStateProps {
error: Error;
onRetry: () => void;
retryCount?: number;
variant?: 'inline' | 'full-page' | 'toast';
}
function ErrorState({ error, onRetry, retryCount }: ErrorStateProps) {
const isRetryExhausted = (retryCount ?? 0) >= 3;
return (
<div className="flex flex-col items-center gap-4 py-12" role="alert">
<div className="rounded-full bg-destructive/10 p-3">
<AlertTriangle className="h-6 w-6 text-destructive" />
</div>
<p className="text-sm font-medium text-foreground">Something went wrong</p>
<p className="text-xs text-muted-foreground">{error.message}</p>
{!isRetryExhausted ? (
<Button variant="outline" size="sm" onClick={onRetry}>
Try Again
</Button>
) : (
<p className="text-xs text-muted-foreground">
Still failing after multiple attempts. Please try again later.
</p>
)}
</div>
);
}
```
### Empty State
```tsx
interface EmptyStateProps {
icon?: ReactNode;
title: string;
description?: string;
action?: { label: string; onClick: () => void };
variant?: 'mascot' | 'icon' | 'minimal';
}
const EMPTY_STATES = {
messages: { icon: MessageSquare, title: 'No messages yet', description: 'Messages will appear here once they are captured.' },
speakers: { icon: Mic, title: 'No active speakers', description: 'Quiet in here...' },
recordings: { icon: Radio, title: 'No recordings', description: 'Join a voice channel to start recording.' },
analytics: { icon: BarChart3, title: 'Not enough data', description: 'Analytics will populate as data accumulates.' },
users: { icon: Users, title: 'No users found', description: 'Try adjusting your filters.' },
};
```
---
## ♻️ State Transition Diagram
```
┌──────────┐
│ IDLE │
└────┬─────┘
│ execute()
┌──────────┐
│ LOADING │◄────── retry()
└────┬─────┘
┌───────┴───────────┐
│ │
↓ ↓
┌──────────┐ ┌──────────┐
│ SUCCESS │ │ ERROR │
│ data: T │ │ err: E │
└────┬─────┘ └────┬─────┘
│ │
│ (data.length │ retry()
│ === 0) │
↓ │
┌──────────┐ │
│ EMPTY │ │
│ msg: str │ │
└──────────┘ │
│ │
└──────┬───────────┘
│ reset()
┌──────────┐
│ IDLE │
└──────────┘
```
---
## 🔄 Retry Strategy
```typescript
const RETRY_CONFIG = {
maxAttempts: 3,
baseDelay: 1000, // 1s
maxDelay: 10000, // 10s
backoff: 'exponential' as const,
onRetry: (attempt: number, error: Error) => {
logger.warn(`Retry attempt ${attempt}`, { error: error.message });
},
};
```
### Exponential Backoff
```typescript
function calculateDelay(attempt: number): number {
return Math.min(
1000 * Math.pow(2, attempt - 1), // 1s, 2s, 4s
10000 // cap at 10s
);
}
```
---
## 📦 Component State Map
| Component | Loading | Error | Empty | Success |
|-----------|---------|-------|-------|---------|
| MessageFeed | Card skeletons | ErrorState + retry | Mascot "No messages" | Message list |
| VoiceCards | Card skeletons | ErrorState | "No connected channels" | VoiceCard list |
| ActiveSpeakers | Dot skeletons | Silent fallback | "No speakers" | Speaker list |
| Analytics | Skeleton grid | ErrorState | "Not enough data" | Charts |
| Recordings | List skeletons | ErrorState | "No recordings" | Recording list |
| UserList | List skeletons | ErrorState + retry | "No users found" | User list |
| DashboardStats | Stat skeletons | ErrorState | "No data available" | Stat grid |
---
## ⚠️ Anti-Patterns State
### ❌ Missing state handling
```tsx
// ❌ JANGAN — hanya handle SUCCESS
function Panel() {
const { data, isLoading } = useQuery(...);
if (isLoading) return <Spinner />;
return <DataView data={data} />; // ERROR? EMPTY?
}
```
### ❌ Loading state after error
```tsx
// ❌ JANGAN — loading infinite loop setelah error
function Panel() {
const { data, isLoading } = useQuery(..., { retry: true });
if (isLoading) return <Spinner />;
// ERROR: retry=true + error = loading terus
}
```
### ❌ Empty state default terlalu generic
```tsx
// ❌ JANGAN — tidak helpful
<div>No data</div>
// ✅ Kontekstual dengan action
<EmptyState icon={MessageSquare} title="No messages" action={{ label: "Refresh", onClick: refetch }} />
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [State Reducer Pattern](https://kentcdodds.com/blog/state-reducer-pattern) | Advanced state management |
| [XState](https://stately.ai/docs/xstate) | Visual state machines |
| [React useReducer](https://react.dev/reference/react/useReducer) | Built-in state management |
---
*"Setiap state adalah babak dalam cerita data — dari sunyi hingga berbicara."* ❄️🩵
+288
View File
@@ -0,0 +1,288 @@
# Responsive System — Shapeshifting Glass
> *"Content is like water — it should flow into whatever container it's poured into."*
> — Ethan Marcotte
---
## 🎯 Filosofi Responsif
BETE menggunakan pendekatan **mobile-first** dengan tiga prinsip:
1. **Content parity** — Konten yang sama di semua ukuran, layout yang berbeda
2. **Touch-optimized** — Target 44×44px minimum di mobile
3. **Progressive enhancement** — Desktop mendapat fitur tambahan (hover, sidebar, multi-column)
---
## 📐 Breakpoint System
```css
:root {
--bp-sm: 640px; /* Mobile landscape */
--bp-md: 768px; /* Tablet portrait */
--bp-lg: 1024px; /* Tablet landscape / small desktop */
--bp-xl: 1280px; /* Desktop */
--bp-2xl: 1536px; /* Wide desktop */
}
```
### Layout Behavior Matrix
| Viewport | Sidebar | Header | Content Grid | Font Size |
|----------|---------|--------|-------------|-----------|
| < 640px | Bottom tab (56px) | Compact | 1 col | sm |
| 640-768 | Bottom tab | Compact | 1-2 col | sm |
| 768-1024 | Icon 64px | Standard | 2 col | base |
| 1024-1280 | Full 256px | Standard | 2-3 col | base |
| > 1280px | Full 256px | Full | 3-4 col | base+ |
---
## 📱 Mobile Adaptations
### Navigation
- **< 768px:** Bottom tab bar menggantikan sidebar
- **Tab icons:** Home, Live, Messages, Settings (maks 5 tabs)
- **Tab bar height:** 56px (dengan safe area padding untuk notched phones)
### Content
- **Cards:** Full-width (margin 16px), stacked vertical
- **Tables:** Horizontal scroll atau card view alternatif
- **Charts:** Simplified (less data points, larger labels)
- **Modals:** Full-screen drawer dari bawah (bottom sheet)
### Touch Targets
```css
/* Minimum 44×44px untuk semua interactive elements */
.button, .nav-item, .tab-item {
min-height: 44px;
min-width: 44px;
}
/* Forms on mobile */
.input, .select {
height: 48px; /* Larger tap target */
font-size: 16px; /* Prevent iOS zoom on focus */
}
```
---
## 💻 Desktop Adaptations
### Navigation
- **≥ 1024px:** Full sidebar (256px) dengan label teks
- **Sidebar states:** Collapsed (icon-only, 64px) ↔ Expanded (256px)
- **Keyboard shortcuts:** Didokumentasikan di help panel
### Content
- **Multi-column grids:** 2-4 columns depending on container width
- **Sticky elements:** Sidebar, header, filter bars
- **Hover previews:** Tooltips, popovers untuk informasi tambahan
- **Drag & drop:** Dukungan untuk reorder, upload area
---
## 🧩 Responsive Component Patterns
### Pattern 1: Responsive Card Grid
```css
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--sp-3);
}
@media (min-width: 640px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1280px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}
```
### Pattern 2: Responsive Sidebar + Content
```tsx
function DashboardLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isMobile = useMediaQuery('(max-width: 767px)');
return (
<div className="page-layout">
{/* Mobile: slide-in drawer */}
{isMobile && (
<MobileTabBar activeTab={activeTab} onTabChange={setActiveTab} />
)}
{/* Desktop: persistent sidebar */}
{!isMobile && (
<Sidebar collapsed={sidebarCollapsed} onToggle={() => setSidebarCollapsed(!sidebarCollapsed)} />
)}
<main className="main-area">
<Header onMenuClick={() => setMobileMenuOpen(true)} />
<div className="content-area">
{children}
</div>
</main>
</div>
);
}
```
### Pattern 3: Responsive Typography (Fluid)
```css
/* Fluid type scale — sudah didefinisikan di core/02-typography.md */
--fs-body: clamp(0.94rem, 0.94rem + 0.03vw, 1.00rem);
--fs-h2: clamp(1.50rem, 1.50rem + 0.12vw, 1.88rem);
```
### Pattern 4: Container Queries (for reusable components)
```css
.card-grid-component {
container-type: inline-size;
container-name: card-list;
}
@container card-list (max-width: 400px) {
.card-item { grid-template-columns: 1fr; }
}
@container card-list (min-width: 401px) {
.card-item { grid-template-columns: 1fr 1fr; }
}
```
---
## 🎯 Responsive Decision Tree
```
Layout component →
├── Apakah ini navigasi?
│ ├── Mobile → Bottom tab bar (56px)
│ ├── Tablet → Icon sidebar (64px) + hamburger
│ └── Desktop → Full sidebar (256px)
├── Apakah ini konten list/grid?
│ ├── 1 item → Single column
│ ├── 2-4 items → 2 col (tablet), 3-4 col (desktop)
│ └── > 4 items → auto-fill grid with minmax
├── Apakah ini modal/dialog?
│ ├── Mobile → Bottom sheet (full width, 80% height)
│ └── Desktop → Centered modal (max-w-lg)
└── Apakah ini form?
├── Mobile → Stacked, full-width, larger inputs
└── Desktop → Multi-column, side labels
```
---
## 📏 Responsive Spacing Scale
```css
.content-padding {
padding: var(--sp-3); /* Mobile: 16px */
}
@media (min-width: 768px) {
.content-padding { padding: var(--sp-4); } /* Tablet: 24px */
}
@media (min-width: 1024px) {
.content-padding { padding: var(--sp-5); } /* Desktop: 32px */
}
```
---
## 🧪 Testing Responsive Design
```typescript
// Test utility untuk responsive behavior
const VIEWPORTS = {
mobile: { width: 375, height: 667 },
tablet: { width: 768, height: 1024 },
desktop: { width: 1280, height: 800 },
wide: { width: 1920, height: 1080 },
};
describe('DashboardLayout', () => {
it('shows MobileTabBar on mobile', () => {
cy.viewport(VIEWPORTS.mobile);
cy.get('[data-testid="mobile-tab-bar"]').should('be.visible');
cy.get('[data-testid="sidebar"]').should('not.be.visible');
});
it('shows sidebar on desktop', () => {
cy.viewport(VIEWPORTS.desktop);
cy.get('[data-testid="sidebar"]').should('be.visible');
cy.get('[data-testid="mobile-tab-bar"]').should('not.be.visible');
});
});
```
---
## ⚠️ Anti-Patterns Responsive
### ❌ Hanya media query untuk satu breakpoint
```css
/* ❌ JANGAN — hanya mobile dan desktop */
.panel { padding: 16px; }
@media (min-width: 1024px) { .panel { padding: 32px; } }
/* ✅ Gunakan fluid atau multiple breakpoints */
.panel { padding: clamp(16px, 3vw, 32px); }
```
### ❌ Hidden content on mobile
```tsx
// ❌ JANGAN — "out of sight, out of mind" tapi konten hilang
{isMobile ? null : <ExpensiveChart />}
// ✅ Simplified version untuk mobile
<Chart variant={isMobile ? 'compact' : 'full'} />
```
### ❌ Fixed width containers
```css
/* ❌ JANGAN — overflow on smaller screens */
.container { width: 1200px; }
/* ✅ Gunakan max-width + padding */
.container { max-width: 1200px; margin: 0 auto; padding: 0 var(--sp-4); }
```
---
## 🔗 Referensi
| Sumber | Konsep |
|--------|--------|
| [Every Layout](https://every-layout.dev/) | Reusable layout patterns |
| [Container Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_container_queries) | CSS container queries |
| [Utopia.fyi](https://utopia.fyi/) | Fluid type & space calculator |
---
*"Layout adalah air yang mengalir — ia mengambil bentuk wadahnya tanpa kehilangan esensi."* ❄️🩵