From e45a4d70f6771bc1760a651864abbad68f0891c4 Mon Sep 17 00:00:00 2001 From: "Hein (Warky)" Date: Thu, 12 Feb 2026 21:20:23 +0200 Subject: [PATCH] ... --- package.json | 2 + pnpm-lock.yaml | 22 + src/Griddy/plan.md | 1702 ++++++++++++++++++++++++++++---------------- 3 files changed, 1105 insertions(+), 621 deletions(-) diff --git a/package.json b/package.json index 9608f6e..062647e 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", + "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.18", "moment": "^2.30.1" }, @@ -103,6 +104,7 @@ "@mantine/notifications": "^8.3.5", "@tabler/icons-react": "^3.35.0", "@tanstack/react-query": "^5.90.5", + "@tanstack/react-table": "^8.21.3", "@warkypublic/artemis-kit": "^1.0.10", "@warkypublic/zustandsyncstore": "^0.0.4", "idb-keyval": "^6.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9675bb..d5e8b7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@tanstack/react-query': specifier: ^5.90.5 version: 5.90.5(react@19.2.4) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-virtual': specifier: ^3.13.18 version: 3.13.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1147,12 +1150,23 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + '@tanstack/react-virtual@3.13.18': resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@tanstack/virtual-core@3.13.18': resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==} @@ -5015,12 +5029,20 @@ snapshots: '@tanstack/query-core': 5.90.5 react: 19.2.4 + '@tanstack/react-table@8.21.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + '@tanstack/react-virtual@3.13.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/virtual-core': 3.13.18 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@tanstack/table-core@8.21.3': {} + '@tanstack/virtual-core@3.13.18': {} '@testing-library/dom@10.4.1': diff --git a/src/Griddy/plan.md b/src/Griddy/plan.md index cab8c4f..d19f451 100644 --- a/src/Griddy/plan.md +++ b/src/Griddy/plan.md @@ -1,20 +1,22 @@ # Griddy - Feature Complete Implementation Plan ## Project Overview -Griddy is a native TypeScript HTML table/grid component designed as a lightweight, extensible alternative to both Glide Data Editor and Mantine React Table. It will provide a zero-dependency core with optional plugin architecture, built on TanStack React Virtual for efficient virtualization. +Griddy is a native TypeScript HTML table/grid component designed as a lightweight, extensible alternative to both Glide Data Editor and Mantine React Table. It is built on **TanStack Table** (headless table model for sorting, filtering, pagination, grouping, selection) and **TanStack Virtual** (row virtualization for rendering performance), with a Zustand store for application-level state. + +--- ## Architecture & Core Design Principles ### 1. Core Philosophy -- **Independent**: Minimal peer dependencies (React, React DOM only at minimum) -- **Lightweight**: Pure HTML table rendering with CSS styling -- **Virtual-First**: Virtualization from the ground up using TanStack React Virtual -- **Plugin-Based**: All advanced features as pluggable modules -- **State Agnostic**: Works with any state management solution or local state -- **Callback-Driven**: Similar to Glide Data Editor's read-only data principle +- **TanStack Table as the Table Model**: All table logic (sorting, filtering, column ordering, pagination, row selection, column visibility, grouping) is managed by `@tanstack/react-table`. Griddy's column definitions map to TanStack `ColumnDef` under the hood. +- **TanStack Virtual for Rendering**: `@tanstack/react-virtual` virtualizes the visible row window. The virtualizer receives the row count from TanStack Table's row model and renders only what's on screen. +- **Zustand for Application State**: Grid-level concerns not owned by TanStack Table (active cell position, edit mode, search overlay visibility, focused row index, keyboard mode) live in a Zustand store. +- **Keyboard-First**: Full keyboard navigation is a core feature, not an afterthought. All interactions have keyboard equivalents. +- **Callback-Driven**: Data is read-only from the grid's perspective. Mutations flow through callbacks (`onRowSelectionChange`, `onEditCommit`, `onSortingChange`, etc.). +- **Plugin-Based**: Advanced features are opt-in through configuration, not baked into the core rendering path. ### 1.5. Lessons from Gridler -Gridly (existing implementation) provides valuable patterns: +Gridler (existing implementation) provides valuable patterns: - **Zustand Store Pattern**: Central state management using `createSyncStore` for reactive updates - **Data Adapter Pattern**: Wrapper components that interface with store (LocalDataAdaptor, FormAdaptor, APIAdaptor) - **Event System**: Uses CustomEvent for inter-component communication @@ -23,637 +25,129 @@ Gridly (existing implementation) provides valuable patterns: - **Ref-based API**: Forward refs for imperative commands (refresh, reload, scrollTo, selectRow) - **Persistence**: Automatic localStorage persistence for column order and sizing -**Griddy will adopt these proven patterns while building on TanStack React Virtual instead of Glide Data Grid** +**Griddy will adopt these proven patterns while building on TanStack Table + TanStack Virtual instead of Glide Data Grid** -### 2. Component Structure +### 2. How TanStack Table and TanStack Virtual Work Together -Following Gridler's proven architecture, with improvements: +``` +┌─────────────────────────────────────────────────────┐ +│ GriddyProps │ +│ data: T[], columns: GriddyColumn[], config... │ +└──────────────────────┬──────────────────────────────┘ + │ + ┌────────────▼────────────┐ + │ useReactTable() │ + │ @tanstack/react-table │ + │ │ + │ • ColumnDef[] built │ + │ from GriddyColumn │ + │ • Sorting state │ + │ • Filtering state │ + │ • Row selection state │ + │ • Pagination state │ + │ • Column visibility │ + │ • Column ordering │ + │ • Grouping state │ + │ │ + │ Output: table instance │ + │ → table.getRowModel() │ + │ → table.getHeaderGroups│ + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ useVirtualizer() │ + │ @tanstack/react-virtual│ + │ │ + │ count = table │ + │ .getRowModel() │ + │ .rows.length │ + │ │ + │ Output: virtualRows[] │ + │ (only visible window) │ + └────────────┬────────────┘ + │ + ┌────────────▼────────────┐ + │ Render Loop │ + │ │ + │ for each virtualRow: │ + │ row = tableRows │ + │ [virtualRow.index] │ + │ for each cell in row:│ + │ render cell via │ + │ flexRender() │ + └─────────────────────────┘ +``` + +**Key integration points**: +1. `useReactTable()` produces the full sorted/filtered/grouped row model +2. `useVirtualizer()` receives `table.getRowModel().rows.length` as its `count` +3. The render loop maps virtual items back to TanStack Table rows by index +4. TanStack Table owns all state for sorting, filtering, selection, pagination +5. The Zustand store owns UI state: focused row, edit mode, search overlay, keyboard navigation position + +### 3. Component Structure ``` Griddy/ ├── core/ -│ ├── Griddy.tsx (main component, similar to Gridler.tsx) -│ ├── GriddyStore.ts (zustand store, similar to Gridler's createSyncStore) -│ ├── GriddyProvider.tsx (context provider for configuration) -│ └── types.ts (core type definitions) -├── virtualization/ -│ ├── VirtualBody.tsx (uses TanStack React Virtual) -│ ├── VirtualHeader.tsx -│ └── hooks/useVirtualize.ts -├── features/ -│ ├── filtering/ (column filtering) -│ ├── sorting/ (multi-column sorting) -│ ├── grouping/ (header grouping & data grouping) -│ ├── search/ (global & column-specific search) -│ ├── selection/ (row/column/range selection) -│ ├── editing/ (in-place cell editors) -│ ├── pagination/ (cursor & offset-based) -│ ├── resizing/ (column width resizing) -│ ├── pinning/ (freeze columns/rows) -│ └── export/ (CSV/data export) -├── editors/ -│ ├── TextEditor.tsx -│ ├── NumericEditor.tsx -│ ├── DateEditor.tsx -│ ├── SelectEditor.tsx -│ └── CustomEditor.tsx (extensibility point) -├── renderers/ -│ ├── defaultRenderers.ts -│ ├── CustomRenderer.tsx (extensibility point) -│ └── formatters.ts (value formatting) -├── hooks/ -│ ├── useGriddy.ts (main hook) -│ ├── useFiltering.ts -│ ├── useSorting.ts -│ ├── useSearch.ts -│ ├── useSelection.ts -│ ├── usePagination.ts -│ ├── useEditing.ts -│ └── useVirtualization.ts -├── adapters/ -│ ├── LocalDataAdapter.ts -│ ├── RemoteServerAdapter.ts -│ ├── GraphQLAdapter.ts (optional) -│ └── RESTAdapter.ts (optional) -├── styles/ -│ ├── griddy.module.css -│ └── themes.ts (theme system) -└── index.ts (exports) -``` - -## Feature Specifications - -### Core Features - -#### 1. Data Handling -- **Local Data**: Direct array-based data binding -- **Remote Server Data**: Async data fetching with loading states -- **Cursor-Based Paging**: Server-side paging with cursor tokens (for performance) -- **Offset-Based Paging**: Traditional offset/limit paging -- **Lazy Loading**: Data loaded as user scrolls (virtual scrolling integration) -- **Data Adapters**: Pluggable adapters for different data sources - -API: -```typescript -interface GriddyDataSource { - data: T[] - total?: number - pageInfo?: { hasNextPage: boolean, cursor?: string } - isLoading?: boolean - error?: Error -} - -interface GriddyProps { - data: T[] - columns: Column[] - onDataChange?: (data: T[]) => void - dataAdapter?: DataAdapter - // ... other props -} -``` - -#### 2. Virtualization -- **Row Virtualization**: Only render visible rows using TanStack React Virtual -- **Column Virtualization**: Optionally virtualize columns for wide tables -- **Scrolling Performance**: Smooth scrolling with overscan optimization -- **Height Calculation**: Fixed height container with auto-height option -- **Sticky Headers**: Virtual header stays fixed while scrolling - -Implementation: -- Use `@tanstack/react-virtual` for row/column virtualization -- Configurable overscan count for smooth scrolling -- Support both fixed and variable row heights - -#### 3. Column Management -- **Column Definition**: Type-safe column definitions with accessors -- **Header Grouping**: Multi-level header groups (similar to Glide) -- **Column Pinning**: Freeze left/right columns -- **Column Resizing**: Drag-to-resize with min/max constraints -- **Column Hiding**: Show/hide columns dynamically -- **Column Reordering**: Drag-and-drop column reordering -- **Header Customization**: Custom header renderers - -API: -```typescript -interface Column { - id: string - header: string | ReactNode - accessor: keyof T | ((row: T) => any) - width?: number - minWidth?: number - maxWidth?: number - pinned?: 'left' | 'right' - sortable?: boolean - filterable?: boolean - searchable?: boolean - editable?: boolean - editor?: EditorComponent - renderer?: RendererComponent - headerGroup?: string - hidden?: boolean -} -``` - -#### 4. Filtering -- **Column Filtering**: Per-column filter UI (text, number, date, select) -- **Filter Modes**: Exact match, contains, starts with, ends with, between (for numbers/dates) -- **Multi-Filter**: Multiple filters per column with AND/OR logic -- **Filter Persistence**: Save/restore filter state -- **Custom Filters**: User-provided filter functions - -Implementation: -- Filter dropdown in column header -- Configuration for filter types per column -- Debounced filtering for performance - -#### 5. Search -- **Global Search**: Search across all searchable columns -- **Column Search**: Search within specific columns -- **Search Highlighting**: Highlight matching text -- **Search Navigation**: Navigate through search results -- **Fuzzy Search**: Optional fuzzy search capability - -API: -```typescript -interface SearchConfig { - enabled: boolean - debounceMs?: number - fuzzy?: boolean - highlightMatches?: boolean - caseSensitive?: boolean - columnsToSearch?: string[] // column IDs -} -``` - -#### 6. Sorting -- **Single Column Sort**: Sort by one column (default) -- **Multi-Column Sort**: Sort by multiple columns with priority -- **Sort Direction**: Ascending/descending/unsorted -- **Custom Sort Functions**: User-provided sort comparators -- **Sort Persistence**: Save/restore sort state -- **Server-Side Sort**: Offload sorting to server - -Implementation: -- Click header to sort (first click = asc, second = desc, third = unsorted) -- Shift+Click for multi-column sort -- Visual indicators in header (arrows/icons) - -#### 7. Row/Column Selection -- **Row Selection**: Single or multi-select with checkboxes -- **Column Selection**: Select entire columns -- **Range Selection**: Rectangular range selection (like Glide) -- **Selection Callbacks**: On selection change events -- **Selection Persistence**: Maintain selection across pagination -- **Selection Styling**: Highlight selected rows/columns - -API: -```typescript -interface SelectionConfig { - mode: 'none' | 'row' | 'column' | 'range' - multiSelect?: boolean - selectOnClick?: boolean - deselectOnClick?: boolean - onSelectionChange?: (selection: Selection) => void -} -``` - -#### 8. Grouping -- **Header Grouping**: Multi-level column groups (visual grouping only, no data aggregation initially) -- **Data Grouping**: Group rows by column value with collapsible groups -- **Aggregation**: Show aggregate values (count, sum, avg, etc.) in group headers -- **Group Actions**: Expand/collapse groups, delete groups - -#### 9. In-Place Editing -- **Text Editor**: Simple text input with validation -- **Numeric Editor**: Number input with constraints -- **Date Editor**: Date picker or input -- **Select Editor**: Dropdown with predefined options -- **Custom Editors**: User-provided editor components -- **Validation**: Per-cell or per-row validation -- **Undo/Redo**: Optional undo/redo support -- **Batch Edits**: Edit multiple cells and submit together - -API: -```typescript -interface EditorProps { - value: any - column: Column - row: T - onCommit: (newValue: any) => void - onCancel: () => void - onBlur?: () => void -} - -interface Column { - editor?: EditorComponent - editable?: boolean | ((row: T) => boolean) - onEditCommit?: (newValue: any, row: T) => Promise - validation?: (value: any) => string | null -} -``` - -#### 10. Pagination -- **Offset Pagination**: Page/limit based (e.g., page 1-10, 20 items per page) -- **Cursor Pagination**: Cursor-based (for infinite scroll or server-side) -- **Page Navigation**: Next/prev buttons, page input, jump to page -- **Items Per Page**: Configurable items per page with preset options -- **Total Count**: Display total items and current range - -API: -```typescript -interface PaginationConfig { - enabled: boolean - type: 'offset' | 'cursor' - pageSize: number - pageSizeOptions?: number[] - onPageChange?: (page: number) => void - onPageSizeChange?: (pageSize: number) => void -} -``` - -#### 11. Extensibility - -**Custom Cell Renderers**: -```typescript -interface RendererProps { - value: any - row: T - column: Column - rowIndex: number - columnIndex: number - isEditing?: boolean -} - -type CellRenderer = (props: RendererProps) => ReactNode -``` - -**Custom Features via Hooks**: -- Users can wrap Griddy with custom hooks to add features -- Plugin architecture through context and callbacks - -**Custom Editors**: -- Any component can be an editor if it implements EditorProps interface - -**Custom Data Adapters**: -```typescript -interface DataAdapter { - fetch: (config: FetchConfig) => Promise> - save: (row: T) => Promise - delete: (row: T) => Promise -} -``` - ---- - -## Architectural Patterns from Gridler to Adopt - -### 1. Zustand Store Pattern -Gridler's `GridlerStore.tsx` demonstrates effective state management: -```typescript -// Griddy will follow similar pattern -const { Provider, useGriddyStore } = createSyncStore( - (set, get) => ({ - // State mutations and getters - // Immer integration for immutable updates - // Custom methods for grid operations - }) -) -``` -**Benefits**: Reactive updates, easy testing, dev tools support, persistence support - -### 2. Data Adapter Pattern -Gridler's adapters (LocalDataAdaptor, FormAdaptor, APIAdaptor) show clean separation: -- Adapters are wrapper components that consume store and trigger data operations -- They handle filtering, sorting, searching via store mutations -- Support both local and remote data without core changes -- Each adapter implements `useAPIQuery` pattern for pagination - -**Griddy will adopt this for extensibility**: -```typescript -// Location: src/Griddy/adapters/ -// - LocalDataAdapter.tsx (array data) -// - RemoteServerAdapter.tsx (API with cursor/offset paging) -// - CustomAdapter pattern for user extensions -``` - -### 3. Event System -Gridler uses `CustomEvent` for loose coupling between components: -```typescript -state._events.dispatchEvent(new CustomEvent('loadPage', { detail })) -state._events.addEventListener('reload', handler) -``` -**Griddy benefit**: Allows features to trigger actions without tight coupling - -### 4. Column Definition System -Gridler's `GridlerColumn` interface shows best practices: -- Optional `Cell` renderer for custom cell display -- `disableSort`, `disableFilter`, `disableMove`, `disableResize` flags -- Custom menu items via `getMenuItems` -- Tooltip support (string or function) -- Nested field access via dot notation - -**Griddy will enhance this**: -```typescript -interface Column { - id: string - header: string - accessor: keyof T | ((row: T) => any) - renderer?: (props: RendererProps) => ReactNode - editor?: EditorComponent - sortable?: boolean - filterable?: boolean - resizable?: boolean - pinned?: 'left' | 'right' - // ... more options -} -``` - -### 5. Ref-Based Imperative API -Gridler's `GridlerRef` provides necessary imperative operations: -```typescript -interface GridlerRef { - getState: () => GridlerState - refresh: () => Promise - scrollToRow: (key: string | number) => Promise - selectRow: (key: string | number) => Promise - reloadRow: (key: string | number) => Promise -} -``` -**Griddy will provide similar imperative API for parent control** - -### 6. Persistence Layer -Gridler uses localStorage for column state: -```typescript -persist={{ - name: `Gridler_${props.uniqueid}`, - partialize: (s) => ({ colOrder: s.colOrder, colSize: s.colSize }), - version: 1, -}} -``` -**Griddy will support this optional persistence** - ---- - -## Implementation Phases - -### Phase 1: Core Foundation (Weeks 1-2) -- [ ] Set up Griddy package structure -- [ ] Create core types and interfaces -- [ ] Implement basic Griddy component with: - - Simple table rendering (HTML ``) - - Column definition system - - Basic props interface - - Initial Zustand store (optional, minimal state) -- [ ] Implement LocalDataAdapter -- [ ] Add Storybook stories for basic table - -**Deliverable**: Functional table rendering local data with column definitions - -### Phase 2: Virtualization & Performance (Weeks 2-3) -- [ ] Integrate TanStack React Virtual -- [ ] Implement row virtualization -- [ ] Implement sticky header with virtual scroll -- [ ] Add column resizing -- [ ] Performance optimization (memo, useMemo) -- [ ] Test with large datasets (10k+ rows) - -**Deliverable**: High-performance virtualized table - -### Phase 3: Data Handling (Weeks 3-4) -- [ ] Implement RemoteServerAdapter -- [ ] Add cursor-based pagination -- [ ] Add offset-based pagination -- [ ] Implement loading states -- [ ] Error handling and display -- [ ] Infinite scroll pattern - -**Deliverable**: Full data source flexibility (local, remote, paginated) - -### Phase 4: Selection & Interaction (Week 4) -- [ ] Row selection (checkbox + keyboard) -- [ ] Column selection -- [ ] Range selection -- [ ] Selection persistence -- [ ] Keyboard navigation (arrows, shift+click, etc.) - -**Deliverable**: Full selection capabilities - -### Phase 5: Filtering, Sorting, Search (Week 5) -- [ ] Sorting (single & multi-column) -- [ ] Column filtering UI and logic -- [ ] Global search -- [ ] Search highlighting -- [ ] Filter/sort state persistence - -**Deliverable**: Data manipulation features - -### Phase 6: In-Place Editing (Week 5-6) -- [ ] Text, Numeric, Date editors -- [ ] Select editor -- [ ] Validation system -- [ ] Edit callbacks (onEditCommit, onEditCancel) -- [ ] Undo/redo (optional) -- [ ] Trailing row for new records (optional) - -**Deliverable**: Full editing capabilities - -### Phase 7: Advanced Features (Week 6-7) -- [ ] Grouping (header groups) -- [ ] Data grouping with aggregation -- [ ] Column pinning/freezing -- [ ] Column reordering -- [ ] Column hiding/showing -- [ ] Export to CSV - -**Deliverable**: Advanced features - -### Phase 8: Polish & Documentation (Week 7-8) -- [ ] Comprehensive Storybook stories -- [ ] API documentation -- [ ] TypeScript definitions and examples -- [ ] Integration examples -- [ ] Performance benchmarks -- [ ] Accessibility improvements (ARIA, keyboard nav) -- [ ] Theme system documentation - -**Deliverable**: Production-ready component - ---- - -## Comparison with Competitors - -### vs. Glide Data Editor -**Advantages**: -- No proprietary license needed -- Simpler API, less callback hell -- Smaller bundle size -- Framework agnostic (could be ported to Vue, Svelte, etc.) -- More transparent development - -**Disadvantages**: -- Less mature (initially) -- Fewer enterprise features -- No rich text editing (not planned for MVP) - -### vs. Mantine React Table -**Advantages**: -- Lighter weight -- Pure virtual rendering (no DOM overhead) -- More flexible CSS styling (pure HTML table) -- Better performance with large datasets -- Simpler core API - -**Disadvantages**: -- No built-in Modal editing (only in-line) -- No tree/hierarchical data (initially) -- Requires Mantine knowledge (eventually provide integrations) - ---- - -## Technology Stack - -- **React**: 19.x (peer dependency) -- **TypeScript**: 5.9+ -- **Virtualization**: @tanstack/react-virtual 3.13+ -- **State Management**: Zustand (optional, for advanced features) -- **Styling**: CSS Modules + Mantine theming integration (optional) -- **Testing**: Vitest, React Testing Library -- **Build**: Vite with TypeScript declaration generation - ---- - -## Dependencies - -**Peer Dependencies**: -- react >= 19.0.0 -- react-dom >= 19.0.0 -- @tanstack/react-virtual >= 3.13.0 - -**Optional Peer Dependencies**: -- @mantine/core (for integrated theming) -- @tanstack/react-query (for server-side data) - -**Dev Dependencies**: -- Same as Oranguru project (Vitest, Storybook, etc.) - ---- - -## Testing Strategy - -1. **Unit Tests**: - - Editor components - - Filter/Sort logic - - Data adapter functions - - Hook logic - -2. **Integration Tests**: - - Column definition system - - Data flow end-to-end - - Selection with filtering - - Editing with validation - -3. **Performance Tests**: - - Rendering 10k+ rows - - Filtering/sorting on large datasets - - Memory usage monitoring - -4. **Accessibility Tests**: - - Keyboard navigation - - ARIA labels and roles - - Screen reader compatibility - ---- - -## Browser Support - -- Chrome/Edge: Latest 2 versions -- Firefox: Latest 2 versions -- Safari: Latest 2 versions -- No IE11 support (modern React requirement) - ---- - -## Success Criteria - -1. ✅ Table renders 10k+ rows smoothly (60fps) -2. ✅ Filtering/sorting on 10k rows completes in <500ms -3. ✅ TypeScript types cover 95%+ of code -4. ✅ Bundle size < 50KB (gzipped, excluding Mantine) -5. ✅ Full keyboard accessibility (WCAG AA) -6. ✅ 100+ Storybook stories covering all features -7. ✅ Zero external styling dependencies (works with any CSS) -8. ✅ All features tested with >80% coverage - ---- - -## Known Limitations (Phase 1) - -- Tree/hierarchical data: Planned for Phase 2 -- Drag-and-drop column reordering: Phase 2 -- Copy/paste: Phase 2 -- Rich text/HTML content: Not planned -- Master-detail/expandable rows: Phase 2 - ---- - -## File Structure (Detailed) - -``` -src/Griddy/ -├── core/ -│ ├── Griddy.tsx # Main component -│ ├── GriddyContext.ts # React context -│ ├── GriddyStore.ts # Zustand store (optional) -│ ├── types.ts # Core types -│ ├── defaults.ts # Default config -│ └── constants.ts # Constants -├── virtualization/ -│ ├── VirtualBody.tsx # Virtual row rendering -│ ├── VirtualHeader.tsx # Virtual column header -│ ├── hooks/ -│ │ ├── useVirtualize.ts # Virtualization logic -│ │ └── useVirtualizeColumns.ts -│ └── utils/ -│ └── sizing.ts # Size calculations +│ ├── Griddy.tsx # Main component, orchestrates table + virtualizer +│ ├── GriddyStore.ts # Zustand store for UI state (focus, edit mode, search) +│ ├── GriddyProvider.tsx # Context provider: table instance + store + config +│ ├── GriddyTable.tsx # TanStack Table setup (useReactTable call) +│ ├── types.ts # Core types: GriddyColumn, GriddyProps, etc. +│ ├── columnMapper.ts # Maps GriddyColumn → TanStack ColumnDef +│ ├── defaults.ts # Default config values +│ └── constants.ts # Key codes, CSS class names, etc. +├── rendering/ +│ ├── VirtualBody.tsx # Virtual row rendering via useVirtualizer +│ ├── TableHeader.tsx # Renders table.getHeaderGroups() +│ ├── TableRow.tsx # Renders a single row's cells via flexRender +│ ├── TableCell.tsx # Individual cell with edit/render mode +│ └── hooks/ +│ └── useGridVirtualizer.ts # Wraps useVirtualizer with grid-specific config ├── features/ +│ ├── keyboard/ +│ │ ├── useKeyboardNavigation.ts # Master keyboard handler +│ │ ├── keyMap.ts # Key binding definitions +│ │ └── types.ts +│ ├── selection/ +│ │ ├── SelectionCheckbox.tsx # Row selection checkbox column +│ │ ├── useGridSelection.ts # Wraps TanStack Table row selection +│ │ └── types.ts │ ├── filtering/ │ │ ├── FilterControl.tsx -│ │ ├── useFiltering.ts +│ │ ├── useGridFiltering.ts # Wraps TanStack Table column filters │ │ ├── types.ts │ │ └── operators.ts │ ├── sorting/ │ │ ├── SortIndicator.tsx -│ │ ├── useSorting.ts +│ │ ├── useGridSorting.ts # Wraps TanStack Table sorting │ │ └── types.ts │ ├── search/ -│ │ ├── SearchBox.tsx -│ │ ├── useSearch.ts -│ │ └── types.ts -│ ├── selection/ -│ │ ├── SelectionCheckbox.tsx -│ │ ├── useSelection.ts -│ │ ├── types.ts -│ │ └── keyboard.ts -│ ├── grouping/ -│ │ ├── HeaderGroup.tsx -│ │ ├── DataGrouping.tsx -│ │ ├── useGrouping.ts +│ │ ├── SearchOverlay.tsx # Ctrl+F search overlay UI +│ │ ├── useGridSearch.ts # Global filter via TanStack Table │ │ └── types.ts │ ├── editing/ │ │ ├── EditableCell.tsx -│ │ ├── useEditing.ts +│ │ ├── useGridEditing.ts │ │ ├── validation.ts │ │ └── types.ts │ ├── pagination/ │ │ ├── PaginationControl.tsx -│ │ ├── usePagination.ts +│ │ ├── useGridPagination.ts # Wraps TanStack Table pagination +│ │ └── types.ts +│ ├── grouping/ +│ │ ├── GroupHeader.tsx +│ │ ├── useGridGrouping.ts # Wraps TanStack Table grouping │ │ └── types.ts │ ├── pinning/ -│ │ ├── usePinning.ts +│ │ ├── useGridPinning.ts # Wraps TanStack Table column pinning │ │ └── types.ts -│ └── resizing/ -│ ├── useColumnResizing.ts -│ └── types.ts +│ ├── resizing/ +│ │ ├── useGridResizing.ts # Wraps TanStack Table column resizing +│ │ └── types.ts +│ └── export/ +│ └── exportCsv.ts ├── editors/ │ ├── TextEditor.tsx │ ├── NumericEditor.tsx @@ -671,7 +165,7 @@ src/Griddy/ │ ├── base.ts │ └── types.ts ├── hooks/ -│ ├── useGriddy.ts # Main hook composing all features +│ ├── useGriddy.ts # Main hook: composes table + virtualizer + store │ ├── useMergedRefs.ts │ ├── useAsync.ts │ └── index.ts @@ -683,20 +177,986 @@ src/Griddy/ ├── utils/ │ ├── merge.ts │ ├── classNames.ts -│ ├── keyboard.ts │ ├── accessibility.ts │ └── index.ts -├── index.ts # Main export +├── index.ts # Main export ├── Griddy.test.tsx └── README.md ``` --- +## Feature Specifications + +### Core Features + +#### 1. TanStack Table Integration + +TanStack Table is the **headless table engine** that manages all table state and logic. Griddy wraps it with opinionated defaults and a simpler column API. + +**Column Definition Mapping**: +```typescript +// Griddy's user-facing column API +interface GriddyColumn { + id: string + header: string | ReactNode + accessor: keyof T | ((row: T) => any) + width?: number + minWidth?: number + maxWidth?: number + pinned?: 'left' | 'right' + sortable?: boolean // maps to enableSorting + filterable?: boolean // maps to enableColumnFilter + searchable?: boolean // included in global filter + editable?: boolean | ((row: T) => boolean) + editor?: EditorComponent + renderer?: CellRenderer + headerGroup?: string + hidden?: boolean // maps to column visibility + sortFn?: SortingFn // custom TanStack sort function + filterFn?: FilterFn // custom TanStack filter function +} + +// Internal: columnMapper.ts converts GriddyColumn → ColumnDef +function mapColumns(columns: GriddyColumn[]): ColumnDef[] { + return columns.map(col => ({ + id: col.id, + accessorFn: typeof col.accessor === 'function' + ? col.accessor + : (row: T) => row[col.accessor as keyof T], + header: col.header, + cell: col.renderer + ? ({ getValue, row, column }) => col.renderer!({ value: getValue(), row: row.original, column: col, ... }) + : undefined, + size: col.width, + minSize: col.minWidth ?? 50, + maxSize: col.maxWidth, + enableSorting: col.sortable ?? true, + enableColumnFilter: col.filterable ?? false, + sortingFn: col.sortFn, + filterFn: col.filterFn, + enablePinning: col.pinned !== undefined, + enableHiding: true, + meta: { griddy: col }, // preserve original GriddyColumn for editors, etc. + })) +} +``` + +**Table Instance Setup** (in `GriddyTable.tsx` / `useGriddy.ts`): +```typescript +const table = useReactTable({ + data, + columns: mappedColumns, + state: { + sorting, + columnFilters, + globalFilter, + rowSelection, + columnVisibility, + columnOrder, + columnPinning, + pagination, + grouping, + expanded, + }, + // Feature toggles + enableRowSelection: selectionConfig.mode !== 'none', + enableMultiRowSelection: selectionConfig.multiSelect, + enableSorting: true, + enableFilters: true, + enableColumnResizing: true, + enableGrouping: groupingConfig?.enabled ?? false, + enablePinning: true, + // State handlers + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onGlobalFilterChange: setGlobalFilter, + onRowSelectionChange: setRowSelection, + onColumnVisibilityChange: setColumnVisibility, + onColumnOrderChange: setColumnOrder, + onPaginationChange: setPagination, + // Pipeline + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: paginationConfig?.enabled ? getPaginationRowModel() : undefined, + getGroupedRowModel: groupingConfig?.enabled ? getGroupedRowModel() : undefined, + getExpandedRowModel: getExpandedRowModel(), +}) +``` + +#### 2. Virtualization (TanStack Virtual) + +TanStack Virtual renders only visible rows from the TanStack Table row model. + +```typescript +// In useGridVirtualizer.ts +const rowModel = table.getRowModel() + +const virtualizer = useVirtualizer({ + count: rowModel.rows.length, + getScrollElement: () => scrollContainerRef.current, + estimateSize: () => rowHeight, // configurable, default 36px + overscan: overscanCount, // configurable, default 10 +}) + +// Render loop maps virtual items → table rows +const virtualRows = virtualizer.getVirtualItems() +virtualRows.map(virtualRow => { + const row = rowModel.rows[virtualRow.index] + // render row.getVisibleCells() via flexRender +}) +``` + +- **Row Virtualization**: Only visible rows rendered, powered by TanStack Virtual +- **Scrolling Performance**: Configurable overscan (default 10 rows) +- **Height Calculation**: Fixed height container with configurable row height +- **Sticky Headers**: CSS `position: sticky` on `` +- **Column Virtualization**: Deferred (not needed initially) + +#### 3. Data Handling +- **Local Data**: Direct array-based data binding passed to TanStack Table +- **Remote Server Data**: Async data fetching with loading states +- **Cursor-Based Paging**: Server-side paging with cursor tokens +- **Offset-Based Paging**: Traditional offset/limit paging via TanStack Table pagination +- **Lazy Loading**: Data loaded as user scrolls (virtual scrolling integration) +- **Data Adapters**: Pluggable adapters for different data sources + +API: +```typescript +interface GriddyDataSource { + data: T[] + total?: number + pageInfo?: { hasNextPage: boolean, cursor?: string } + isLoading?: boolean + error?: Error +} + +interface GriddyProps { + data: T[] + columns: GriddyColumn[] + getRowId?: (row: T) => string // for stable row identity + onDataChange?: (data: T[]) => void + dataAdapter?: DataAdapter + // Keyboard + keyboardNavigation?: boolean // default: true + // Selection + selection?: SelectionConfig + onRowSelectionChange?: (selection: Record) => void + // Sorting + sorting?: SortingState + onSortingChange?: (sorting: SortingState) => void + // Filtering + columnFilters?: ColumnFiltersState + onColumnFiltersChange?: (filters: ColumnFiltersState) => void + // Search + search?: SearchConfig + // Editing + onEditCommit?: (rowId: string, columnId: string, value: any) => void | Promise + // Pagination + pagination?: PaginationConfig + // Virtualization + rowHeight?: number // default: 36 + overscan?: number // default: 10 + height?: number | string // container height + // Persistence + persistenceKey?: string // localStorage key prefix +} +``` + +#### 4. Column Management +Powered by TanStack Table's column APIs: +- **Column Definition**: GriddyColumn mapped to TanStack ColumnDef +- **Header Grouping**: TanStack Table `getHeaderGroups()` for multi-level headers +- **Column Pinning**: TanStack Table `columnPinning` state +- **Column Resizing**: TanStack Table `enableColumnResizing` + drag handlers +- **Column Hiding**: TanStack Table `columnVisibility` state +- **Column Reordering**: TanStack Table `columnOrder` state + drag-and-drop +- **Header Customization**: Custom header via `header` field in column definition + +#### 5. Filtering +Powered by TanStack Table's filtering pipeline: +- **Column Filtering**: `enableColumnFilter` per column, `getFilteredRowModel()` +- **Filter Modes**: Built-in TanStack filter functions + custom `filterFn` per column +- **Multi-Filter**: Multiple column filters applied simultaneously (AND logic by default) +- **Filter Persistence**: Save/restore `columnFilters` state +- **Custom Filters**: User-provided `filterFn` on column definition + +#### 6. Search +Global search powered by TanStack Table's `globalFilter`: +- **Global Search**: `setGlobalFilter()` searches across all columns with `searchable: true` +- **Search Overlay**: Ctrl+F opens search overlay UI (custom, not browser find) +- **Search Highlighting**: Custom cell renderer highlights matching text +- **Search Navigation**: Navigate through matches with Enter/Shift+Enter in overlay +- **Fuzzy Search**: Optional via custom global filter function + +API: +```typescript +interface SearchConfig { + enabled: boolean + debounceMs?: number // default: 300 + fuzzy?: boolean + highlightMatches?: boolean // default: true + caseSensitive?: boolean // default: false + placeholder?: string +} +``` + +#### 7. Sorting +Powered by TanStack Table's sorting pipeline: +- **Single Column Sort**: Default mode (click header to cycle asc → desc → none) +- **Multi-Column Sort**: Shift+Click adds to sort stack +- **Custom Sort Functions**: `sortFn` on column definition → TanStack `sortingFn` +- **Sort Persistence**: Save/restore `sorting` state +- **Server-Side Sort**: When `manualSorting: true`, callbacks handle sort externally + +--- + +### Keyboard Navigation (Core Feature) + +Griddy is a **keyboard-first** grid. The grid container is focusable (`tabIndex={0}`) and maintains a **focused row index** in the Zustand store. All keyboard shortcuts work when the grid has focus. + +#### Keyboard State (Zustand Store) +```typescript +interface GriddyUIState { + // Focus + focusedRowIndex: number | null // index into table.getRowModel().rows + focusedColumnId: string | null // for future cell-level focus + + // Modes + isEditing: boolean // true when a cell editor is open + isSearchOpen: boolean // true when Ctrl+F search overlay is shown + isSelecting: boolean // true when in selection mode (Ctrl+S) + + // Actions + setFocusedRow: (index: number | null) => void + setEditing: (editing: boolean) => void + setSearchOpen: (open: boolean) => void + setSelecting: (selecting: boolean) => void + moveFocus: (direction: 'up' | 'down', amount: number) => void + moveFocusToStart: () => void + moveFocusToEnd: () => void +} +``` + +#### Key Bindings + +All key bindings are handled in `useKeyboardNavigation.ts` which attaches a `keydown` listener to the grid container. When in **edit mode** or **search mode**, most bindings are suppressed (only Escape is active to exit the mode). + +| Key | Action | Context | +|-----|--------|---------| +| `ArrowUp` | Move focus to previous row | Normal mode | +| `ArrowDown` | Move focus to next row | Normal mode | +| `PageUp` | Move focus up by one page (visible row count) | Normal mode | +| `PageDown` | Move focus down by one page (visible row count) | Normal mode | +| `Home` | Move focus to first row | Normal mode | +| `End` | Move focus to last row | Normal mode | +| `Ctrl+F` | Open search overlay (preventDefault to block browser find) | Normal mode | +| `Escape` | Close search overlay / cancel edit / exit selection mode | Any mode | +| `Ctrl+E` | Enter edit mode on focused row's first editable cell | Normal mode | +| `Enter` | Enter edit mode on focused row's first editable cell | Normal mode | +| `Ctrl+S` | Toggle selection mode (preventDefault to block browser save) | Normal mode | +| `Space` | Toggle selection of focused row | Selection mode or normal mode with selection enabled | +| `Shift+ArrowUp` | Extend selection to include previous row | Selection mode (multi-select) | +| `Shift+ArrowDown` | Extend selection to include next row | Selection mode (multi-select) | +| `Ctrl+A` | Select all rows (when multi-select enabled) | Normal mode | +| `Tab` | Move to next editable cell | Edit mode | +| `Shift+Tab` | Move to previous editable cell | Edit mode | +| `Enter` | Commit edit and move to next row | Edit mode | +| `Escape` | Cancel edit, return to normal mode | Edit mode | + +#### Implementation: `useKeyboardNavigation.ts` + +```typescript +function useKeyboardNavigation( + table: Table, + virtualizer: Virtualizer, + store: GriddyUIState, + config: { + selectionMode: SelectionConfig['mode'] + multiSelect: boolean + editingEnabled: boolean + searchEnabled: boolean + } +) { + const handleKeyDown = useCallback((e: KeyboardEvent) => { + const { focusedRowIndex, isEditing, isSearchOpen } = store.getState() + const rowCount = table.getRowModel().rows.length + const pageSize = virtualizer.getVirtualItems().length + + // Search mode: only Escape exits + if (isSearchOpen) { + if (e.key === 'Escape') { + store.getState().setSearchOpen(false) + e.preventDefault() + } + return // let SearchOverlay handle its own keys + } + + // Edit mode: Tab, Shift+Tab, Enter, Escape + if (isEditing) { + if (e.key === 'Escape') { + store.getState().setEditing(false) + e.preventDefault() + } + return // let editor handle its own keys + } + + // Normal mode + switch (true) { + case e.key === 'ArrowDown': + e.preventDefault() + store.getState().moveFocus('down', 1) + break + + case e.key === 'ArrowUp': + e.preventDefault() + store.getState().moveFocus('up', 1) + break + + case e.key === 'PageDown': + e.preventDefault() + store.getState().moveFocus('down', pageSize) + break + + case e.key === 'PageUp': + e.preventDefault() + store.getState().moveFocus('up', pageSize) + break + + case e.key === 'Home': + e.preventDefault() + store.getState().moveFocusToStart() + break + + case e.key === 'End': + e.preventDefault() + store.getState().moveFocusToEnd() + break + + case e.key === 'f' && e.ctrlKey: + if (config.searchEnabled) { + e.preventDefault() // block browser find + store.getState().setSearchOpen(true) + } + break + + case (e.key === 'e' && e.ctrlKey) || e.key === 'Enter': + if (config.editingEnabled && focusedRowIndex !== null) { + e.preventDefault() + store.getState().setEditing(true) + } + break + + case e.key === 's' && e.ctrlKey: + if (config.selectionMode !== 'none') { + e.preventDefault() // block browser save + store.getState().setSelecting(!store.getState().isSelecting) + } + break + + case e.key === ' ': + if (config.selectionMode !== 'none' && focusedRowIndex !== null) { + e.preventDefault() + const row = table.getRowModel().rows[focusedRowIndex] + row.toggleSelected() + } + break + + case e.key === 'a' && e.ctrlKey: + if (config.multiSelect) { + e.preventDefault() + table.toggleAllRowsSelected() + } + break + + case e.key === 'ArrowDown' && e.shiftKey: + if (config.multiSelect && focusedRowIndex !== null) { + e.preventDefault() + const nextIdx = Math.min(focusedRowIndex + 1, rowCount - 1) + const row = table.getRowModel().rows[nextIdx] + row.toggleSelected(true) // select (don't deselect) + store.getState().moveFocus('down', 1) + } + break + + case e.key === 'ArrowUp' && e.shiftKey: + if (config.multiSelect && focusedRowIndex !== null) { + e.preventDefault() + const prevIdx = Math.max(focusedRowIndex - 1, 0) + const row = table.getRowModel().rows[prevIdx] + row.toggleSelected(true) + store.getState().moveFocus('up', 1) + } + break + } + + // Auto-scroll focused row into view + const newFocused = store.getState().focusedRowIndex + if (newFocused !== null) { + virtualizer.scrollToIndex(newFocused, { align: 'auto' }) + } + }, [table, virtualizer, store, config]) + + useEffect(() => { + const el = scrollContainerRef.current + el?.addEventListener('keydown', handleKeyDown) + return () => el?.removeEventListener('keydown', handleKeyDown) + }, [handleKeyDown]) +} +``` + +#### Focus Visual Indicator +The focused row receives a CSS class `griddy-row--focused` which renders a visible focus ring/highlight. This is distinct from selection highlighting. + +```css +.griddy-row--focused { + outline: 2px solid var(--griddy-focus-color, #228be6); + outline-offset: -2px; + z-index: 1; +} + +.griddy-row--selected { + background-color: var(--griddy-selection-bg, rgba(34, 139, 230, 0.1)); +} + +.griddy-row--focused.griddy-row--selected { + outline: 2px solid var(--griddy-focus-color, #228be6); + background-color: var(--griddy-selection-bg, rgba(34, 139, 230, 0.1)); +} +``` + +#### Auto-Scroll on Focus Change +When the focused row changes (via keyboard), `virtualizer.scrollToIndex()` ensures the focused row is visible. The `align: 'auto'` option scrolls only if the row is outside the visible area. + +--- + +### Row Selection (Core Feature) + +Row selection is powered by **TanStack Table's row selection** (`enableRowSelection`, `onRowSelectionChange`). Griddy adds keyboard-driven selection on top. + +#### Selection Modes + +```typescript +interface SelectionConfig { + /** 'none' = no selection, 'single' = one row at a time, 'multi' = multiple rows */ + mode: 'none' | 'single' | 'multi' + + /** Show checkbox column (auto-added as first column) */ + showCheckbox?: boolean // default: true when mode !== 'none' + + /** Allow clicking row body to toggle selection */ + selectOnClick?: boolean // default: true + + /** Maintain selection across pagination/sorting */ + preserveSelection?: boolean // default: true + + /** Callback when selection changes */ + onSelectionChange?: (selectedRows: T[], selectionState: Record) => void +} +``` + +#### Single Selection Mode (`mode: 'single'`) +- Only one row selected at a time +- Clicking a row selects it and deselects the previous +- Arrow keys move focus; Space selects the focused row (deselects previous) +- TanStack Table config: `enableRowSelection: true`, `enableMultiRowSelection: false` + +#### Multi Selection Mode (`mode: 'multi'`) +- Multiple rows can be selected simultaneously +- Click toggles individual row selection +- Shift+Click selects range from last selected to clicked row +- Ctrl+Click (or Cmd+Click on Mac) toggles individual without affecting others +- Keyboard: + - `Space` toggles focused row + - `Shift+ArrowUp/Down` extends selection + - `Ctrl+A` selects all + - `Escape` clears selection +- TanStack Table config: `enableRowSelection: true`, `enableMultiRowSelection: true` + +#### Checkbox Column +When `showCheckbox` is true (default for selection modes), a checkbox column is automatically prepended: + +```typescript +// Auto-injected via columnMapper when selection is enabled +const checkboxColumn: ColumnDef = { + id: '_selection', + header: ({ table }) => ( + selectionConfig.mode === 'multi' ? ( + + ) : null + ), + cell: ({ row }) => ( + + ), + size: 40, + enableSorting: false, + enableColumnFilter: false, + enableResizing: false, + enablePinning: false, +} +``` + +#### Selection State +Selection state uses TanStack Table's `rowSelection` state (a `Record` keyed by row ID). This integrates automatically with sorting, filtering, and pagination. + +```typescript +// Controlled selection +const [rowSelection, setRowSelection] = useState({}) + +// In useReactTable: +{ + state: { rowSelection }, + onRowSelectionChange: setRowSelection, + enableRowSelection: true, + enableMultiRowSelection: config.selection.mode === 'multi', + getRowId: config.getRowId ?? ((row, index) => String(index)), +} +``` + +--- + +### In-Place Editing + +#### Edit Mode Activation +- `Ctrl+E` or `Enter` on a focused row opens the first editable cell for editing +- Double-click on a cell opens it for editing +- When editing: + - `Tab` moves to next editable cell in the row + - `Shift+Tab` moves to previous editable cell + - `Enter` commits the edit and moves focus to the next row + - `Escape` cancels the edit + +#### Editor Components +```typescript +interface EditorProps { + value: any + column: GriddyColumn + row: T + rowIndex: number + onCommit: (newValue: any) => void + onCancel: () => void + onMoveNext: () => void // Tab + onMovePrev: () => void // Shift+Tab +} +``` + +Built-in editors: TextEditor, NumericEditor, DateEditor, SelectEditor, CheckboxEditor. + +--- + +### Search Overlay + +#### Behavior +- `Ctrl+F` opens a search overlay bar at the top of the grid +- Search input is auto-focused +- Typing updates `table.setGlobalFilter()` with debounce +- Matching cells are highlighted via custom cell renderer +- `Enter` in search input navigates to next match +- `Shift+Enter` navigates to previous match +- `Escape` closes overlay and clears search + +#### Implementation +```typescript +// SearchOverlay.tsx +function SearchOverlay({ table, store }: { table: Table, store: GriddyUIState }) { + const [query, setQuery] = useState('') + + const debouncedFilter = useDebouncedCallback((value: string) => { + table.setGlobalFilter(value || undefined) + }, 300) + + return ( +
+ { + setQuery(e.target.value) + debouncedFilter(e.target.value) + }} + onKeyDown={e => { + if (e.key === 'Escape') { + table.setGlobalFilter(undefined) + store.setSearchOpen(false) + } + }} + placeholder="Search..." + /> +
+ ) +} +``` + +--- + +### Pagination + +Powered by TanStack Table's pagination: +- **Client-Side**: `getPaginationRowModel()` handles slicing +- **Server-Side**: `manualPagination: true`, data provided per page +- **Cursor-Based**: Adapter handles cursor tokens, data swapped per page + +```typescript +interface PaginationConfig { + enabled: boolean + type: 'offset' | 'cursor' + pageSize: number // default: 50 + pageSizeOptions?: number[] // default: [25, 50, 100] + onPageChange?: (page: number) => void + onPageSizeChange?: (pageSize: number) => void +} +``` + +**Keyboard interaction with pagination**: +- `PageUp` / `PageDown` move focus within the current visible window +- When focus reaches the edge of the current page (in paginated mode), it does NOT auto-advance to the next page +- Page navigation is done via the pagination controls or programmatically + +--- + +### Grouping + +Powered by TanStack Table's grouping + expanded row model: +- **Header Grouping**: Multi-level column groups via `getHeaderGroups()` +- **Data Grouping**: `enableGrouping` + `getGroupedRowModel()` +- **Aggregation**: TanStack Table aggregation functions (count, sum, avg, etc.) +- **Expand/Collapse**: `row.toggleExpanded()`, keyboard: `ArrowRight` to expand, `ArrowLeft` to collapse + +--- + +### Column Pinning + +Powered by TanStack Table's column pinning: +```typescript +// TanStack Table state +columnPinning: { + left: ['_selection', 'id'], // pinned left columns + right: ['actions'], // pinned right columns +} +``` + +Rendering uses `table.getLeftHeaderGroups()`, `table.getCenterHeaderGroups()`, `table.getRightHeaderGroups()` for separate sticky regions. + +--- + +## Architectural Patterns from Gridler to Adopt + +### 1. Zustand Store Pattern +For UI state not managed by TanStack Table: +```typescript +const { Provider, useGriddyStore } = createSyncStore( + (set, get) => ({ + focusedRowIndex: null, + isEditing: false, + isSearchOpen: false, + isSelecting: false, + setFocusedRow: (index) => set({ focusedRowIndex: index }), + setEditing: (editing) => set({ isEditing: editing }), + setSearchOpen: (open) => set({ isSearchOpen: open }), + setSelecting: (selecting) => set({ isSelecting: selecting }), + moveFocus: (direction, amount) => set(state => { + const current = state.focusedRowIndex ?? 0 + const delta = direction === 'down' ? amount : -amount + return { focusedRowIndex: Math.max(0, Math.min(current + delta, get().totalRows - 1)) } + }), + moveFocusToStart: () => set({ focusedRowIndex: 0 }), + moveFocusToEnd: () => set(state => ({ focusedRowIndex: get().totalRows - 1 })), + }) +) +``` + +### 2. Data Adapter Pattern +Adapters feed data into TanStack Table: +```typescript +// LocalDataAdapter: passes array directly to table +// RemoteServerAdapter: fetches data, manages loading state, handles pagination callbacks +// CustomAdapter: user-defined data fetching +``` + +### 3. Event System +CustomEvent for inter-component communication (same as Gridler): +```typescript +state._events.dispatchEvent(new CustomEvent('loadPage', { detail })) +state._events.addEventListener('reload', handler) +``` + +### 4. Ref-Based Imperative API +```typescript +interface GriddyRef { + getState: () => GriddyUIState + getTable: () => Table // TanStack Table instance + getVirtualizer: () => Virtualizer // TanStack Virtual instance + refresh: () => Promise + scrollToRow: (id: string) => void + selectRow: (id: string) => void + deselectAll: () => void + focusRow: (index: number) => void + startEditing: (rowId: string, columnId?: string) => void +} +``` + +### 5. Persistence Layer +```typescript +persist={{ + name: `Griddy_${props.persistenceKey}`, + partialize: (s) => ({ + columnOrder: s.columnOrder, + columnSizing: s.columnSizing, + columnVisibility: s.columnVisibility, + sorting: s.sorting, + }), + version: 1, +}} +``` + +--- + +## Implementation Phases + +### Phase 1: Core Foundation + TanStack Table +- [ ] Set up Griddy package structure +- [ ] Install `@tanstack/react-table` as dependency +- [ ] Create core types: `GriddyColumn`, `GriddyProps`, `SelectionConfig`, etc. +- [ ] Implement `columnMapper.ts` (GriddyColumn → ColumnDef) +- [ ] Implement `Griddy.tsx` with `useReactTable()` setup +- [ ] Implement basic HTML table rendering with `flexRender()` +- [ ] Implement `GriddyProvider.tsx` with context for table instance +- [ ] Implement `GriddyStore.ts` (Zustand store for UI state) +- [ ] Implement `LocalDataAdapter` +- [ ] Add Storybook stories for basic table + +**Deliverable**: Functional table rendering with TanStack Table powering the data model + +### Phase 2: Virtualization + Keyboard Navigation +- [ ] Integrate TanStack Virtual (`useVirtualizer`) with TanStack Table row model +- [ ] Implement `VirtualBody.tsx` with virtual row rendering +- [ ] Implement `TableHeader.tsx` with sticky headers +- [ ] Implement `useKeyboardNavigation.ts` with full key bindings: + - Arrow Up/Down for row focus + - Page Up/Down for page-sized jumps + - Home/End for first/last row + - Auto-scroll focused row into view +- [ ] Implement focused row visual indicator (CSS) +- [ ] Performance optimization (memo, useMemo) +- [ ] Test with large datasets (10k+ rows) +- [ ] Add column resizing via TanStack Table + +**Deliverable**: High-performance virtualized table with full keyboard navigation + +### Phase 3: Row Selection +- [ ] Implement single selection mode via TanStack Table `enableRowSelection` +- [ ] Implement multi selection mode via TanStack Table `enableMultiRowSelection` +- [ ] Implement `SelectionCheckbox.tsx` (auto-prepended column) +- [ ] Keyboard selection: + - Space to toggle + - Shift+Arrow to extend (multi) + - Ctrl+A to select all (multi) + - Ctrl+S to toggle selection mode + - Escape to clear selection +- [ ] Click-to-select and Shift+Click range selection +- [ ] Selection persistence across sort/filter +- [ ] `onSelectionChange` callback with selected rows + +**Deliverable**: Full row selection with single and multi modes, keyboard support + +### Phase 4: Search +- [ ] Implement `SearchOverlay.tsx` (Ctrl+F activated) +- [ ] Wire global filter to TanStack Table `setGlobalFilter()` +- [ ] Implement search highlighting in cell renderer +- [ ] Search navigation (Enter/Shift+Enter through matches) +- [ ] Debounced input +- [ ] Escape to close and clear + +**Deliverable**: Global search with keyboard-activated overlay + +### Phase 5: Sorting & Filtering +- [ ] Sorting via TanStack Table (click header, Shift+Click for multi) +- [ ] Sort indicators in headers +- [ ] Column filtering UI (per-column filter dropdowns) +- [ ] Filter operators (contains, exact, between, etc.) +- [ ] Server-side sort/filter support (`manualSorting`, `manualFiltering`) +- [ ] Sort/filter state persistence + +**Deliverable**: Data manipulation features powered by TanStack Table + +### Phase 6: In-Place Editing +- [ ] Implement `EditableCell.tsx` with editor mounting +- [ ] Implement built-in editors: Text, Numeric, Date, Select, Checkbox +- [ ] Keyboard editing: + - Ctrl+E or Enter to start editing + - Tab/Shift+Tab between editable cells + - Enter to commit + move to next row + - Escape to cancel +- [ ] Validation system +- [ ] `onEditCommit` callback +- [ ] Undo/redo (optional) + +**Deliverable**: Full in-place editing with keyboard support + +### Phase 7: Pagination & Data Adapters +- [ ] Client-side pagination via TanStack Table `getPaginationRowModel()` +- [ ] Pagination controls UI (page nav, page size selector) +- [ ] Implement `RemoteServerAdapter` with cursor + offset support +- [ ] Loading states and error handling +- [ ] Infinite scroll pattern (optional) + +**Deliverable**: Pagination and remote data support + +### Phase 8: Advanced Features +- [ ] Header grouping via TanStack Table `getHeaderGroups()` +- [ ] Data grouping via TanStack Table `getGroupedRowModel()` +- [ ] Column pinning via TanStack Table `columnPinning` +- [ ] Column reordering (drag-and-drop + TanStack `columnOrder`) +- [ ] Column hiding (TanStack `columnVisibility`) +- [ ] Export to CSV + +**Deliverable**: Advanced table features + +### Phase 9: Polish & Documentation +- [ ] Comprehensive Storybook stories +- [ ] API documentation +- [ ] TypeScript definitions and examples +- [ ] Integration examples +- [ ] Performance benchmarks +- [ ] ARIA attributes and screen reader compatibility +- [ ] Theme system (CSS variables) + +**Deliverable**: Production-ready component + +--- + +## Technology Stack + +- **React**: 19.x (peer dependency) +- **TypeScript**: 5.9+ +- **Table Model**: @tanstack/react-table 8.x +- **Virtualization**: @tanstack/react-virtual 3.13+ +- **State Management**: Zustand (for UI state beyond TanStack Table) +- **Styling**: CSS Modules + CSS custom properties for theming +- **Testing**: Vitest, React Testing Library +- **Build**: Vite with TypeScript declaration generation + +--- + +## Dependencies + +**Peer Dependencies**: +- react >= 19.0.0 +- react-dom >= 19.0.0 +- @tanstack/react-table >= 8.0.0 +- @tanstack/react-virtual >= 3.13.0 + +**Optional Peer Dependencies**: +- @mantine/core (for integrated theming) +- @tanstack/react-query (for server-side data) + +**Dev Dependencies**: +- Same as Oranguru project (Vitest, Storybook, etc.) + +--- + +## Testing Strategy + +1. **Unit Tests**: + - Column mapper (GriddyColumn → ColumnDef) + - Keyboard navigation logic + - Selection state management + - Editor components + - Filter/Sort operators + - Data adapter functions + +2. **Integration Tests**: + - TanStack Table + Virtual rendering together + - Keyboard navigation with virtualized rows + - Selection with sorting/filtering active + - Editing with validation + - Search with highlighting + +3. **Performance Tests**: + - Rendering 10k+ rows (virtual) + - Filtering/sorting on large datasets (TanStack Table performance) + - Keyboard navigation speed with 100k rows + - Memory usage monitoring + +4. **Accessibility Tests**: + - Full keyboard navigation (all documented bindings) + - ARIA roles: `grid`, `row`, `gridcell`, `columnheader` + - `aria-selected` on selected rows + - `aria-activedescendant` for focused row + - Screen reader announcement of sort/filter state + +--- + +## ARIA / Accessibility + +The grid follows WAI-ARIA grid pattern: + +```html +
+
+
+
Name
+
Email
+
+
+
+
+
John
+
john@example.com
+
+
+
+``` + +--- + +## Browser Support + +- Chrome/Edge: Latest 2 versions +- Firefox: Latest 2 versions +- Safari: Latest 2 versions +- No IE11 support (modern React requirement) + +--- + +## Success Criteria + +1. Table renders 10k+ rows smoothly (60fps) via TanStack Virtual +2. All table logic (sort, filter, select, paginate) handled by TanStack Table +3. Full keyboard navigation: Arrow keys, PageUp/Down, Home/End, Ctrl+F, Ctrl+E/Enter, Ctrl+S +4. Single and multi row selection with keyboard and mouse +5. TypeScript types cover 95%+ of code +6. Bundle size < 50KB (gzipped, excluding peer deps) +7. Full keyboard accessibility (WCAG AA) +8. All features tested with >80% coverage + +--- + +## Known Limitations (Phase 1) + +- Column virtualization: Deferred (TanStack Virtual supports it, but not needed initially) +- Tree/hierarchical data: Planned for later +- Copy/paste: Planned for later +- Rich text/HTML content: Not planned +- Master-detail/expandable rows: Planned for later +- Cell-level focus (left/right arrow between cells): Planned for later; Phase 1 focuses on row-level navigation + +--- + ## Next Steps 1. Review and approve this plan -2. Create project directory structure -3. Set up initial TypeScript types and interfaces -4. Begin Phase 1 implementation -5. Create Storybook stories alongside implementation +2. Install `@tanstack/react-table` dependency +3. Create project directory structure +4. Set up core types and column mapper +5. Begin Phase 1 implementation +6. Create Storybook stories alongside implementation