feat(ui): implement comprehensive Table component system with compound API

This commit is contained in:
deytenit committed 2026-02-09 23:15:39 +03:00
1 parent df2abc06f2
commit 42a0b71a8f
27 files changed
+2016 -7

No files matched your search

@@ -0,0 +1,482 @@
# Rainby Table Components Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Implement a comprehensive, accessible, and composable set of Table components (Table, Thead, Tbody, Tr, Th, Td, Caption) and a specialized Cell layout primitive, adhering to the Rainby Design System.
**Architecture:**
A Compound Component pattern using React Context (`TableContext`) to propagate high-level style props (`density`, `variant`, `striped`) from the root `Table` to all child components. This ensures API cleanliness and visual consistency. The `Cell` component serves as a polymorphic layout helper for cell content.
**Tech Stack:** React, Tailwind CSS, TypeScript, `class-variance-authority` (or manual `cn` utility), `genericMemo` from internal STL.
---
### Task 1: Setup TableContext and Types
Define the context that will share styling configuration across the table sub-components.
**Files:**
- Create: `packages/ui/src/components/Table/TableContext.tsx`
- Create: `packages/ui/src/components/Table/types.ts`
**Step 1: Define shared types**
Create `packages/ui/src/components/Table/types.ts`.
```typescript
export type TableDensity = 's' | 'm' | 'l';
export type TableVariant = 'surface' | 'outline' | 'ghost';
export interface TableContextValue {
density: TableDensity;
variant: TableVariant;
striped: boolean;
hoverable: boolean;
}
```
**Step 2: Create Context**
Create `packages/ui/src/components/Table/TableContext.tsx`.
```typescript
import { createContext, useContext } from 'react';
import type { TableContextValue } from './types';
const TableContext = createContext<TableContextValue | null>(null);
export const useTableContext = () => {
const context = useContext(TableContext);
if (!context) {
throw new Error('Table sub-components must be used within a Table component.');
}
return context;
};
export const TableProvider = TableContext.Provider;
```
**Step 3: Commit**
```bash
git add packages/ui/src/components/Table/types.ts packages/ui/src/components/Table/TableContext.tsx
git commit -m "feat(ui): add TableContext and types"
```
---
### Task 2: Implement Table Container and Root Component
Implement `TableContainer` (wrapper) and `Table` (root element with Provider).
**Files:**
- Create: `packages/ui/src/components/Table/Table.tsx`
- Create: `packages/ui/src/components/Table/Table.test.tsx` (Basic render test)
**Step 1: Write minimal test**
Create `packages/ui/src/components/Table/Table.test.tsx`.
```typescript
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Table, TableContainer } from './Table';
describe('Table', () => {
it('renders correctly', () => {
render(
<TableContainer>
<Table density="m">
<tbody>
<tr>
<td>Test</td>
</tr>
</tbody>
</Table>
</TableContainer>
);
expect(screen.getByRole('table')).toBeInTheDocument();
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
```
**Step 2: Implement Components**
Create `packages/ui/src/components/Table/Table.tsx`.
```typescript
import React, { forwardRef } from 'react';
import { cn, castRef } from '@ermnvldmr/stl';
import { TableProvider } from './TableContext';
import type { TableContextValue } from './types';
// TableContainer
export interface TableContainerProps extends React.HTMLAttributes<HTMLDivElement> {}
export const TableContainer = forwardRef<HTMLDivElement, TableContainerProps>(
({ className, children, ...props }, ref) => (
<div
ref={ref}
className={cn('w-full overflow-auto rounded-md border border-border', className)}
{...props}
>
{children}
</div>
)
);
TableContainer.displayName = 'TableContainer';
// Table
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement>, Partial<TableContextValue> {
stickyHeader?: boolean;
}
export const Table = forwardRef<HTMLTableElement, TableProps>(
({
className,
children,
density = 'm',
variant = 'surface',
striped = false,
hoverable = false,
stickyHeader = false,
...props
}, ref) => {
const contextValue: TableContextValue = { density, variant, striped, hoverable };
return (
<TableProvider value={contextValue}>
<table
ref={ref}
className={cn(
'w-full caption-bottom text-sm',
variant === 'ghost' && 'border-none',
className
)}
{...props}
>
{children}
</table>
</TableProvider>
);
}
);
Table.displayName = 'Table';
```
**Step 3: Run Test**
`npm test packages/ui/src/components/Table/Table.test.tsx`
**Step 4: Commit**
```bash
git add packages/ui/src/components/Table/Table.tsx packages/ui/src/components/Table/Table.test.tsx
git commit -m "feat(ui): implement Table and TableContainer components"
```
---
### Task 3: Implement Semantic Table Sections (Header, Body, Footer, Caption)
Simple wrappers that forward refs and apply minimal base styles.
**Files:**
- Modify: `packages/ui/src/components/Table/Table.tsx` (Add exports)
- Modify: `packages/ui/src/components/Table/Table.test.tsx` (Update test)
**Step 1: Extend Test**
Add check for caption and section roles in `packages/ui/src/components/Table/Table.test.tsx`.
**Step 2: Add Components to Table.tsx**
Append to `packages/ui/src/components/Table/Table.tsx`:
```typescript
// TableHeader
export const TableHeader = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
)
);
TableHeader.displayName = 'TableHeader';
// TableBody
export const TableBody = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
)
);
TableBody.displayName = 'TableBody';
// TableFooter
export const TableFooter = forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot ref={ref} className={cn('bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
)
);
TableFooter.displayName = 'TableFooter';
// TableCaption
export const TableCaption = forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
)
);
TableCaption.displayName = 'TableCaption';
```
**Step 3: Run Test**
`npm test packages/ui/src/components/Table/Table.test.tsx`
**Step 4: Commit**
```bash
git add packages/ui/src/components/Table/Table.tsx packages/ui/src/components/Table/Table.test.tsx
git commit -m "feat(ui): add TableHeader, TableBody, TableFooter, TableCaption"
```
---
### Task 4: Implement TableRow, TableHead, TableCell
These components consume `TableContext` to apply correct padding and visual states.
**Files:**
- Modify: `packages/ui/src/components/Table/Table.tsx`
- Modify: `packages/ui/src/components/Table/Table.test.tsx`
**Step 1: Implement Row and Cells**
Append to `packages/ui/src/components/Table/Table.tsx`:
```typescript
import { useTableContext } from './TableContext';
// Helper for padding
const densityClasses = {
s: 'p-2',
m: 'p-4',
l: 'p-6',
};
// TableRow
export const TableRow = forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => {
const { hoverable, striped } = useTableContext();
return (
<tr
ref={ref}
className={cn(
'border-b transition-colors data-[state=selected]:bg-muted',
hoverable && 'hover:bg-muted/50',
striped && 'even:bg-muted/20', // Minimal stripe effect
className
)}
{...props}
/>
);
}
);
TableRow.displayName = 'TableRow';
// TableHead
export const TableHead = forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => {
const { density } = useTableContext();
return (
<th
ref={ref}
className={cn(
'h-12 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0',
densityClasses[density],
className
)}
{...props}
/>
);
}
);
TableHead.displayName = 'TableHead';
// TableCell
export const TableCell = forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => {
const { density } = useTableContext();
return (
<td
ref={ref}
className={cn(
'align-middle [&:has([role=checkbox])]:pr-0',
densityClasses[density],
className
)}
{...props}
/>
);
}
);
TableCell.displayName = 'TableCell';
```
**Step 2: Verify styles in Test**
Update `packages/ui/src/components/Table/Table.test.tsx` to check for density classes on cells.
**Step 3: Run Test**
`npm test packages/ui/src/components/Table/Table.test.tsx`
**Step 4: Commit**
```bash
git add packages/ui/src/components/Table/Table.tsx packages/ui/src/components/Table/Table.test.tsx
git commit -m "feat(ui): add TableRow, TableHead, TableCell with Context integration"
```
---
### Task 5: Implement Cell (Layout Helper)
The polymorphic layout primitive.
**Files:**
- Create: `packages/ui/src/components/Table/Cell.tsx`
- Create: `packages/ui/src/components/Table/Cell.test.tsx`
**Step 1: Write Test**
Create `packages/ui/src/components/Table/Cell.test.tsx`.
```typescript
import React from 'react';
import { render, screen } from '@testing-library/react';
import { Cell } from './Cell';
describe('Cell', () => {
it('renders correctly with default props', () => {
render(<Cell>Content</Cell>);
expect(screen.getByText('Content')).toHaveClass('flex', 'items-center', 'text-left');
});
it('applies numeric variant styles', () => {
render(<Cell variant="numeric">100</Cell>);
expect(screen.getByText('100')).toHaveClass('justify-end');
});
});
```
**Step 2: Implement Cell**
Create `packages/ui/src/components/Table/Cell.tsx`.
```typescript
import React, { forwardRef } from 'react';
import { cn } from '@ermnvldmr/stl';
export interface CellProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'text' | 'numeric' | 'icon' | 'checkbox';
layout?: 'flex' | 'stack';
align?: 'start' | 'center' | 'end' | 'between';
gap?: 's' | 'm' | 'l';
truncate?: boolean;
color?: 'default' | 'muted' | 'error' | 'success';
}
const gapClasses = {
s: 'gap-1.5',
m: 'gap-3',
l: 'gap-4',
};
const colorClasses = {
default: 'text-foreground',
muted: 'text-muted-foreground',
error: 'text-error',
success: 'text-success',
};
export const Cell = forwardRef<HTMLDivElement, CellProps>(
({
className,
children,
variant = 'text',
layout = 'flex',
align,
gap,
truncate = false,
color = 'default',
...props
}, ref) => {
// Determine default alignment based on variant if not explicitly provided
let defaultAlign = 'start';
if (variant === 'numeric') defaultAlign = 'end';
if (variant === 'icon' || variant === 'checkbox') defaultAlign = 'center';
const finalAlign = align || defaultAlign;
const alignClasses = {
start: layout === 'flex' ? 'justify-start' : 'items-start',
center: layout === 'flex' ? 'justify-center' : 'items-center',
end: layout === 'flex' ? 'justify-end' : 'items-end',
between: layout === 'flex' ? 'justify-between' : 'items-stretch', // Approximation for stack
};
return (
<div
ref={ref}
className={cn(
layout === 'flex' ? 'flex flex-row' : 'flex flex-col',
layout === 'flex' && 'items-center', // Vertically center flex items by default
alignClasses[finalAlign],
gap && gapClasses[gap],
color && colorClasses[color],
truncate && 'truncate',
variant === 'numeric' && 'tabular-nums',
variant === 'checkbox' && 'w-min mx-auto',
className
)}
{...props}
>
{children}
</div>
);
}
);
Cell.displayName = 'Cell';
```
**Step 3: Run Test**
`npm test packages/ui/src/components/Table/Cell.test.tsx`
**Step 4: Commit**
```bash
git add packages/ui/src/components/Table/Cell.tsx packages/ui/src/components/Table/Cell.test.tsx
git commit -m "feat(ui): add Cell layout primitive"
```
---
### Task 6: Export and Integration
Finalize package exports.
**Files:**
- Modify: `packages/ui/src/components/Table/index.ts`
- Modify: `packages/ui/src/components/index.ts`
**Step 1: Create Table/index.ts**
```typescript
export * from './Table';
export * from './Cell';
export * from './types';
```
**Step 2: Update Main Index**
Add `export * from './Table';` to `packages/ui/src/components/index.ts`.
**Step 3: Commit**
```bash
git add packages/ui/src/components/Table/index.ts packages/ui/src/components/index.ts
git commit -m "feat(ui): export Table components"
```
@@ -0,0 +1,242 @@
# Rainby Table Components Implementation Plan (Refined)
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Implement a comprehensive, modular, and highly controllable Table system adhering to Rainby design standards. This involves a compound component architecture with granular file separation, memoization, and exhaustive testing.
**Architecture:**
- **Compound Pattern:** `Table` as the main entry point with sub-components attached (e.g., `Table.Row`).
- **Context API:** `TableContext` for global defaults (`density`, `variant`, `striped`).
- **Granular Control:** Overrides at row and cell levels (e.g., `padding`, `noBorder`).
- **Standards:** `forwardRef` with named functions, `genericMemo` wrapping, and separate files for every sub-component.
**Tech Stack:** React 19, Tailwind CSS, TypeScript, `@ermnvldmr/stl` (for `cn`, `castRef`, `genericMemo`).
---
### Task 1: Foundation (Types & Context)
Setup the shared types and the React Context that will drive the table's global styling.
**Files:**
- Create: `packages/ui/src/components/Table/types.ts`
- Create: `packages/ui/src/components/Table/TableContext.tsx`
**Step 1: Define Shared Types**
Create `packages/ui/src/components/Table/types.ts`.
```typescript
export type TableDensity = 's' | 'm' | 'l';
export type TableVariant = 'surface' | 'outline' | 'ghost';
export type TablePadding = 'none' | TableDensity;
export interface TableContextValue {
density: TableDensity;
variant: TableVariant;
striped: boolean;
hoverable: boolean;
}
```
**Step 2: Create Context & Hook**
Create `packages/ui/src/components/Table/TableContext.tsx`.
```typescript
import { createContext, useContext } from 'react';
import type { TableContextValue } from './types';
const TableContext = createContext<TableContextValue | null>(null);
export function useTableContext() {
const context = useContext(TableContext);
if (!context) {
throw new Error('Table sub-components must be used within a Table component.');
}
return context;
}
export const TableProvider = TableContext.Provider;
```
**Step 3: Commit**
```bash
git add packages/ui/src/components/Table/types.ts packages/ui/src/components/Table/TableContext.tsx
git commit -m "feat(ui): add Table types and context"
```
---
### Task 2: Implement TableRow (Granular Control)
Implement the row component with specific overrides for borders and hover states.
**Files:**
- Create: `packages/ui/src/components/Table/TableRow/TableRow.tsx`
- Create: `packages/ui/src/components/Table/TableRow/TableRow.test.tsx`
**Step 1: Implement TableRow**
Create `packages/ui/src/components/Table/TableRow/TableRow.tsx`.
```typescript
import { cn, castRef, genericMemo } from '@ermnvldmr/stl';
import React, { forwardRef } from 'react';
import { useTableContext } from '../TableContext';
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
noBorder?: boolean;
hoverable?: boolean;
selected?: boolean;
}
const TableRowComponent = forwardRef<HTMLTableRowElement, TableRowProps>(
function TableRow({ className, children, noBorder, hoverable, selected, ...props }, ref) {
const context = useTableContext();
const isHoverable = hoverable ?? context.hoverable;
return (
<tr
{...props}
ref={castRef<HTMLTableRowElement>(ref)}
className={cn(
'border-b transition-colors',
!noBorder && 'border-border',
noBorder && 'border-b-0',
isHoverable && 'hover:bg-muted/50',
context.striped && 'even:bg-muted/20',
selected && 'bg-muted',
className
)}
>
{children}
</tr>
);
}
);
export const TableRow = genericMemo(TableRowComponent);
```
**Step 2: Write TableRow Test**
Create `packages/ui/src/components/Table/TableRow/TableRow.test.tsx`.
**Step 3: Commit**
```bash
git add packages/ui/src/components/Table/TableRow/
git commit -m "feat(ui): implement TableRow with granular controls"
```
---
### Task 3: Implement TableCell & TableHead (Padding Overrides)
Implement the data and header cells with support for padding overrides.
**Files:**
- Create: `packages/ui/src/components/Table/TableCell/TableCell.tsx`
- Create: `packages/ui/src/components/Table/TableHead/TableHead.tsx`
**Step 1: Implement TableCell**
Create `packages/ui/src/components/Table/TableCell/TableCell.tsx`.
```typescript
import { cn, castRef, genericMemo } from '@ermnvldmr/stl';
import React, { forwardRef } from 'react';
import { useTableContext } from '../TableContext';
import type { TablePadding } from '../types';
export interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
padding?: TablePadding;
align?: 'left' | 'center' | 'right';
}
const paddingClasses: Record<TablePadding, string> = {
none: 'p-0',
s: 'p-2',
m: 'p-4',
l: 'p-6',
};
const TableCellComponent = forwardRef<HTMLTableCellElement, TableCellProps>(
function TableCell({ className, children, padding, align, ...props }, ref) {
const { density } = useTableContext();
const finalPadding = padding || density;
return (
<td
{...props}
ref={castRef<HTMLTableCellElement>(ref)}
className={cn(
'align-middle',
paddingClasses[finalPadding],
align === 'left' && 'text-left',
align === 'center' && 'text-center',
align === 'right' && 'text-right',
className
)}
>
{children}
</td>
);
}
);
export const TableCell = genericMemo(TableCellComponent);
```
**Step 2: Implement TableHead** (Similar logic to TableCell but for `th`)
**Step 3: Commit**
```bash
git add packages/ui/src/components/Table/TableCell/ packages/ui/src/components/Table/TableHead/
git commit -m "feat(ui): implement TableCell and TableHead with padding overrides"
```
---
### Task 4: Implement Cell (Layout Primitive)
Implement the internal layout helper for cell content.
**Files:**
- Create: `packages/ui/src/components/Table/Cell/Cell.tsx`
**Step 1: Implement Cell**
Create `packages/ui/src/components/Table/Cell/Cell.tsx`.
Follow the Section 3 design (polymorphic variants for numeric, icon, etc.).
**Step 2: Commit**
```bash
git add packages/ui/src/components/Table/Cell/
git commit -m "feat(ui): implement Cell layout primitive"
```
---
### Task 5: Root Component & Assembly
Finalize the root `Table` component and assemble the compound API.
**Files:**
- Create: `packages/ui/src/components/Table/Table.tsx`
- Create: `packages/ui/src/components/Table/TableContainer.tsx`
- Modify: `packages/ui/src/components/Table/index.ts`
**Step 1: Assemble Table**
In `packages/ui/src/components/Table/Table.tsx`, use `Object.assign` to attach all sub-components.
**Step 2: Final Commit & Exports**
```bash
git add packages/ui/src/components/Table/
git commit -m "feat(ui): finalize Table compound component assembly"
```
+138
View File
@@ -0,0 +1,138 @@
# Table Component Design
**Date:** 2026-02-08
**Status:** Approved
**Topic:** Core Table Primitives for Rainby Design System
## 1. Overview
A comprehensive, accessible, and highly composable set of Table components. The goal is to provide a direct counterpart to every native HTML table element, styled according to the Rainby Design System, while offering a specialized `Cell` helper to manage common content layouts (alignment, icons, truncation) without enforcing complex logic.
## 2. Architecture
The solution uses a **Context-based** architecture. The root `Table` component accepts high-level style props (`density`, `variant`) and propagates them to all child components via React Context. This avoids prop-drilling and ensures visual consistency.
### Components
- **`TableContainer`**: Wrapper for responsive scrolling and outer boundary styling.
- **`Table`**: Root element. Manages context.
- **`TableHeader`** (`thead`), **`TableBody`** (`tbody`), **`TableFooter`** (`tfoot`): Semantic wrappers.
- **`TableRow`** (`tr`): Handles row-level states (hover, striping).
- **`TableHead`** (`th`): Header cells.
- **`TableCell`** (`td`): Data cells.
- **`TableCaption`** (`caption`): Accessible description.
- **`Cell`**: **(New)** A polymorphic layout primitive for content _inside_ cells.
## 3. API & Props
### 3.1 `Table` (Root)
| Prop | Type | Default | Description |
| :------------- | :---------------------------------- | :---------- | :-------------------------------------------- |
| `density` | `'s' \| 'm' \| 'l'` | `'m'` | Controls padding of all cells. |
| `variant` | `'surface' \| 'outline' \| 'ghost'` | `'surface'` | Controls borders and backgrounds. |
| `striped` | `boolean` | `false` | Enables zebra-striping on rows. |
| `hoverable` | `boolean` | `false` | Enables hover effects on rows. |
| `stickyHeader` | `boolean` | `false` | Fixes the header to the top of the container. |
### 3.2 `Cell` (Content Helper)
Used strictly for _layout and styling_ within a `TableCell` or `TableHead`. It does **not** handle sorting logic.
| Prop | Type | Default | Description |
| :--------- | :--------------------------------------------- | :---------- | :--------------------------------- |
| `variant` | `'text' \| 'numeric' \| 'icon' \| 'checkbox'` | `'text'` | Presets for alignment and spacing. |
| `layout` | `'flex' \| 'stack'` | `'flex'` | Internal flex direction. |
| `align` | `'start' \| 'center' \| 'end' \| 'between'` | _(auto)_ | Flex alignment overrides. |
| `truncate` | `boolean` | `false` | Forces single-line ellipsis. |
| `color` | `'default' \| 'muted' \| 'error' \| 'success'` | `'default'` | Text color token mapping. |
## 4. Styling Strategy
### Visual Tokens
Mappings to Rainby CSS variables:
- **Backgrounds**: `bg-surface` (default), `bg-muted/50` (headers/striped).
- **Borders**: `border-border` (outline), `border-b` only (ghost).
- **Text**: `text-foreground` (body), `text-muted-foreground` (headers/icons).
### Density Map
- **Small (`s`)**: `px-3 py-2 text-sm`
- **Medium (`m`)**: `px-4 py-3 text-sm`
- **Large (`l`)**: `px-6 py-4 text-base`
## 5. Usage Example
```tsx
import {
TableContainer,
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
Cell,
} from '@ermnvldmr/ui';
export const UserList = () => (
<TableContainer>
<Table density="m" variant="surface" striped hoverable>
<TableHeader>
<TableRow>
<TableHead>
<Cell variant="checkbox">
<Checkbox />
</Cell>
</TableHead>
<TableHead>
<Cell>User</Cell>
</TableHead>
<TableHead>
<Cell variant="numeric">Revenue</Cell>
</TableHead>
<TableHead>
<Cell variant="icon">Actions</Cell>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<Cell variant="checkbox">
<Checkbox />
</Cell>
</TableCell>
<TableCell>
{/* Complex Content Layout */}
<Cell layout="flex" gap="m">
<Avatar src="..." />
<Cell layout="stack" gap="s">
<span className="font-medium">Jane Doe</span>
<span className="text-muted-foreground text-xs">Admin</span>
</Cell>
</Cell>
</TableCell>
<TableCell>
<Cell variant="numeric">$1,200.00</Cell>
</TableCell>
<TableCell>
<Cell variant="icon">
<Button variant="ghost" size="s" icon="more-horizontal" />
</Cell>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
);
```
## 6. Implementation Notes
- **Accessibility**: Ensure `th` elements have `scope="col"` or `scope="row"` automatically where possible, or allow override. `TableCaption` is critical for screen readers.
- **Polymorphism**: `TableContainer` should probably just be a `div`, but `Table` components must forward refs to underlying DOM nodes.
- **Sorting**: Sorting UI is manual. Users will wrap `Cell` content in a button or place a sort icon next to it manually.