Prisma
Live Preview
Name Status Role Amount
John Doe Active Admin $2,500.00
Jane Smith Pending Editor $1,250.00
Bob Lee Inactive Viewer $0.00

--- 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.jsontable · 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 --muted cho header bg, --border cho cell borders, --state-layer-hover cho 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 with aria-label.
  • Keyboard: Arrow keys navigate cells, Space toggles selection, Enter activates 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. Xem icon.md mụ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} />