import type { Table } from '@tanstack/react-table'; import type { ColumnFiltersState, ColumnPinningState, RowSelectionState, SortingState, } from '@tanstack/react-table'; import type { Virtualizer } from '@tanstack/react-virtual'; import { createSyncStore } from '@warkypublic/zustandsyncstore'; import type { AdvancedSearchConfig, DataAdapter, GriddyColumn, GriddyProps, GriddyUIState, GroupingConfig, InfiniteScrollConfig, PaginationConfig, SearchConfig, SelectionConfig, TreeConfig, } from './types'; // ─── Store State ───────────────────────────────────────────────────────────── /** * Full store state: UI state + synced props + internal references. * Props from GriddyProps are synced automatically via createSyncStore's $sync. * Fields from GriddyProps must be declared here so TypeScript can see them. */ export interface GriddyStoreState extends GriddyUIState { _scrollRef: HTMLDivElement | null; // ─── Internal refs (set imperatively) ─── _table: null | Table; _virtualizer: null | Virtualizer; advancedSearch?: AdvancedSearchConfig; // ─── Adapter Actions ─── appendData: (data: any[]) => void; className?: string; columnFilters?: ColumnFiltersState; columnPinning?: ColumnPinningState; columns?: GriddyColumn[]; data?: any[]; dataAdapter?: DataAdapter; dataCount?: number; // ─── Error State ─── error: Error | null; exportFilename?: string; filterPresets?: boolean; getRowId?: (row: any, index: number) => string; grouping?: GroupingConfig; height?: number | string; infiniteScroll?: InfiniteScrollConfig; isLoading?: boolean; keyboardNavigation?: boolean; manualFiltering?: boolean; manualSorting?: boolean; onColumnFiltersChange?: (filters: ColumnFiltersState) => void; onColumnPinningChange?: (pinning: ColumnPinningState) => void; onEditCommit?: (rowId: string, columnId: string, value: unknown) => Promise | void; onError?: (error: Error) => void; onRowSelectionChange?: (selection: RowSelectionState) => void; onSortingChange?: (sorting: SortingState) => void; overscan?: number; pagination?: PaginationConfig; paginationState?: { pageIndex: number; pageSize: number }; persistenceKey?: string; rowHeight?: number; rowSelection?: RowSelectionState; search?: SearchConfig; selection?: SelectionConfig; setData: (data: any[]) => void; setDataCount: (count: number) => void; setError: (error: Error | null) => void; setInfiniteScroll: (config: InfiniteScrollConfig | undefined) => void; setIsLoading: (loading: boolean) => void; setPaginationState: (state: { pageIndex: number; pageSize: number }) => void; setScrollRef: (el: HTMLDivElement | null) => void; // ─── Internal ref setters ─── setTable: (table: Table) => void; setTreeChildrenCache: (nodeId: string, children: any[]) => void; setTreeLoadingNode: (nodeId: string, loading: boolean) => void; setVirtualizer: (virtualizer: Virtualizer) => void; showToolbar?: boolean; sorting?: SortingState; // ─── Tree/Hierarchical Data ─── tree?: TreeConfig; treeChildrenCache: Map; treeLoadingNodes: Set; // ─── Synced from GriddyProps (written by $sync) ─── uniqueId?: string; } // ─── Create Store ──────────────────────────────────────────────────────────── export const { Provider: GriddyProvider, useStore: useGriddyStore } = createSyncStore< GriddyStoreState, GriddyProps >((set, get) => ({ _scrollRef: null, // ─── Internal Refs ─── _table: null, _virtualizer: null, appendData: (data) => set((state) => ({ data: [...(state.data ?? []), ...data] })), error: null, focusedColumnId: null, // ─── Focus State ─── focusedRowIndex: null, // ─── Mode State ─── isEditing: false, isSearchOpen: false, isSelecting: false, moveFocus: (direction, amount) => { const { focusedRowIndex, totalRows } = get(); const current = focusedRowIndex ?? 0; const delta = direction === 'down' ? amount : -amount; const next = Math.max(0, Math.min(current + delta, totalRows - 1)); set({ focusedRowIndex: next }); }, moveFocusToEnd: () => { const { totalRows } = get(); set({ focusedRowIndex: Math.max(0, totalRows - 1) }); }, moveFocusToStart: () => set({ focusedRowIndex: 0 }), setData: (data) => set({ data }), setDataCount: (count) => set({ dataCount: count }), setEditing: (editing) => set({ isEditing: editing }), setError: (error) => set({ error }), setFocusedColumn: (id) => set({ focusedColumnId: id }), // ─── Actions ─── setFocusedRow: (index) => set({ focusedRowIndex: index }), setInfiniteScroll: (config) => set({ infiniteScroll: config }), setIsLoading: (loading) => set({ isLoading: loading }), setPaginationState: (state) => set({ paginationState: state }), setScrollRef: (el) => set({ _scrollRef: el }), setSearchOpen: (open) => set({ isSearchOpen: open }), setSelecting: (selecting) => set({ isSelecting: selecting }), // ─── Internal Ref Setters ─── setTable: (table) => set({ _table: table }), setTotalRows: (count) => set({ totalRows: count }), setTreeChildrenCache: (nodeId, children) => set((state) => { const newMap = new Map(state.treeChildrenCache); newMap.set(nodeId, children); return { treeChildrenCache: newMap }; }), setTreeLoadingNode: (nodeId, loading) => set((state) => { const newSet = new Set(state.treeLoadingNodes); if (loading) { newSet.add(nodeId); } else { newSet.delete(nodeId); } return { treeLoadingNodes: newSet }; }), setVirtualizer: (virtualizer) => set({ _virtualizer: virtualizer }), // ─── Row Count ─── totalRows: 0, treeChildrenCache: new Map(), // ─── Tree State ─── treeLoadingNodes: new Set(), }));