From 42a0b71a8fece39afacbca8a4f4399a4a44f6967 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Mon, 9 Feb 2026 00:15:33 +0300 Subject: [PATCH] feat(ui): implement comprehensive Table component system with compound API --- AGENTS.md => GEMINI.md | 0 .../2026-02-08-rainby-table-implementation.md | 482 ++++++++++++++++++ ...26-02-08-rainby-table-v2-implementation.md | 242 +++++++++ docs/plans/2026-02-08-table-component.md | 138 +++++ .../ui/src/components/Code/Code.stories.tsx | 4 +- .../ui/src/components/Image/Image.stories.tsx | 6 +- .../ui/src/components/Table/Table.stories.tsx | 189 +++++++ .../ui/src/components/Table/Table.test.tsx | 117 +++++ packages/ui/src/components/Table/Table.tsx | 101 ++++ .../components/Table/TableBody/TableBody.tsx | 29 ++ .../Table/TableCaption/TableCaption.tsx | 29 ++ .../components/Table/TableCell/TableCell.tsx | 81 +++ .../Table/TableContent/TableContent.test.tsx | 21 + .../Table/TableContent/TableContent.tsx | 135 +++++ .../Table/TableFooter/TableFooter.tsx | 36 ++ .../components/Table/TableHead/TableHead.tsx | 76 +++ .../Table/TableHeader/TableHeader.tsx | 29 ++ .../Table/TableRow/TableRow.test.tsx | 33 ++ .../components/Table/TableRow/TableRow.tsx | 68 +++ packages/ui/src/components/Table/constants.ts | 25 + .../contexts/TableContext/TableContext.ts | 29 ++ .../TableRowContext/TableRowContext.ts | 24 + packages/ui/src/components/Table/index.ts | 15 + .../testing/TableContainer/TableContainer.tsx | 51 ++ packages/ui/src/components/Table/types.ts | 42 ++ packages/ui/src/components/index.ts | 13 + services/www/src/shared/constants.ts | 8 +- 27 files changed, 2016 insertions(+), 7 deletions(-) rename AGENTS.md => GEMINI.md (100%) create mode 100644 docs/plans/2026-02-08-rainby-table-implementation.md create mode 100644 docs/plans/2026-02-08-rainby-table-v2-implementation.md create mode 100644 docs/plans/2026-02-08-table-component.md create mode 100644 packages/ui/src/components/Table/Table.stories.tsx create mode 100644 packages/ui/src/components/Table/Table.test.tsx create mode 100644 packages/ui/src/components/Table/Table.tsx create mode 100644 packages/ui/src/components/Table/TableBody/TableBody.tsx create mode 100644 packages/ui/src/components/Table/TableCaption/TableCaption.tsx create mode 100644 packages/ui/src/components/Table/TableCell/TableCell.tsx create mode 100644 packages/ui/src/components/Table/TableContent/TableContent.test.tsx create mode 100644 packages/ui/src/components/Table/TableContent/TableContent.tsx create mode 100644 packages/ui/src/components/Table/TableFooter/TableFooter.tsx create mode 100644 packages/ui/src/components/Table/TableHead/TableHead.tsx create mode 100644 packages/ui/src/components/Table/TableHeader/TableHeader.tsx create mode 100644 packages/ui/src/components/Table/TableRow/TableRow.test.tsx create mode 100644 packages/ui/src/components/Table/TableRow/TableRow.tsx create mode 100644 packages/ui/src/components/Table/constants.ts create mode 100644 packages/ui/src/components/Table/contexts/TableContext/TableContext.ts create mode 100644 packages/ui/src/components/Table/contexts/TableRowContext/TableRowContext.ts create mode 100644 packages/ui/src/components/Table/index.ts create mode 100644 packages/ui/src/components/Table/testing/TableContainer/TableContainer.tsx create mode 100644 packages/ui/src/components/Table/types.ts diff --git a/AGENTS.md b/GEMINI.md similarity index 100% rename from AGENTS.md rename to GEMINI.md diff --git a/docs/plans/2026-02-08-rainby-table-implementation.md b/docs/plans/2026-02-08-rainby-table-implementation.md new file mode 100644 index 0000000..c8d4716 --- /dev/null +++ b/docs/plans/2026-02-08-rainby-table-implementation.md @@ -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(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( + + + + + + + +
Test
+
+ ); + 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 {} + +export const TableContainer = forwardRef( + ({ className, children, ...props }, ref) => ( +
+ {children} +
+ ) +); +TableContainer.displayName = 'TableContainer'; + +// Table +export interface TableProps extends React.TableHTMLAttributes, Partial { + stickyHeader?: boolean; +} + +export const Table = forwardRef( + ({ + className, + children, + density = 'm', + variant = 'surface', + striped = false, + hoverable = false, + stickyHeader = false, + ...props + }, ref) => { + const contextValue: TableContextValue = { density, variant, striped, hoverable }; + + return ( + + + {children} +
+
+ ); + } +); +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>( + ({ className, ...props }, ref) => ( + + ) +); +TableHeader.displayName = 'TableHeader'; + +// TableBody +export const TableBody = forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +TableBody.displayName = 'TableBody'; + +// TableFooter +export const TableFooter = forwardRef>( + ({ className, ...props }, ref) => ( + tr]:last:border-b-0', className)} {...props} /> + ) +); +TableFooter.displayName = 'TableFooter'; + +// TableCaption +export const TableCaption = forwardRef>( + ({ className, ...props }, ref) => ( + + ) +); +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>( + ({ className, ...props }, ref) => { + const { hoverable, striped } = useTableContext(); + return ( + + ); + } +); +TableRow.displayName = 'TableRow'; + +// TableHead +export const TableHead = forwardRef>( + ({ className, ...props }, ref) => { + const { density } = useTableContext(); + return ( + + ); + } +); +TableHead.displayName = 'TableHead'; + +// TableCell +export const TableCell = forwardRef>( + ({ className, ...props }, ref) => { + const { density } = useTableContext(); + return ( + + ); + } +); +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(Content); + expect(screen.getByText('Content')).toHaveClass('flex', 'items-center', 'text-left'); + }); + + it('applies numeric variant styles', () => { + render(100); + 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 { + 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( + ({ + 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 ( +
+ {children} +
+ ); + } +); +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" +``` diff --git a/docs/plans/2026-02-08-rainby-table-v2-implementation.md b/docs/plans/2026-02-08-rainby-table-v2-implementation.md new file mode 100644 index 0000000..4c04863 --- /dev/null +++ b/docs/plans/2026-02-08-rainby-table-v2-implementation.md @@ -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(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 { + noBorder?: boolean; + hoverable?: boolean; + selected?: boolean; +} + +const TableRowComponent = forwardRef( + function TableRow({ className, children, noBorder, hoverable, selected, ...props }, ref) { + const context = useTableContext(); + const isHoverable = hoverable ?? context.hoverable; + + return ( + (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} + + ); + } +); + +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 { + padding?: TablePadding; + align?: 'left' | 'center' | 'right'; +} + +const paddingClasses: Record = { + none: 'p-0', + s: 'p-2', + m: 'p-4', + l: 'p-6', +}; + +const TableCellComponent = forwardRef( + function TableCell({ className, children, padding, align, ...props }, ref) { + const { density } = useTableContext(); + const finalPadding = padding || density; + + return ( + (ref)} + className={cn( + 'align-middle', + paddingClasses[finalPadding], + align === 'left' && 'text-left', + align === 'center' && 'text-center', + align === 'right' && 'text-right', + className + )} + > + {children} + + ); + } +); + +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" +``` diff --git a/docs/plans/2026-02-08-table-component.md b/docs/plans/2026-02-08-table-component.md new file mode 100644 index 0000000..cc5daeb --- /dev/null +++ b/docs/plans/2026-02-08-table-component.md @@ -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 = () => ( + + + + + + + + + + + User + + + Revenue + + + Actions + + + + + + + + + + + + + {/* Complex Content Layout */} + + + + Jane Doe + Admin + + + + + $1,200.00 + + + +
+
+); +``` + +## 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. diff --git a/packages/ui/src/components/Code/Code.stories.tsx b/packages/ui/src/components/Code/Code.stories.tsx index 714fde0..a2d0774 100644 --- a/packages/ui/src/components/Code/Code.stories.tsx +++ b/packages/ui/src/components/Code/Code.stories.tsx @@ -26,7 +26,9 @@ type Story = StoryObj; // Helper to simulate highlighted lines const MockHighlightedCode = () => ( <> - import React from 'react'; + + import React from 'react'; + export const MyComponent = () => { diff --git a/packages/ui/src/components/Image/Image.stories.tsx b/packages/ui/src/components/Image/Image.stories.tsx index 2278f01..f95d401 100644 --- a/packages/ui/src/components/Image/Image.stories.tsx +++ b/packages/ui/src/components/Image/Image.stories.tsx @@ -117,8 +117,10 @@ export const LoadingState: Story = {
Simulated Loading (Using Stub) Loading -

