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
9.7 KiB
9.7 KiB
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:
- Satu tanggung jawab — Satu komponen, satu fungsi
- State-driven — Visual merepresentasikan state, bukan sebaliknya
- Composable — Bisa digabung seperti LEGO
- Theme-aware — Menggunakan CSS variables, bukan hardcoded values
- 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
interface ButtonProps {
variant: 'primary' | 'secondary' | 'destructive' | 'outline' | 'ghost';
size: 'sm' | 'default' | 'lg' | 'icon';
loading?: boolean;
disabled?: boolean;
icon?: ReactNode;
children?: ReactNode;
}
.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
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
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
interface ModalProps {
open: boolean;
onClose: () => void;
title?: string;
description?: string;
size?: 'sm' | 'default' | 'lg' | 'full';
closeOnOverlay?: boolean;
children: ReactNode;
}
Toast
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
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
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:
// 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
// ❌ JANGAN — props turun 4 level
<Dashboard user={user} messages={messages} settings={settings} />
// ✅ Gunakan context atau komposisi
<Dashboard>
<MessageFeed>
<MessageCard />
</MessageFeed>
</Dashboard>
❌ Komponen terlalu besar
// ❌ JANGAN — 400+ baris
function LivePanel() { /* 400 lines */ }
// ✅ Bagi ke sub-komponen
function LivePanel() {
return (
<div>
<VoiceControls />
<ActiveSpeakers />
<NowPlaying />
<RecordingsList />
</div>
);
}
❌ Conditional terlalu kompleks
// ❌ 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) | Atom-molecule-organism |
| Radix UI | Headless UI primitives |
| shadcn/ui | Component pattern reference |
"Komponen adalah kristal ingatan — setiap bagian kecil menyatu membentuk keindahan yang utuh." ❄️🩵