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
314 lines
9.7 KiB
Markdown
314 lines
9.7 KiB
Markdown
# 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."* ❄️🩵
|