# 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 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. ## Read these Always have a look in llm/docs folder for info about the tools. Refer to your last context and update it. src/Griddy/CONTEXT.md --- ## Architecture & Core Design Principles ### 1. Core Philosophy - **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 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 - **Provider/Context Pattern**: Wraps grid in Provider for configuration and state sharing - **Column Definition System**: Type-safe, extensible column definitions with custom rendering - **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 Table + TanStack Virtual instead of Glide Data Grid** ### 2. How TanStack Table and TanStack Virtual Work Together ``` ┌─────────────────────────────────────────────────────┐ │ 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, 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 │ │ ├── useGridFiltering.ts # Wraps TanStack Table column filters │ │ ├── types.ts │ │ └── operators.ts │ ├── sorting/ │ │ ├── SortIndicator.tsx │ │ ├── useGridSorting.ts # Wraps TanStack Table sorting │ │ └── types.ts │ ├── search/ │ │ ├── SearchOverlay.tsx # Ctrl+F search overlay UI │ │ ├── useGridSearch.ts # Global filter via TanStack Table │ │ └── types.ts │ ├── editing/ │ │ ├── EditableCell.tsx │ │ ├── useGridEditing.ts │ │ ├── validation.ts │ │ └── types.ts │ ├── pagination/ │ │ ├── PaginationControl.tsx │ │ ├── useGridPagination.ts # Wraps TanStack Table pagination │ │ └── types.ts │ ├── grouping/ │ │ ├── GroupHeader.tsx │ │ ├── useGridGrouping.ts # Wraps TanStack Table grouping │ │ └── types.ts │ ├── pinning/ │ │ ├── useGridPinning.ts # Wraps TanStack Table column pinning │ │ └── types.ts │ ├── resizing/ │ │ ├── useGridResizing.ts # Wraps TanStack Table column resizing │ │ └── types.ts │ └── export/ │ └── exportCsv.ts ├── editors/ │ ├── TextEditor.tsx │ ├── NumericEditor.tsx │ ├── DateEditor.tsx │ ├── SelectEditor.tsx │ ├── CheckboxEditor.tsx │ └── index.ts ├── renderers/ │ ├── defaultRenderers.tsx │ ├── formatters.ts │ └── types.ts ├── adapters/ │ ├── LocalDataAdapter.ts │ ├── RemoteServerAdapter.ts │ ├── base.ts │ └── types.ts ├── hooks/ │ ├── useGriddy.ts # Main hook: composes table + virtualizer + store │ ├── useMergedRefs.ts │ ├── useAsync.ts │ └── index.ts ├── styles/ │ ├── griddy.module.css │ ├── themes.ts │ ├── variables.css │ └── reset.css ├── utils/ │ ├── merge.ts │ ├── classNames.ts │ ├── accessibility.ts │ └── index.ts ├── 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. createSyncStore Pattern (from @warkypublic/zustandsyncstore) Uses `createSyncStore` which provides a Provider that auto-syncs parent props into the Zustand store, plus a context-scoped `useStore` hook with selector support. `GriddyStoreState` includes both UI state AND synced prop fields (so TypeScript sees them): ```typescript const { Provider: GriddyProvider, useStore: useGriddyStore } = createSyncStore< GriddyStoreState, // UI state + prop fields + internal refs GriddyProps // Props synced from parent >( (set, get) => ({ // UI state focusedRowIndex: null, isEditing: false, isSearchOpen: false, isSelecting: false, // Internal refs _table: null, _virtualizer: null, // Actions setFocusedRow: (index) => set({ focusedRowIndex: index }), moveFocus: (direction, amount) => { ... }, setTable: (table) => set({ _table: table }), ... }) ) // Usage: // All props (data, columns, selection, etc.) are available via useGriddyStore((s) => s.data) ``` ### 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 - [x] Sorting via TanStack Table (click header, Shift+Click for multi) - [x] Sort indicators in headers - [x] Column filtering UI (right-click context menu for sort/filter options) - [x] Filter operators (contains, equals, startsWith, endsWith, notContains, isEmpty, isNotEmpty, between, greaterThan, lessThan, includes, excludes, etc.) - [x] Text, number, enum, and boolean filter types - [x] Filter UI with operator dropdown and type-specific inputs - [x] Filter status indicators (blue/gray icons in headers) - [x] Debounced text input (300ms) - [x] Apply/Clear buttons for filter controls - [x] Date filtering (Phase 5.5 - COMPLETE with @mantine/dates) - [x] Server-side sort/filter support (`manualSorting`, `manualFiltering`) - COMPLETE - [ ] Sort/filter state persistence **Deliverable**: Complete data manipulation features powered by TanStack Table **Files Created** (9 components): - `src/Griddy/features/filtering/types.ts` — Filter type system - `src/Griddy/features/filtering/operators.ts` — Operator definitions for all 4 types - `src/Griddy/features/filtering/filterFunctions.ts` — TanStack FilterFn implementations - `src/Griddy/features/filtering/FilterInput.tsx` — Text/number input with debouncing - `src/Griddy/features/filtering/FilterSelect.tsx` — Multi-select for enums - `src/Griddy/features/filtering/FilterBoolean.tsx` — Radio group for booleans - `src/Griddy/features/filtering/ColumnFilterButton.tsx` — Filter status icon - `src/Griddy/features/filtering/ColumnFilterPopover.tsx` — Filter UI popover - `src/Griddy/features/filtering/ColumnFilterContextMenu.tsx` — Right-click context menu **Files Modified**: - `src/Griddy/rendering/TableHeader.tsx` — Integrated context menu + filter popover - `src/Griddy/core/columnMapper.ts` — Set default filterFn for filterable columns - `src/Griddy/core/types.ts` — Added FilterConfig to GriddyColumn - `src/Griddy/core/constants.ts` — Added CSS class names and defaults - `src/Griddy/styles/griddy.module.css` — Filter UI styling - `src/Griddy/Griddy.stories.tsx` — Added 6 filtering examples **Tests**: - `playwright.config.ts` — Playwright configuration - `tests/e2e/filtering-context-menu.spec.ts` — 8 comprehensive E2E test cases ### Phase 6: In-Place Editing - [x] Implement `EditableCell.tsx` with editor mounting - [x] Implement built-in editors: Text, Numeric, Date, Select, Checkbox - [x] Keyboard editing: - Ctrl+E or Enter to start editing - Tab/Shift+Tab between editable cells (partial - editors handle Tab) - Enter to commit - Escape to cancel - [x] `onEditCommit` callback - [x] Double-click to edit - [x] Editor types: text, number, date, select, checkbox - [ ] Validation system (deferred) - [ ] Tab to next editable cell navigation (deferred) - [ ] Undo/redo (optional, deferred) **Deliverable**: Full in-place editing with keyboard support - COMPLETE ✅ ### Phase 7: Pagination & Data Adapters - [x] Client-side pagination via TanStack Table `getPaginationRowModel()` - [x] Pagination controls UI (page nav, page size selector) - [x] Server-side pagination callbacks (`onPageChange`, `onPageSizeChange`) - [x] Page navigation controls (first, previous, next, last) - [x] Page size selector dropdown - [x] Storybook stories (client-side + server-side) - [ ] Implement `RemoteServerAdapter` with cursor + offset support (deferred - callbacks sufficient) - [ ] Loading states UI (deferred - handled externally) - [ ] Infinite scroll pattern (optional, deferred) **Deliverable**: Pagination and remote data support - COMPLETE ✅ ### Phase 8: Advanced Features - [x] Column hiding/visibility (TanStack `columnVisibility`) - COMPLETE - [x] Export to CSV - COMPLETE - [x] Toolbar component (column visibility + export) - COMPLETE - [x] Column pinning via TanStack Table `columnPinning` ✅ - [x] Header grouping via TanStack Table `getHeaderGroups()` ✅ - [x] Data grouping via TanStack Table `getGroupedRowModel()` ✅ - [x] Column reordering (drag-and-drop + TanStack `columnOrder`) ✅ **Deliverable**: Advanced table features - PARTIAL ✅ (core features complete) ### Phase 9: Polish & Documentation - [x] Comprehensive Storybook stories (15+ stories covering all features) - [x] API documentation (README.md with full API reference) - [x] TypeScript definitions and examples (EXAMPLES.md) - [x] Integration examples (server-side, custom renderers, etc.) - [x] Theme system documentation (THEME.md with CSS variables) - [x] ARIA attributes (grid, row, gridcell, aria-selected, aria-activedescendant) - [ ] Performance benchmarks (deferred - already tested with 10k rows) **Deliverable**: Production-ready component - COMPLETE ✅ --- ## 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 --- --- ## Phase 10: Future Enhancements ### Bug Fixes - [ ] **Unique Row ID** - Add a unique row ID to the data if no uniqueId is provided. - [ ] **Tree row selection** - The tree row selection breaks, it selects the same item. Suspect the uniqueId - [ ] **Infinite scroll** - Header Spec infinite scroll server side filtering and sorting not working ### Data & State Management - [ ] **Column layout persistence** - Save/restore column order, widths, visibility to localStorage - [ ] **Sort/filter state persistence** - Persist column filters and sorting state - [ ] **Undo/redo for edits** - Ctrl+Z/Ctrl+Y for edit history with state snapshots - [ ] **RemoteServerAdapter class** - Formal adapter pattern for server data (currently using callbacks) - [x] **Error boundary** - Graceful error handling with retry (GriddyErrorBoundary, onError/onRetry props) ✅ - [x] **Loading states UI** - Skeleton rows with shimmer + translucent overlay spinner (isLoading prop) ✅ ### Advanced Data Features - [ ] **Master-detail rows** - Expandable detail panels per row with custom content - [ ] **Bulk operations** - Multi-row edit, bulk delete with confirmation - [ ] **Smart column types** - Auto-detect date, number, email columns from data - [ ] **Copy/paste support** - Clipboard integration (Ctrl+C/Ctrl+V) for cells and rows ### Tree/hierarchical data - [x] **Tree Structure Column** - Parent-child rows with expand/collapse (nested data structures) ✅ - [x] **On Demand Expand** - Lazy loading with getChildren callback ✅ - [x] **On Search Callback** - Auto-expand parent nodes when search matches children ✅ - [x] **Adaptor Integration** - Lazy tree expansion integrated with data transformations ✅ ### Editing Enhancements - [ ] **Validation system** - Validate edits before commit (min/max, regex, custom validators) - [ ] **Tab-to-next-editable-cell** - Navigate between editable cells with Tab key - [ ] **Inline validation feedback** - Show validation errors in edit mode - [x] **Custom cell renderers** - ProgressBar, Badge, Image, Sparkline renderers via `renderer` + `rendererMeta` ✅ ### Filtering & Search - [x] **Quick filters** - Checkbox list of unique values in filter popover (`filterConfig.quickFilter: true`) ✅ - [x] **Advanced search** - Multi-condition search with AND/OR/NOT operators (AdvancedSearchPanel) ✅ - [x] **Filter presets** - Save/load/delete named filter presets to localStorage (FilterPresetsMenu) ✅ - [x] **Search history** - Recent searches dropdown with localStorage persistence (SearchHistoryDropdown) ✅ ### Export & Import - [ ] **Export to CSV/Excel** - Download current view with filters/sorts applied (load all data) - [ ] **Export selected rows** - Export only selected rows - [ ] **Import from CSV** - Bulk data import with validation - [ ] **PDF export** - Generate PDF reports from grid data ### UI/UX Improvements - [ ] **Context menu enhancements** - Right-click menu for pin/hide/group/freeze operations - [ ] **Keyboard shortcuts help** - Modal overlay showing available shortcuts (Ctrl+?) - [ ] **Column auto-sizing** - Double-click resize handle to fit content - [ ] **Mobile/touch support** - Touch gestures for scroll, select, swipe actions - [ ] **Responsive columns** - Hide/show columns based on viewport width - [ ] **Theme presets** - Built-in light/dark/high-contrast themes ### Performance & Optimization - [ ] **Column virtualization** - Horizontal virtualization for 100+ columns - [ ] **Row virtualization improvements** - Variable row heights, smoother scrolling - [ ] **Performance benchmarks** - Document render time, memory usage, FPS - [ ] **Lazy loading images** - Load images as rows scroll into view - [ ] **Web Worker support** - Offload sorting/filtering to background thread ### Accessibility & Testing - [ ] **Accessibility improvements** - Enhanced ARIA roles, screen reader announcements - [ ] **Accessibility audit** - WCAG 2.1 AA compliance verification - [x] **E2E test suite** - 34 Playwright tests: 8 filtering + 26 Phase 10 feature tests, all passing ✅ - [ ] **Visual regression tests** - Screenshot comparison tests - [ ] **Performance tests** - Automated performance benchmarking ### Developer Experience - [ ] **Plugin architecture** - Extensibility system for custom features - [ ] **Custom hooks** - useGriddyTable, useGriddySelection, useGriddyFilters - [ ] **TypeDoc documentation** - Auto-generated API docs - [ ] **Migration guide** - Gridler → Griddy migration documentation - [ ] **CodeSandbox examples** - Live playground with all features - [ ] **Storybook controls** - Interactive prop controls for all stories ### Advanced Features - [ ] **Cell-level focus** - Left/right arrow navigation between cells - [ ] **Row reordering** - Drag-and-drop to reorder rows - [ ] **Frozen rows** - Pin specific rows at top/bottom - [ ] **Column spanning** - Cells that span multiple columns - [ ] **Row spanning** - Cells that span multiple rows - [ ] **Conditional formatting** - Highlight cells based on rules - [ ] **Formulas** - Excel-like formulas for calculated columns - [ ] **Real-time collaboration** - Multiple users editing simultaneously --- ## Implementation Priority **High Priority** (Next): 1. Column layout persistence 2. Validation system for editors 3. Tab-to-next-editable-cell navigation 4. Context menu enhancements **Medium Priority**: 1. Tree/hierarchical data 2. Master-detail rows 3. Export enhancements (selected rows, Excel format) 4. Keyboard shortcuts help overlay 5. Copy/paste support **Low Priority** (Nice to have): 1. Mobile/touch support 2. Plugin architecture 3. Undo/redo 4. Real-time collaboration 5. Column/row spanning --- ## Completed Milestones 1. ✅ Phase 1-9: Core, virtualization, selection, search, filtering, editing, pagination, advanced features, polish 2. ✅ Phase 7.5: Infinite scroll 3. ✅ Phase 8 completion: Column pinning, header grouping, data grouping, column reordering 4. ✅ Phase 10 batch 1 (7 features): Error boundary, loading states, custom renderers, quick filters, advanced search, filter presets, search history 5. ✅ E2E test suite: 34 Playwright tests (all passing) 6. ✅ Tree/Hierarchical Data: Full tree support with nested/flat/lazy modes, keyboard navigation, search auto-expand ## Next Steps 1. Choose remaining Phase 10 features based on user needs 2. Column layout persistence (highest priority remaining) 3. Validation system for editors 4. Update main package README with Griddy documentation