Note: This will stay in "loading" then "error" state because URL is invalid.

- +

+ Note: This will stay in "loading" then "error" state because URL is + invalid. +

), }; diff --git a/packages/ui/src/components/Table/Table.stories.tsx b/packages/ui/src/components/Table/Table.stories.tsx new file mode 100644 index 0000000..8396365 --- /dev/null +++ b/packages/ui/src/components/Table/Table.stories.tsx @@ -0,0 +1,189 @@ +import React from 'react'; + +import { Table } from './Table'; +import { TableContainer } from './testing/TableContainer/TableContainer'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Table', + component: Table, + tags: ['autodocs'], + argTypes: { + density: { + control: 'select', + options: ['s', 'm', 'l'], + }, + variant: { + control: 'select', + options: ['surface', 'outline', 'ghost'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +const TableTemplate = (args: any) => ( + + + A list of recent transactions. + + + Invoice + Status + Method + Amount + + + + + INV001 + Paid + Credit Card + $250.00 + + + INV002 + Pending + PayPal + $150.00 + + + INV003 + Unpaid + Bank Transfer + $350.00 + + + + + Total + $750.00 + + +
+
+); + +export const Default: Story = { + args: { + density: 'm', + variant: 'surface', + striped: false, + hoverable: true, + }, + render: TableTemplate, +}; + +export const Striped: Story = { + args: { + ...Default.args, + striped: true, + }, + render: TableTemplate, +}; + +export const Compact: Story = { + args: { + ...Default.args, + density: 's', + }, + render: TableTemplate, +}; + +export const Ghost: Story = { + args: { + ...Default.args, + variant: 'ghost', + hoverable: true, + }, + render: TableTemplate, +}; + +export const Grid: Story = { + args: { + ...Default.args, + grid: true, + }, + render: TableTemplate, +}; + +export const BorderedCells: Story = { + args: { + ...Default.args, + }, + render: (args) => ( + + + + + Header with Right Border + Standard Header + + + + + All Borders + X-Axis Borders + + + Y-Axis Borders + Left Border Only + + +
+
+ ), +}; + +export const ContentCellLayouts: Story = { + args: { + density: 'm', + variant: 'outline', + }, + + render: () => ( + + + + + User + Details + Balance + Status + + + + + + +
+ JD +
+ + Jane Doe + Admin + +
+
+ + + This is a very long description that should be truncated by the content helper after + two lines. + + + + $10,240.50 + + + +
+ + + + +
+
+ ), +}; diff --git a/packages/ui/src/components/Table/Table.test.tsx b/packages/ui/src/components/Table/Table.test.tsx new file mode 100644 index 0000000..dac8f30 --- /dev/null +++ b/packages/ui/src/components/Table/Table.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { Table } from './Table'; +import { TableContainer } from './testing/TableContainer/TableContainer'; + +describe('Table', () => { + it('renders correctly with all sub-components', () => { + render( + + + Caption + + + Head + + + + + Cell + + + + + Foot + + +
+
+ ); + + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText('Caption')).toBeInTheDocument(); + expect(screen.getByText('Head')).toBeInTheDocument(); + expect(screen.getByText('Cell')).toBeInTheDocument(); + expect(screen.getByText('Foot')).toBeInTheDocument(); + }); + + it('applies density classes from context to cells', () => { + render( + + + + Small + + +
+ ); + // 's' density is 'p-2' + expect(screen.getByText('Small')).toHaveClass('p-2'); + }); + + it('allows cell-level density overrides', () => { + render( + + + + Large + + +
+ ); + // Overridden to 'l' which is 'p-6' + expect(screen.getByText('Large')).toHaveClass('p-6'); + expect(screen.getByText('Large')).not.toHaveClass('p-2'); + }); + + it('allows row-level border overrides', () => { + render( + + + + No Border + + +
+ ); + // Now the cell handles the noBorder state + expect(screen.getByTestId('no-border-cell')).not.toHaveClass('border-b'); + }); + + it('renders Table.Content with correct layout', () => { + render( + + 123 + + ); + const content = screen.getByTestId('numeric-content'); + expect(content).toHaveClass('justify-end', 'tabular-nums'); + }); + + it('TableContainer has accessibility attributes', () => { + render( + + + + ); + const container = screen.getByTestId('container'); + expect(container).toHaveAttribute('role', 'region'); + expect(container).toHaveAttribute('tabindex', '0'); + }); + + it('TableCell applies border classes', () => { + render( +
+ + + + Bordered + + + +
+ ); + expect(screen.getByTestId('bordered-cell')).toHaveClass('border'); + }); +}); diff --git a/packages/ui/src/components/Table/Table.tsx b/packages/ui/src/components/Table/Table.tsx new file mode 100644 index 0000000..9d17e0e --- /dev/null +++ b/packages/ui/src/components/Table/Table.tsx @@ -0,0 +1,101 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +import { TableContext } from './contexts/TableContext/TableContext'; +import { TableBody } from './TableBody/TableBody'; +import { TableCaption } from './TableCaption/TableCaption'; +import { TableCell } from './TableCell/TableCell'; +import { TableContent } from './TableContent/TableContent'; +import { TableFooter } from './TableFooter/TableFooter'; +import { TableHead } from './TableHead/TableHead'; +import { TableHeader } from './TableHeader/TableHeader'; +import { TableRow } from './TableRow/TableRow'; + +import type { TableContextValue } from './types'; + +/** + * Props for the root Table component. + */ +export interface TableProps + extends React.TableHTMLAttributes, + Partial { + /** + * Whether the header remains visible during scroll. + * @default false + */ + stickyHeader?: boolean; +} + +const TableComponent = forwardRef(function Table( + { + className, + children, + density = 'm', + variant = 'surface', + striped = false, + hoverable = false, + grid = false, + stickyHeader = false, + ...props + }, + ref +) { + const contextValue: TableContextValue = { density, variant, striped, hoverable, grid }; + + return ( + + (ref)} + className={cn( + 'w-full border-collapse caption-bottom text-sm', + // Mask borders at the edges to prevent doubling with container borders + '[&_td:first-child]:border-l-0 [&_th:first-child]:border-l-0', + '[&_td:last-child]:border-r-0 [&_th:last-child]:border-r-0', + '[&_tr:first-child_td]:border-t-0 [&_tr:first-child_th]:border-t-0', + '[&_tr:last-child_td]:border-b-0', + variant === 'ghost' && 'border-none', + stickyHeader && 'relative', + className + )} + > + {children} +
+
+ ); +}); + +/** + * A comprehensive Table system component that uses a compound component pattern. + * + * @param props - Component properties + * @returns The rendered table system + * + * @example + * ```tsx + * + * + * + * Name + * Role + * + * + * + * + * Vladimir + * Developer + * + * + *
+ * ``` + */ +export const Table = Object.assign(genericMemo(TableComponent), { + Header: TableHeader, + Body: TableBody, + Footer: TableFooter, + Row: TableRow, + Head: TableHead, + Cell: TableCell, + Caption: TableCaption, + Content: TableContent, +}); diff --git a/packages/ui/src/components/Table/TableBody/TableBody.tsx b/packages/ui/src/components/Table/TableBody/TableBody.tsx new file mode 100644 index 0000000..48b56f7 --- /dev/null +++ b/packages/ui/src/components/Table/TableBody/TableBody.tsx @@ -0,0 +1,29 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * A wrapper for the table body (tbody) element. + * + * @param props - Component properties + * @returns The rendered table body element + * + * @example + * ```tsx + * + * + * Data + * + * + * ``` + */ +export const TableBody = genericMemo( + forwardRef>( + function TableBody({ className, children, ...props }, ref) { + return ( + (ref)} className={cn(className)}> + {children} + + ); + } + ) +); diff --git a/packages/ui/src/components/Table/TableCaption/TableCaption.tsx b/packages/ui/src/components/Table/TableCaption/TableCaption.tsx new file mode 100644 index 0000000..3ff4bb1 --- /dev/null +++ b/packages/ui/src/components/Table/TableCaption/TableCaption.tsx @@ -0,0 +1,29 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * A wrapper for the table caption element. + * + * @param props - Component properties + * @returns The rendered table caption element + * + * @example + * ```tsx + * A list of your recent transactions. + * ``` + */ +export const TableCaption = genericMemo( + forwardRef>( + function TableCaption({ className, children, ...props }, ref) { + return ( + (ref)} + className={cn('mt-4 mb-4 text-sm text-[var(--rb-muted-text)]', className)} + > + {children} + + ); + } + ) +); diff --git a/packages/ui/src/components/Table/TableCell/TableCell.tsx b/packages/ui/src/components/Table/TableCell/TableCell.tsx new file mode 100644 index 0000000..6565e72 --- /dev/null +++ b/packages/ui/src/components/Table/TableCell/TableCell.tsx @@ -0,0 +1,81 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +import { paddingClasses, borderClasses } from '../constants'; +import { useTableContext } from '../contexts/TableContext/TableContext'; +import { useTableRowContext } from '../contexts/TableRowContext/TableRowContext'; + +import type { TablePadding, TableCellBorder } from '../types'; + +/** + * Props for the TableCell component. + */ +export interface TableCellProps extends React.TdHTMLAttributes { + /** Padding override for the cell */ + padding?: TablePadding; + /** Text alignment override */ + align?: 'left' | 'center' | 'right'; + /** + * Border configuration for the cell. + * If not provided, defaults to 'all' if Table grid is enabled, + * or 'bottom' unless the row/table is ghost. + */ + border?: TableCellBorder; +} + +/** + * A standard table data cell (td) with density and alignment control. + * + * @param props - Component properties + * @returns The rendered table cell element + * + * @example + * ```tsx + * 1,234.56 + * ``` + * + * @example + * ```tsx + * Bordered Cell + * ``` + */ +export const TableCell = genericMemo( + forwardRef(function TableCell( + { className, children, padding, align, border, ...props }, + ref + ) { + const { density, grid } = useTableContext(); + const { noBorder } = useTableRowContext(); + const finalPadding = padding ?? density; + + // Resolve the border to apply + let resolvedBorder: TableCellBorder; + if (border) { + resolvedBorder = border; + } else if (grid) { + resolvedBorder = 'all'; + } else if (noBorder) { + resolvedBorder = 'none'; + } else { + resolvedBorder = 'bottom'; + } + + return ( + (ref)} + className={cn( + 'align-middle', + paddingClasses[finalPadding], + borderClasses[resolvedBorder], + align === 'left' && 'text-left', + align === 'center' && 'text-center', + align === 'right' && 'text-right', + className + )} + > + {children} + + ); + }) +); diff --git a/packages/ui/src/components/Table/TableContent/TableContent.test.tsx b/packages/ui/src/components/Table/TableContent/TableContent.test.tsx new file mode 100644 index 0000000..2630d57 --- /dev/null +++ b/packages/ui/src/components/Table/TableContent/TableContent.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { TableContent } from './TableContent'; + +describe('TableContent', () => { + it('renders correctly', () => { + render(Content); + expect(screen.getByText('Content')).toBeInTheDocument(); + }); + + it('applies stack layout', () => { + render(Stack); + expect(screen.getByText('Stack')).toHaveClass('flex-col'); + }); + + it('applies truncate class', () => { + render(Truncated); + expect(screen.getByText('Truncated')).toHaveClass('truncate'); + }); +}); diff --git a/packages/ui/src/components/Table/TableContent/TableContent.tsx b/packages/ui/src/components/Table/TableContent/TableContent.tsx new file mode 100644 index 0000000..4a1d16d --- /dev/null +++ b/packages/ui/src/components/Table/TableContent/TableContent.tsx @@ -0,0 +1,135 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * Props for the TableContent component. + */ +export interface TableContentProps extends React.HTMLAttributes { + /** + * Visual variant presets. + * @default 'text' + */ + variant?: 'text' | 'numeric' | 'icon' | 'checkbox'; + /** + * Internal layout direction. + * @default 'flex' + */ + layout?: 'flex' | 'stack'; + /** Flex alignment override */ + align?: 'start' | 'center' | 'end' | 'between'; + /** Spacing between items */ + gap?: 's' | 'm' | 'l'; + /** + * Whether to truncate text with ellipsis (single line). + * @default false + */ + truncate?: boolean; + /** + * Maximum number of lines to display before truncating with ellipsis. + * Overrides 'truncate' if provided. + */ + maxLines?: number; + /** + * Text color variant. + * @default 'default' + */ + color?: 'default' | 'muted' | 'error' | 'success'; +} + +const gapClasses: Record = { + s: 'gap-1.5', + m: 'gap-3', + l: 'gap-4', +}; + +const colorClasses: Record = { + default: 'text-[var(--rb-text)]', + muted: 'text-[var(--rb-muted-text)]', + error: 'text-[var(--rb-error-text)]', + success: 'text-[var(--rb-success-text)]', +}; + +/** + * A polymorphic layout primitive for content within table cells. + * Standardizes alignment, spacing, and truncation for common data types. + * + * @param props - Component properties + * @returns The rendered layout element + * + * @example + * ```tsx + * $1,234.56 + * ``` + * + * @example + * ```tsx + * + * This is a long description that will be truncated after two lines. + * + * ``` + */ +export const TableContent = genericMemo( + forwardRef(function TableContent( + { + className, + children, + variant = 'text', + layout = 'flex', + align, + gap, + truncate = false, + maxLines, + color = 'default', + style, + ...props + }, + ref + ) { + 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', + }; + + // Determine default alignment based on variant if not explicitly provided + let defaultAlign: keyof typeof alignClasses = 'start'; + if (variant === 'numeric') defaultAlign = 'end'; + else if (variant === 'icon' || variant === 'checkbox') defaultAlign = 'center'; + + const finalAlign = align ?? defaultAlign; + const alignmentClass = alignClasses[finalAlign]; + + // Truncation logic + const isTruncated = truncate || maxLines !== undefined; + const lineCount = maxLines ?? (truncate ? 1 : undefined); + + return ( +
(ref)} + className={cn( + 'flex', + layout === 'flex' ? 'flex-row items-center' : 'flex-col', + alignmentClass, + gap && gapClasses[gap], + colorClasses[color], + // When truncating, we need to handle block layout and min-width + isTruncated && (lineCount === 1 ? 'truncate block' : 'line-clamp'), + isTruncated && 'min-w-0', + variant === 'numeric' && 'tabular-nums', + variant === 'checkbox' && 'w-min mx-auto', + className + )} + style={{ + ...(lineCount && lineCount > 1 + ? { WebkitLineClamp: lineCount, display: '-webkit-box', WebkitBoxOrient: 'vertical' } + : {}), + ...style, + }} + > + {children} +
+ ); + }) +); diff --git a/packages/ui/src/components/Table/TableFooter/TableFooter.tsx b/packages/ui/src/components/Table/TableFooter/TableFooter.tsx new file mode 100644 index 0000000..484304e --- /dev/null +++ b/packages/ui/src/components/Table/TableFooter/TableFooter.tsx @@ -0,0 +1,36 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * A wrapper for the table footer (tfoot) element. + * + * @param props - Component properties + * @returns The rendered table footer element + * + * @example + * ```tsx + * + * + * Total + * + * + * ``` + */ +export const TableFooter = genericMemo( + forwardRef>( + function TableFooter({ className, children, ...props }, ref) { + return ( + (ref)} + className={cn( + 'bg-[var(--rb-muted-base)]/50 font-medium [&>tr]:last:border-b-0', + className + )} + > + {children} + + ); + } + ) +); diff --git a/packages/ui/src/components/Table/TableHead/TableHead.tsx b/packages/ui/src/components/Table/TableHead/TableHead.tsx new file mode 100644 index 0000000..e79b69d --- /dev/null +++ b/packages/ui/src/components/Table/TableHead/TableHead.tsx @@ -0,0 +1,76 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +import { paddingClasses, borderClasses } from '../constants'; +import { useTableContext } from '../contexts/TableContext/TableContext'; +import { useTableRowContext } from '../contexts/TableRowContext/TableRowContext'; + +import type { TablePadding, TableCellBorder } from '../types'; + +/** + * Props for the TableHead component. + */ +export interface TableHeadProps extends React.ThHTMLAttributes { + /** Padding override for the cell */ + padding?: TablePadding; + /** Text alignment override */ + align?: 'left' | 'center' | 'right'; + /** + * Border configuration for the cell. + * If not provided, defaults to 'all' if Table grid is enabled, + * or 'bottom' unless the row/table is ghost. + */ + border?: TableCellBorder; +} + +/** + * A table header cell (th) with density and alignment control. + * + * @param props - Component properties + * @returns The rendered table header element + * + * @example + * ```tsx + * Invoice Number + * ``` + */ +export const TableHead = genericMemo( + forwardRef(function TableHead( + { className, children, padding, align = 'left', border, ...props }, + ref + ) { + const { density, grid } = useTableContext(); + const { noBorder } = useTableRowContext(); + const finalPadding = padding ?? density; + + // Resolve the border to apply + let resolvedBorder: TableCellBorder; + if (border) { + resolvedBorder = border; + } else if (grid) { + resolvedBorder = 'all'; + } else if (noBorder) { + resolvedBorder = 'none'; + } else { + resolvedBorder = 'bottom'; + } + + return ( + (ref)} + className={cn( + 'align-middle font-medium text-[var(--rb-muted-text)]', + paddingClasses[finalPadding], + borderClasses[resolvedBorder], + align === 'left' && 'text-left', + align === 'center' && 'text-center', + align === 'right' && 'text-right', + className + )} + > + {children} + + ); + }) +); diff --git a/packages/ui/src/components/Table/TableHeader/TableHeader.tsx b/packages/ui/src/components/Table/TableHeader/TableHeader.tsx new file mode 100644 index 0000000..2db702a --- /dev/null +++ b/packages/ui/src/components/Table/TableHeader/TableHeader.tsx @@ -0,0 +1,29 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * A wrapper for the table head (thead) element. + * + * @param props - Component properties + * @returns The rendered table head element + * + * @example + * ```tsx + * + * + * Header + * + * + * ``` + */ +export const TableHeader = genericMemo( + forwardRef>( + function TableHeader({ className, children, ...props }, ref) { + return ( + (ref)} className={cn(className)}> + {children} + + ); + } + ) +); diff --git a/packages/ui/src/components/Table/TableRow/TableRow.test.tsx b/packages/ui/src/components/Table/TableRow/TableRow.test.tsx new file mode 100644 index 0000000..8303b93 --- /dev/null +++ b/packages/ui/src/components/Table/TableRow/TableRow.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { Table } from '../Table'; +import { TableRow } from './TableRow'; + +describe('TableRow', () => { + it('renders correctly', () => { + render( + + + + + + +
Row
+ ); + expect(screen.getByText('Row').closest('tr')).toBeInTheDocument(); + }); + + it('applies selected class', () => { + render( + + + + + + +
Selected
+ ); + expect(screen.getByTestId('row')).toHaveClass('bg-[var(--rb-muted-base)]'); + }); +}); diff --git a/packages/ui/src/components/Table/TableRow/TableRow.tsx b/packages/ui/src/components/Table/TableRow/TableRow.tsx new file mode 100644 index 0000000..0cd21a5 --- /dev/null +++ b/packages/ui/src/components/Table/TableRow/TableRow.tsx @@ -0,0 +1,68 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +import { useTableContext } from '../contexts/TableContext/TableContext'; +import { TableRowContext } from '../contexts/TableRowContext/TableRowContext'; + +/** + * Props for the TableRow component. + */ +export interface TableRowProps extends React.HTMLAttributes { + /** + * Removes the bottom border. + * @default false + */ + noBorder?: boolean; + /** + * Overrides global hover state. + * If not provided, inherits from Table context. + */ + hoverable?: boolean; + /** + * Forces selected/highlighted state. + * @default false + */ + selected?: boolean; +} + +/** + * A table row component (tr) with hover, stripe, and border controls. + * + * @param props - Component properties + * @returns The rendered table row element + * + * @example + * ```tsx + * + * Row Data + * + * ``` + */ +export const TableRow = genericMemo( + forwardRef(function TableRow( + { className, children, noBorder, hoverable, selected, ...props }, + ref + ) { + const context = useTableContext(); + const isHoverable = hoverable ?? context.hoverable; + const isNoBorder = noBorder ?? context.variant === 'ghost'; + + return ( + + (ref)} + className={cn( + 'transition-colors', + isHoverable && 'hover:bg-[var(--rb-muted-base)]/50', + context.striped && 'even:bg-[var(--rb-muted-base)]/20', + selected && 'bg-[var(--rb-muted-base)]', + className + )} + > + {children} + + + ); + }) +); diff --git a/packages/ui/src/components/Table/constants.ts b/packages/ui/src/components/Table/constants.ts new file mode 100644 index 0000000..8111b3c --- /dev/null +++ b/packages/ui/src/components/Table/constants.ts @@ -0,0 +1,25 @@ +import type { TablePadding, TableCellBorder } from './types'; + +/** + * Shared padding classes based on table density. + */ +export const paddingClasses: Record = { + none: 'p-0', + s: 'p-2', + m: 'p-4', + l: 'p-6', +}; + +/** + * Shared border classes for table cells. + */ +export const borderClasses: Record = { + none: '', + all: 'border border-[var(--rb-border)]', + top: 'border-t border-[var(--rb-border)]', + bottom: 'border-b border-[var(--rb-border)]', + left: 'border-l border-[var(--rb-border)]', + right: 'border-r border-[var(--rb-border)]', + x: 'border-x border-[var(--rb-border)]', + y: 'border-y border-[var(--rb-border)]', +}; diff --git a/packages/ui/src/components/Table/contexts/TableContext/TableContext.ts b/packages/ui/src/components/Table/contexts/TableContext/TableContext.ts new file mode 100644 index 0000000..27de8df --- /dev/null +++ b/packages/ui/src/components/Table/contexts/TableContext/TableContext.ts @@ -0,0 +1,29 @@ +import { createContext, useContext } from 'react'; + +import type { TableContextValue } from '../../types'; + +/** + * Context for sharing table configuration (density, variant, etc.) + * with sub-components. + */ +export const TableContext = createContext(null); + +/** + * Hook to access the Table configuration context. + * Must be used within a Table component. + * + * @returns The current table context value. + * @throws Error if used outside of a Table component. + * + * @example + * ```tsx + * const { density, striped } = useTableContext(); + * ``` + */ +export function useTableContext(): TableContextValue { + const context = useContext(TableContext); + if (!context) { + throw new Error('Table sub-components must be used within a Table component.'); + } + return context; +} diff --git a/packages/ui/src/components/Table/contexts/TableRowContext/TableRowContext.ts b/packages/ui/src/components/Table/contexts/TableRowContext/TableRowContext.ts new file mode 100644 index 0000000..262473e --- /dev/null +++ b/packages/ui/src/components/Table/contexts/TableRowContext/TableRowContext.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from 'react'; + +/** + * Context for sharing row-specific configuration with cells. + */ +export interface TableRowContextValue { + /** Whether the row should have borders */ + noBorder?: boolean; +} + +export const TableRowContext = createContext({ noBorder: false }); + +/** + * Hook to access the TableRow context. + * + * @returns The current table row context value. + * @example + * ```tsx + * const { noBorder } = useTableRowContext(); + * ``` + */ +export function useTableRowContext(): TableRowContextValue { + return useContext(TableRowContext); +} diff --git a/packages/ui/src/components/Table/index.ts b/packages/ui/src/components/Table/index.ts new file mode 100644 index 0000000..5b25b17 --- /dev/null +++ b/packages/ui/src/components/Table/index.ts @@ -0,0 +1,15 @@ +export * from './Table'; +export * from './types'; +export * from './constants'; +export * from './contexts/TableContext/TableContext'; +export * from './contexts/TableRowContext/TableRowContext'; + +// Export sub-components individually for flexibility +export * from './TableHeader/TableHeader'; +export * from './TableBody/TableBody'; +export * from './TableFooter/TableFooter'; +export * from './TableRow/TableRow'; +export * from './TableHead/TableHead'; +export * from './TableCell/TableCell'; +export * from './TableCaption/TableCaption'; +export * from './TableContent/TableContent'; diff --git a/packages/ui/src/components/Table/testing/TableContainer/TableContainer.tsx b/packages/ui/src/components/Table/testing/TableContainer/TableContainer.tsx new file mode 100644 index 0000000..c47124c --- /dev/null +++ b/packages/ui/src/components/Table/testing/TableContainer/TableContainer.tsx @@ -0,0 +1,51 @@ +import { cn, castRef, genericMemo } from '@ermnvldmr/stl'; +import React, { forwardRef } from 'react'; + +/** + * Props for the TableContainer component. + */ +export interface TableContainerProps extends React.HTMLAttributes { + /** + * Visual variant of the container. + * If 'ghost', the outer border is removed. + * @default 'surface' + */ + variant?: 'surface' | 'ghost'; +} + +/** + * A responsive wrapper for the Table component. + * Handles horizontal scrolling, outer borders, and accessibility for scrollable regions. + * + * @param props - Component properties + * @returns The rendered container element + * + * @example + * ```tsx + * + * ...
+ *
+ * ``` + */ +export const TableContainer = genericMemo( + forwardRef(function TableContainer( + { className, children, variant = 'surface', ...props }, + ref + ) { + return ( +
(ref)} + className={cn( + 'w-full overflow-auto rounded-md', + variant !== 'ghost' && 'border border-[var(--rb-border)]', + className + )} + role="region" + tabIndex={0} + > + {children} +
+ ); + }) +); diff --git a/packages/ui/src/components/Table/types.ts b/packages/ui/src/components/Table/types.ts new file mode 100644 index 0000000..425621d --- /dev/null +++ b/packages/ui/src/components/Table/types.ts @@ -0,0 +1,42 @@ +/** + * Table density options. + * - 's': Small (compact) + * - 'm': Medium (standard) + * - 'l': Large (spacious) + */ +export type TableDensity = 's' | 'm' | 'l'; + +/** + * Table visual variants. + * - 'surface': Standard with background + * - 'outline': Bordered without background + * - 'ghost': No borders or background + */ +export type TableVariant = 'surface' | 'outline' | 'ghost'; + +/** + * Table padding options. + * Includes all density presets and 'none'. + */ +export type TablePadding = 'none' | TableDensity; + +/** + * Table cell border options. + */ +export type TableCellBorder = 'none' | 'all' | 'top' | 'bottom' | 'left' | 'right' | 'x' | 'y'; + +/** + * Global configuration for the Table system. + */ +export interface TableContextValue { + /** The density of the table rows and cells */ + density: TableDensity; + /** The visual style of the table */ + variant: TableVariant; + /** Whether rows should have alternating background colors */ + striped: boolean; + /** Whether rows should highlight on hover */ + hoverable: boolean; + /** Whether to show borders between all cells (grid mode) */ + grid?: boolean; +} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 7482119..f77df9f 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -15,6 +15,8 @@ export * from './Blockquote'; export * from './Stub/Stub'; export * from './Image/Image'; +export * from './Table'; + export type { StackProps } from './Stack/Stack'; export type { ListProps, ListVariant, ListSpacing, ListItemProps } from './List/List'; export type { @@ -31,3 +33,14 @@ export type { CodeProps } from './Code/Code'; export type { BlockquoteProps, BlockquoteCitationProps } from './Blockquote'; export type { StubProps } from './Stub/Stub'; export type { ImageProps } from './Image/Image'; +export type { + TableProps, + TableDensity, + TableVariant, + TablePadding, + TableRowProps, + TableHeadProps, + TableCellProps, + TableCellBorder, + TableContentProps as TableContentCellProps, +} from './Table'; diff --git a/services/www/src/shared/constants.ts b/services/www/src/shared/constants.ts index 76d3c38..7d05729 100644 --- a/services/www/src/shared/constants.ts +++ b/services/www/src/shared/constants.ts @@ -1,8 +1,8 @@ export /** - * - */ + * + */ const SITE_TITLE = 'Vladimir Eremin'; export /** - * - */ + * + */ const SITE_DESCRIPTION = 'Thoughts and creations of mine';