Live Preview
--- description: Hướng dẫn Agent tạo Table/Data Grid — hiển thị dữ liệu dạng bảng, sortable, selectable
Table / Data Grid Component
Skill này hướng dẫn tạo component Table — hiển thị dữ liệu có cấu trúc dạng hàng/cột. Enterprise component #1 được dùng nhiều nhất.
1. Mục tiêu (Objective)
Tạo component Table hỗ trợ sort, row selection, striped rows, sticky header, responsive card layout trên mobile. Table là core component cho mọi enterprise/admin dashboard.
2. AI Context & Intent (Ngữ cảnh cho AI)
Khi nào dùng Table?
- Hiển thị structured data: Danh sách users, orders, transactions, products
- So sánh nhiều items: Compare features, specifications
- Batch operations: Select + delete/export nhiều rows
- Data với nhiều attributes: Mỗi item có 4+ fields cần show cùng lúc
⚠️ Phân biệt Table vs List vs Card Grid (QUAN TRỌNG)
| Tiêu chí | Table | List Group | Card Grid |
|---|---|---|---|
| Bản chất | Structured data rows/columns | Grouped items vertical | Visual content grid |
| Columns | 3–10+ columns | 1 column (title + subtitle) | 1–4 per row |
| Sort | ✅ Multi-column sort | ❌ No sort | ❌ No sort |
| Selection | ✅ Checkbox per row | ✅ Single select | Optional |
| Mobile | ⚠️ Responsive card layout | ✅ Native feel | ✅ Stack vertically |
| Ví dụ | User management, orders | Settings, contacts | Photo gallery, products |
Decision Tree cho AI
text
Cần hiển thị data?
├─ Dữ liệu có cấu trúc rows/columns → Table
│ ├─ 3+ columns, sortable → Table (default)
│ ├─ Dense data, many rows → Table (compact) + Pagination
│ ├─ Alternating rows → Table (striped)
│ └─ Clean minimal → Table (borderless)
│
├─ Danh sách đơn giản (1-2 fields) → List Group
│ ├─ Settings-style → List Group (inset)
│ └─ Navigable items → List Group (link)
│
├─ Visual items (images, cards) → Card Grid
└─ Single data points → Stats / Metric cards
3. Anatomy
text
┌──────────────────────────────────────────────────┐
│ ☐ Name ▲ Email Role ... │ ← Header (sticky, --muted bg)
├──────────────────────────────────────────────────┤ ← --border
│ ☐ John Smith john@email.com Admin │ ← Row (--state-layer-hover on hover)
├──────────────────────────────────────────────────┤
│ ☐ Jane Doe jane@email.com Editor │ ← Row (striped: --muted bg alternate)
├──────────────────────────────────────────────────┤
│ ☐ Bob Wilson bob@email.com Viewer │
├──────────────────────────────────────────────────┤
│ ← Empty State component khi no data → │ ← Slot: empty state
└──────────────────────────────────────────────────┘
│ ◀ 1 2 3 ... 10 ▶ Showing 1-10 of 100 │ ← Pagination slot
4. Platform Mapping
| Platform | Component | Key Difference |
|---|---|---|
| Web | <table> / DataGrid |
Full-featured, sortable columns, pagination |
| iOS | UITableView / List | Single-column list, swipe actions |
| Android (M3) | LazyColumn / List | Material list items, no native table |
| Fluent | DataGrid / List | Full table with column resize |
| Carbon | DataTable | Sortable, expandable rows, batch actions |
Cross-platform note: iOS/Android không có native "table" — dùng List + custom cells. Token file cover cả table & list layout.
5. Variants
| Variant | Mô tả |
|---|---|
default |
Bordered, standard spacing |
striped |
Alternating row backgrounds |
borderless |
No cell borders, clean look |
compact |
Reduced padding for dense data |
5.5. Slot Map (Figma ↔ Code)
📎 Source:
slot-manifest.json→table· Layer: card
| Figma Slot | data-slot |
CSS Class | Required | Accepts |
|---|---|---|---|---|
| Root | table |
.card-table |
✅ | — |
| Header (card) | table-header |
.slot-header |
❌ | title-group + search + filter-actions |
| Head | table-head |
thead |
✅ | column-headers |
| Body | table-body |
.slot-body-flush |
✅ (n×) | table-row |
| Row | table-row |
tr |
✅ (n×) | table-cell |
| Footer | table-footer |
.slot-footer-between |
❌ | count-text + pagination |
6. Token Mapping
📦 Atomic Mapping: Xem
ATOMIC-MAPPING.md→ mục table — UI Layer: Card, Density Tier: comp.Table dùng
--mutedcho header bg,--bordercho cell borders,--state-layer-hovercho row hover. Component token JSON files đã deprecated.
7. Props & API
typescript
interface TableProps {
variant?: 'default' | 'striped' | 'borderless' | 'compact';
stickyHeader?: boolean;
selectable?: boolean;
sortable?: boolean;
onSort?: (column: string, direction: 'asc' | 'desc') => void;
onSelectRows?: (selectedIds: string[]) => void;
}
interface TableColumnProps {
key: string;
label: string;
sortable?: boolean;
width?: string | number;
align?: 'left' | 'center' | 'right';
}
8. Accessibility (a11y)
<table>:role="grid"for interactive tables,role="table"for static.<th>:scope="col",aria-sort="ascending" | "descending" | "none".- Row selection:
aria-selected="true", checkbox witharia-label. - Keyboard:
Arrow keysnavigate cells,Spacetoggles selection,Enteractivates sort.
9. Best Practices & Rules
- Responsive: Table → card layout trên
< 768px(mỗi row = 1 card, column headers = labels). - Sticky header: Header cố định khi scroll dọc.
- Empty state: Hiển thị EmptyState component khi không có data.
- Loading: Skeleton rows khi đang fetch data.
- Sort indicators: Chỉ hiện trên sortable columns, active sort highlighted.
- Icon: Sort icons (
arrow-up,arrow-down,arrow-up-down) CHỈ dùng Lucide icon từassets/icons/. CẤM dùng text emoji. Xemicon.mdmục 10.
10. Example Usage
jsx
{/* Basic table with sorting */}
<Table variant="default" sortable stickyHeader>
<TableHead>
<TableRow>
<TableHeader sortable>Name</TableHeader>
<TableHeader sortable>Email</TableHeader>
<TableHeader>Role</TableHeader>
<TableHeader sortable align="right">Created</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell><Badge variant="primary">{user.role}</Badge></TableCell>
<TableCell align="right">{user.createdAt}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Selectable striped table with pagination */}
<Table variant="striped" selectable onSelectRows={handleSelect}>
<TableHead>
<TableRow>
<TableHeader type="checkbox" />
<TableHeader>Order ID</TableHeader>
<TableHeader>Amount</TableHeader>
<TableHeader>Status</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{orders.map(order => (
<TableRow key={order.id} selected={selectedIds.includes(order.id)}>
<TableCell type="checkbox" />
<TableCell>{order.id}</TableCell>
<TableCell>{order.amount}</TableCell>
<TableCell><chip variant={order.statusVariant}>{order.status}</chip></TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Pagination currentPage={page} totalPages={10} onChange={setPage} />