{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "data-table", "title": "Data Table (All Variants)", "description": "Generic data table with filtering, infinite scroll variant, and tree variant. Fully featured.", "dependencies": [ "date-fns", "@tanstack/react-query", "lucide-react", "@tanstack/react-table", "nuqs", "cmdk" ], "registryDependencies": [ "command", "input", "slider", "table", "accordion", "badge", "tooltip", "button", "dropdown-menu", "sheet", "skeleton", "popover", "checkbox", "separator", "calendar", "drawer" ], "files": [ { "path": "src/components/data-table/data-table.tsx", "content": "\"use client\";\r\n\r\n// REMINDER: React Compiler is not compatible with TanStack Table v8\r\n// https://github.com/TanStack/table/issues/5567\r\n\"use no memo\";\r\n\r\nimport {DataTableFilterCommand} from \"@/components/data-table/data-table-filter-command/index\";\r\nimport {DataTableFilterControls} from \"@/components/data-table/data-table-filter-controls\";\r\nimport {DataTablePagination} from \"@/components/data-table/data-table-pagination\";\r\nimport {DataTableProvider} from \"@/components/data-table/data-table-provider\";\r\nimport {DataTableToolbar} from \"@/components/data-table/data-table-toolbar\";\r\nimport type {DataTableFilterField,SheetField} from \"@/components/data-table/types\";\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from \"@/components/ui/table\";\r\nimport {useLocalStorage} from \"@/hooks/use-local-storage\";\r\nimport {getColumnVisibilityKey} from \"@/lib/constants/local-storage\";\r\nimport type {SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {FetchNextPageOptions,FetchPreviousPageOptions,RefetchOptions} from \"@tanstack/react-query\";\r\nimport type {\r\n ColumnDef,\r\n ColumnFiltersState,\r\n PaginationState,\r\n Row,\r\n RowSelectionState,\r\n SortingState,\r\n TableOptions,\r\n Table as TTable,\r\n VisibilityState,\r\n} from \"@tanstack/react-table\";\r\nimport {\r\n flexRender,\r\n getCoreRowModel,\r\n getFacetedMinMaxValues,\r\n getFacetedRowModel,\r\n getFacetedUniqueValues,\r\n getFilteredRowModel,\r\n getPaginationRowModel,\r\n getSortedRowModel,\r\n useReactTable,\r\n} from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\n\r\nexport interface DataTableProps {\r\n // ── Core ──────────────────────────────────────────────────────────────────\r\n data: TData[];\r\n columns: ColumnDef[];\r\n filterFields?: DataTableFilterField[];\r\n // BYOS — required so component is not coupled to a specific schema\r\n schema: SchemaDefinition;\r\n tableId: string;\r\n\r\n // ── State defaults ────────────────────────────────────────────────────────\r\n defaultColumnFilters?: ColumnFiltersState;\r\n defaultSorting?: SortingState;\r\n defaultColumnVisibility?: VisibilityState;\r\n defaultRowSelection?: RowSelectionState;\r\n defaultPagination?: PaginationState;\r\n\r\n // ── Row behaviour ─────────────────────────────────────────────────────────\r\n getRowId?: TableOptions[\"getRowId\"];\r\n getRowClassName?: (row: Row) => string;\r\n\r\n // ── Server-side facets ────────────────────────────────────────────────────\r\n getFacetedUniqueValues?: (\r\n table: TTable,\r\n columnId: string,\r\n ) => Map;\r\n getFacetedMinMaxValues?: (\r\n table: TTable,\r\n columnId: string,\r\n ) => [number,number]|undefined;\r\n\r\n // ── Column features ───────────────────────────────────────────────────────\r\n enableColumnOrdering?: boolean;\r\n enableColumnResizing?: boolean;\r\n\r\n // ── Loading / fetch state ─────────────────────────────────────────────────\r\n isLoading?: boolean;\r\n isFetching?: boolean;\r\n totalRows?: number;\r\n filterRows?: number;\r\n totalRowsFetched?: number;\r\n\r\n // ── Infinite scroll (optional — use DataTableInfinite for full support) ───\r\n hasNextPage?: boolean;\r\n fetchNextPage?: (options?: FetchNextPageOptions) => Promise;\r\n fetchPreviousPage?: (options?: FetchPreviousPageOptions) => Promise;\r\n refetch?: (options?: RefetchOptions) => void;\r\n\r\n // ── Sheet / detail panel ──────────────────────────────────────────────────\r\n sheetFields?: SheetField[];\r\n renderSheetTitle?: (props: {row?: Row}) => React.ReactNode;\r\n\r\n // ── Render slots ──────────────────────────────────────────────────────────\r\n /** Passed to DataTableToolbar — renders after the reset button and before view options */\r\n renderActions?: () => React.ReactNode;\r\n /** Renders below the toolbar, above the table (e.g. a chart) */\r\n renderChart?: () => React.ReactNode;\r\n /** Renders at the bottom of the sidebar (e.g. a footer) */\r\n renderSidebarFooter?: () => React.ReactNode;\r\n}\r\n\r\nexport function DataTable({\r\n columns,\r\n data,\r\n defaultColumnFilters=[],\r\n defaultSorting=[],\r\n defaultColumnVisibility={},\r\n defaultPagination={pageIndex: 0,pageSize: 10},\r\n filterFields=[],\r\n getFacetedUniqueValues: externalGetFacetedUniqueValues,\r\n getFacetedMinMaxValues: externalGetFacetedMinMaxValues,\r\n isLoading,\r\n schema,\r\n tableId,\r\n renderActions,\r\n renderChart,\r\n renderSidebarFooter,\r\n}: DataTableProps) {\r\n const [columnFilters,setColumnFilters]=\r\n React.useState(defaultColumnFilters);\r\n const [sorting,setSorting]=\r\n React.useState(defaultSorting);\r\n const [pagination,setPagination]=\r\n React.useState(defaultPagination);\r\n const [columnVisibility,setColumnVisibility]=\r\n useLocalStorage(\r\n getColumnVisibilityKey(tableId),\r\n defaultColumnVisibility,\r\n );\r\n\r\n // Reset pagination when filters change to avoid showing empty pages\r\n React.useEffect(() => {\r\n setPagination((prev) => ({...prev,pageIndex: 0}));\r\n },[columnFilters]);\r\n\r\n // Custom getFacetedUniqueValues that handles array column values\r\n const customGetFacetedUniqueValues=React.useCallback(\r\n (table: TTable,columnId: string) => () => {\r\n const facets=getFacetedUniqueValues()(table,columnId)();\r\n const customFacets=new Map();\r\n for (const [key,value] of facets as Map) {\r\n if (Array.isArray(key)) {\r\n for (const k of key) {\r\n customFacets.set(k,(customFacets.get(k)||0)+value);\r\n }\r\n } else {\r\n customFacets.set(key,(customFacets.get(key)||0)+value);\r\n }\r\n }\r\n return customFacets;\r\n },\r\n [],\r\n );\r\n\r\n const table=useReactTable({\r\n data,\r\n columns,\r\n state: {columnFilters,sorting,columnVisibility,pagination},\r\n onColumnVisibilityChange: setColumnVisibility,\r\n onColumnFiltersChange: setColumnFilters,\r\n onSortingChange: setSorting,\r\n onPaginationChange: setPagination,\r\n getSortedRowModel: getSortedRowModel(),\r\n getCoreRowModel: getCoreRowModel(),\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getFacetedRowModel: getFacetedRowModel(),\r\n getPaginationRowModel: getPaginationRowModel(),\r\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\r\n getFacetedUniqueValues: customGetFacetedUniqueValues,\r\n enableFilters: true,\r\n enableColumnFilters: true,\r\n });\r\n\r\n // Adapter signature for DataTableProvider\r\n const getFacetedUniqueValuesForProvider=React.useCallback(\r\n (table: TTable,columnId: string): Map => {\r\n // Prefer externally-provided (server-side) facets over computed ones\r\n if (externalGetFacetedUniqueValues) {\r\n return externalGetFacetedUniqueValues(table,columnId);\r\n }\r\n return customGetFacetedUniqueValues(table,columnId)();\r\n },\r\n [customGetFacetedUniqueValues,externalGetFacetedUniqueValues],\r\n );\r\n\r\n return (\r\n \r\n
\r\n \r\n \r\n {renderSidebarFooter?.()}\r\n
\r\n
\r\n \r\n {renderChart?.()}\r\n \r\n
\r\n \r\n \r\n {table.getHeaderGroups().map((headerGroup) => (\r\n \r\n {headerGroup.headers.map((header) => (\r\n \r\n {header.isPlaceholder\r\n ? null\r\n :flexRender(\r\n header.column.columnDef.header,\r\n header.getContext(),\r\n )}\r\n \r\n ))}\r\n \r\n ))}\r\n \r\n \r\n {table.getRowModel().rows?.length? (\r\n table.getRowModel().rows.map((row) => (\r\n \r\n {row.getVisibleCells().map((cell) => (\r\n \r\n {flexRender(\r\n cell.column.columnDef.cell,\r\n cell.getContext(),\r\n )}\r\n \r\n ))}\r\n \r\n ))\r\n ):(\r\n \r\n \r\n No results.\r\n \r\n \r\n )}\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table.tsx" }, { "path": "src/components/data-table/data-table-provider.tsx", "content": "import {DataTableStoreSync} from \"@/components/data-table/data-table-store-sync\";\r\nimport type {DataTableFilterField} from \"@/components/data-table/types\";\r\nimport {ControlsProvider} from \"@/providers/controls\";\r\nimport type {\r\n ColumnDef,\r\n ColumnFiltersState,\r\n PaginationState,\r\n RowSelectionState,\r\n SortingState,\r\n Table,\r\n VisibilityState,\r\n} from \"@tanstack/react-table\";\r\nimport {createContext,useContext,useMemo} from \"react\";\r\n\r\n// REMINDER: read about how to move controlled state out of the useReactTable hook\r\n// https://github.com/TanStack/table/discussions/4005#discussioncomment-7303569\r\n\r\ninterface DataTableStateContextType {\r\n columnFilters: ColumnFiltersState;\r\n sorting: SortingState;\r\n rowSelection: RowSelectionState;\r\n columnOrder: string[];\r\n columnVisibility: VisibilityState;\r\n pagination: PaginationState;\r\n enableColumnOrdering: boolean;\r\n}\r\n\r\ninterface DataTableBaseContextType {\r\n table: Table;\r\n filterFields: DataTableFilterField[];\r\n columns: ColumnDef[];\r\n isLoading?: boolean;\r\n getFacetedUniqueValues?: (\r\n table: Table,\r\n columnId: string,\r\n ) => Map;\r\n getFacetedMinMaxValues?: (\r\n table: Table,\r\n columnId: string,\r\n ) => undefined|[number,number];\r\n}\r\n\r\ninterface DataTableContextType\r\n extends DataTableStateContextType,\r\n DataTableBaseContextType { }\r\n\r\nexport const DataTableContext=createContext|null>(null);\r\n\r\nexport function DataTableProvider({\r\n children,\r\n ...props\r\n}: Partial&\r\n DataTableBaseContextType&{\r\n children: React.ReactNode;\r\n }) {\r\n const value=useMemo(\r\n // eslint-disable-next-line react-hooks/preserve-manual-memoization\r\n () => ({\r\n ...props,\r\n columnFilters: props.columnFilters??[],\r\n sorting: props.sorting??[],\r\n rowSelection: props.rowSelection??{},\r\n columnOrder: props.columnOrder??[],\r\n columnVisibility: props.columnVisibility??{},\r\n pagination: props.pagination??{pageIndex: 0,pageSize: 10},\r\n enableColumnOrdering: props.enableColumnOrdering??false,\r\n }),\r\n [\r\n props.columnFilters,\r\n props.sorting,\r\n props.rowSelection,\r\n props.columnOrder,\r\n props.columnVisibility,\r\n props.pagination,\r\n props.table,\r\n props.filterFields,\r\n props.columns,\r\n props.enableColumnOrdering,\r\n props.isLoading,\r\n props.getFacetedUniqueValues,\r\n props.getFacetedMinMaxValues,\r\n ],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n {children}\r\n \r\n \r\n );\r\n}\r\n\r\nexport function useDataTable() {\r\n const context=useContext(DataTableContext);\r\n\r\n if (!context) {\r\n throw new Error(\"useDataTable must be used within a DataTableProvider\");\r\n }\r\n\r\n return context as DataTableContextType;\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-provider.tsx" }, { "path": "src/components/data-table/data-table-toolbar.tsx", "content": "\"use client\";\r\n\r\nimport {Kbd} from \"@/components/custom/kbd\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {Button} from \"@/components/ui/button\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport {useHotKey} from \"@/hooks/use-hot-key\";\r\nimport {formatCompactNumber} from \"@/lib/format\";\r\nimport {useControls} from \"@/providers/controls\";\r\nimport {PanelLeftClose,PanelLeftOpen} from \"lucide-react\";\r\nimport {useMemo} from \"react\";\r\nimport {DataTableFilterControlsDrawer} from \"./data-table-filter-controls-drawer\";\r\nimport {DataTableResetButton} from \"./data-table-reset-button\";\r\nimport {DataTableViewOptions} from \"./data-table-view-options\";\r\n\r\ninterface DataTableToolbarProps {\r\n renderActions?: () => React.ReactNode;\r\n}\r\n\r\nexport function DataTableToolbar({renderActions}: DataTableToolbarProps) {\r\n const {table}=useDataTable();\r\n const {open,setOpen}=useControls();\r\n useHotKey(() => setOpen((prev) => !prev),\"b\");\r\n const filters=table.getState().columnFilters;\r\n\r\n const rows=useMemo(\r\n () => ({\r\n total: table.getCoreRowModel().rows.length,\r\n filtered: table.getFilteredRowModel().rows.length,\r\n }),\r\n [table],\r\n );\r\n\r\n return (\r\n
\r\n
\r\n \r\n \r\n \r\n setOpen((prev) => !prev)}\r\n className=\"hidden gap-2 sm:flex\"\r\n >\r\n {open? (\r\n <>\r\n \r\n Hide Controls\r\n \r\n ):(\r\n <>\r\n \r\n Show Controls\r\n \r\n )}\r\n \r\n \r\n \r\n

\r\n Toggle controls with{\" \"}\r\n \r\n \r\n B\r\n \r\n

\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n

\r\n \r\n {formatCompactNumber(rows.filtered)}\r\n {\" \"}\r\n of{\" \"}\r\n \r\n {formatCompactNumber(rows.total)}\r\n {\" \"}\r\n row(s) filtered\r\n

\r\n

\r\n \r\n {formatCompactNumber(rows.filtered)}\r\n {\" \"}\r\n row(s)\r\n

\r\n
\r\n
\r\n
\r\n {filters.length? :null}\r\n {renderActions?.()}\r\n \r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-toolbar.tsx" }, { "path": "src/components/data-table/data-table-pagination.tsx", "content": "\"use client\";\r\n\r\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n ChevronLeft,\r\n ChevronRight,\r\n ChevronsLeft,\r\n ChevronsRight,\r\n} from \"lucide-react\";\r\nimport { useMemo } from \"react\";\r\n\r\nexport function DataTablePagination() {\r\n const { table, pagination, columnFilters } = useDataTable();\r\n const pageCount = useMemo(\r\n () => table.getPageCount(),\r\n [columnFilters, pagination.pageSize, table],\r\n );\r\n\r\n return (\r\n
\r\n
\r\n

Rows per page

\r\n {\r\n table.setPageSize(Number(value));\r\n }}\r\n >\r\n \r\n \r\n \r\n \r\n {[10, 20, 30, 40, 50].map((pageSize) => (\r\n \r\n {pageSize}\r\n \r\n ))}\r\n \r\n \r\n
\r\n
\r\n Page {pagination.pageIndex + 1} of {pageCount}\r\n
\r\n
\r\n {\r\n table.setPageIndex(0);\r\n }}\r\n disabled={!table.getCanPreviousPage()}\r\n >\r\n Go to first page\r\n \r\n \r\n {\r\n table.previousPage();\r\n }}\r\n disabled={!table.getCanPreviousPage()}\r\n >\r\n Go to previous page\r\n \r\n \r\n {\r\n table.nextPage();\r\n }}\r\n disabled={!table.getCanNextPage()}\r\n >\r\n Go to next page\r\n \r\n \r\n {\r\n table.setPageIndex(table.getPageCount() - 1);\r\n }}\r\n disabled={!table.getCanNextPage()}\r\n >\r\n Go to last page\r\n \r\n \r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-pagination.tsx" }, { "path": "src/components/data-table/data-table-filter-controls.tsx", "content": "\"use client\";\r\n\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from \"@/components/custom/accordion\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {DataTableFilterCheckbox} from \"./data-table-filter-checkbox\";\r\nimport {DataTableFilterInput} from \"./data-table-filter-input\";\r\nimport {DataTableFilterResetButton} from \"./data-table-filter-reset-button\";\r\nimport {DataTableFilterSlider} from \"./data-table-filter-slider\";\r\nimport {DataTableFilterTimerange} from \"./data-table-filter-timerange\";\r\n\r\n// FIXME: use @container (especially for the slider element) to restructure elements\r\n\r\n// TODO: only pass the columns to generate the filters!\r\n// https://tanstack.com/table/v8/docs/framework/react/examples/filters\r\n\r\nexport function DataTableFilterControls() {\r\n const {filterFields}=useDataTable();\r\n return (\r\n defaultOpen)\r\n ?.map(({value}) => value as string)}\r\n >\r\n {filterFields?.map((field) => {\r\n const value=field.value as string;\r\n return (\r\n \r\n \r\n
\r\n
\r\n

{field.label}

\r\n {value!==field.label.toLowerCase()&&\r\n !field.commandDisabled? (\r\n

\r\n {value}\r\n

\r\n ):null}\r\n
\r\n \r\n
\r\n
\r\n \r\n {/* REMINDER: avoid the focus state to be cut due to overflow-hidden */}\r\n {/* REMINDER: need to move within here because of accordion height animation */}\r\n
\r\n {(() => {\r\n switch (field.type) {\r\n case \"checkbox\": {\r\n return ;\r\n }\r\n case \"slider\": {\r\n return ;\r\n }\r\n case \"input\": {\r\n return ;\r\n }\r\n case \"timerange\": {\r\n return ;\r\n }\r\n }\r\n })()}\r\n
\r\n
\r\n
\r\n );\r\n })}\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-controls.tsx" }, { "path": "src/components/data-table/data-table-filter-controls-drawer.tsx", "content": "import {Kbd} from \"@/components/custom/kbd\";\r\nimport {Button} from \"@/components/ui/button\";\r\nimport {\r\n Drawer,\r\n DrawerClose,\r\n DrawerContent,\r\n DrawerDescription,\r\n DrawerFooter,\r\n DrawerHeader,\r\n DrawerTitle,\r\n DrawerTrigger,\r\n} from \"@/components/ui/drawer\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport {useHotKey} from \"@/hooks/use-hot-key\";\r\nimport {useMediaQuery} from \"@/hooks/use-media-query\";\r\nimport {FilterIcon} from \"lucide-react\";\r\nimport React from \"react\";\r\nimport {DataTableFilterControls} from \"./data-table-filter-controls\";\r\n\r\nexport function DataTableFilterControlsDrawer() {\r\n const triggerButtonRef=React.useRef(null);\r\n const isMobile=useMediaQuery(\"(max-width: 640px)\");\r\n\r\n useHotKey(() => {\r\n triggerButtonRef.current?.click();\r\n },\"b\");\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

\r\n Toggle controls with{\" \"}\r\n \r\n \r\n B\r\n \r\n

\r\n
\r\n
\r\n
\r\n \r\n
\r\n \r\n Filters\r\n Adjust your table filters\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:block", "target": "components/data-grid/data-table-filter-controls-drawer.tsx" }, { "path": "src/components/data-table/data-table-filter-command/index.tsx", "content": "\"use client\";\r\n\r\nimport {Kbd} from \"@/components/custom/kbd\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n CommandSeparator,\r\n} from \"@/components/ui/command\";\r\nimport {Separator} from \"@/components/ui/separator\";\r\nimport {useHotKey} from \"@/hooks/use-hot-key\";\r\nimport {useLocalStorage} from \"@/hooks/use-local-storage\";\r\nimport {getCommandHistoryKey} from \"@/lib/constants/local-storage\";\r\nimport {formatCompactNumber} from \"@/lib/format\";\r\nimport type {SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport {formatDistanceToNow} from \"date-fns\";\r\nimport {LoaderCircle,Search,X} from \"lucide-react\";\r\nimport {useEffect,useMemo,useRef,useState} from \"react\";\r\nimport type {DataTableFilterField} from \"../types\";\r\nimport {\r\n columnFiltersParserFromSchema,\r\n getFieldOptions,\r\n getFilterValue,\r\n getWordByCaretPosition,\r\n replaceInputByFieldType,\r\n} from \"./utils\";\r\n\r\n// FIXME: there is an issue on cmdk if I wanna only set a single slider value...\r\n\r\ninterface DataTableFilterCommandProps {\r\n // Schema definition for parsing/serializing filter values (BYOS)\r\n schema: SchemaDefinition;\r\n // Unique ID for this table (used to namespace localStorage)\r\n tableId?: string;\r\n}\r\n\r\nexport function DataTableFilterCommand({\r\n schema,\r\n tableId=\"default\",\r\n}: DataTableFilterCommandProps) {\r\n const {\r\n table,\r\n isLoading,\r\n filterFields: _filterFields,\r\n getFacetedUniqueValues,\r\n }=useDataTable();\r\n const columnFilters=table.getState().columnFilters;\r\n const inputRef=useRef(null);\r\n const [open,setOpen]=useState(false);\r\n const [currentWord,setCurrentWord]=useState(\"\");\r\n // Guard to prevent effect cycle when serializing\r\n const isSerializingRef=useRef(false);\r\n const filterFields=useMemo(\r\n () => _filterFields?.filter((i) => !i.commandDisabled),\r\n [_filterFields],\r\n );\r\n const columnParser=useMemo(\r\n () => columnFiltersParserFromSchema({schema,filterFields}),\r\n [schema,filterFields],\r\n );\r\n const [inputValue,setInputValue]=useState(\r\n columnParser.serialize(columnFilters),\r\n );\r\n const [lastSearches,setLastSearches]=useLocalStorage<\r\n {\r\n search: string;\r\n timestamp: number;\r\n }[]\r\n >(getCommandHistoryKey(tableId),[]);\r\n\r\n useEffect(() => {\r\n // Skip if this update came from serialization (prevents infinite loop)\r\n if (isSerializingRef.current) {\r\n isSerializingRef.current=false;\r\n return;\r\n }\r\n // TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing\r\n if (currentWord!==\"\"&&open) return;\r\n // reset\r\n if (currentWord!==\"\"&&!open) setCurrentWord(\"\");\r\n // avoid recursion\r\n if (inputValue.trim()===\"\"&&!open) return;\r\n\r\n const searchParams=columnParser.parse(inputValue);\r\n\r\n const currentFilters=table.getState().columnFilters;\r\n const currentEnabledFilters=currentFilters.filter((filter) => {\r\n const field=_filterFields?.find((field) => field.value===filter.id);\r\n return !field?.commandDisabled;\r\n });\r\n\r\n for (const key of Object.keys(searchParams)) {\r\n const value=searchParams[key as keyof typeof searchParams];\r\n table.getColumn(key)?.setFilterValue(value);\r\n }\r\n const currentFiltersToReset=currentEnabledFilters.filter((filter) => {\r\n return !(filter.id in searchParams);\r\n });\r\n for (const filter of currentFiltersToReset) {\r\n table.getColumn(filter.id)?.setFilterValue(undefined);\r\n }\r\n\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n },[inputValue,open,currentWord]);\r\n\r\n useEffect(() => {\r\n // REMINDER: only update the input value if the command is closed (avoids jumps while open)\r\n if (!open) {\r\n // Set flag to prevent the parse effect from running after serialization\r\n isSerializingRef.current=true;\r\n setInputValue(columnParser.serialize(columnFilters));\r\n }\r\n },[columnFilters,filterFields,open,columnParser]);\r\n\r\n useHotKey(() => setOpen((open) => !open),\"k\");\r\n\r\n useEffect(() => {\r\n if (open) {\r\n inputRef?.current?.focus();\r\n }\r\n },[open]);\r\n\r\n return (\r\n
\r\n setOpen(true)}\r\n >\r\n {isLoading? (\r\n \r\n ):(\r\n \r\n )}\r\n \r\n {inputValue.trim()? (\r\n {inputValue}\r\n ):(\r\n Search data table...\r\n )}\r\n \r\n \r\n \r\n K\r\n \r\n \r\n div]:border-none\",\r\n open? \"visible\":\"hidden\",\r\n )}\r\n filter={(value,search,keywords) =>\r\n getFilterValue({value,search,keywords,currentWord})\r\n }\r\n // loop\r\n >\r\n {\r\n if (e.key===\"Escape\") inputRef?.current?.blur();\r\n }}\r\n onBlur={() => {\r\n setOpen(false);\r\n // FIXME: doesnt reflect the jumps\r\n // FIXME: will save non-existing searches\r\n // TODO: extract into function\r\n const search=inputValue.trim();\r\n if (!search) return;\r\n const timestamp=Date.now();\r\n const searchIndex=lastSearches.findIndex(\r\n (item) => item.search===search,\r\n );\r\n if (searchIndex!==-1) {\r\n lastSearches[searchIndex].timestamp=timestamp;\r\n setLastSearches(lastSearches);\r\n return;\r\n }\r\n setLastSearches([...lastSearches,{search,timestamp}]);\r\n return;\r\n }}\r\n onInput={(e) => {\r\n const caretPosition=e.currentTarget?.selectionStart||-1;\r\n const value=e.currentTarget?.value||\"\";\r\n const word=getWordByCaretPosition({value,caretPosition});\r\n setCurrentWord(word);\r\n }}\r\n placeholder=\"Search data table...\"\r\n className=\"text-foreground\"\r\n />\r\n
\r\n
\r\n {/* default height is 300px but in case of more, we'd like to tease the user */}\r\n \r\n \r\n {filterFields.map((field) => {\r\n if (typeof field.value!==\"string\") return null;\r\n if (inputValue.includes(`${field.value}:`)) return null;\r\n // TBD: should we handle this in the component?\r\n return (\r\n {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }}\r\n onSelect={(value) => {\r\n setInputValue((prev) => {\r\n if (currentWord.trim()===\"\") {\r\n const input=`${prev}${value}`;\r\n return `${input}:`;\r\n }\r\n // lots of cheat\r\n const isStarting=currentWord===prev;\r\n const prefix=isStarting? \"\":\" \";\r\n const input=prev.replace(\r\n `${prefix}${currentWord}`,\r\n `${prefix}${value}`,\r\n );\r\n return `${input}:`;\r\n });\r\n setCurrentWord(`${value}:`);\r\n }}\r\n className=\"group\"\r\n >\r\n {field.value}\r\n \r\n \r\n );\r\n })}\r\n \r\n \r\n \r\n {filterFields?.map((field) => {\r\n if (typeof field.value!==\"string\") return null;\r\n if (!currentWord.includes(`${field.value}:`)) return null;\r\n\r\n const column=table.getColumn(field.value);\r\n const facetedValue=\r\n getFacetedUniqueValues?.(table,field.value)||\r\n column?.getFacetedUniqueValues();\r\n\r\n const options=getFieldOptions({field});\r\n\r\n return options.map((optionValue) => {\r\n return (\r\n {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }}\r\n onSelect={(value) => {\r\n setInputValue((prev) =>\r\n replaceInputByFieldType({\r\n prev,\r\n currentWord,\r\n optionValue,\r\n value,\r\n field,\r\n }),\r\n );\r\n setCurrentWord(\"\");\r\n }}\r\n >\r\n {`${optionValue}`}\r\n {facetedValue?.has(optionValue)? (\r\n \r\n {formatCompactNumber(\r\n facetedValue.get(optionValue)||0,\r\n )}\r\n \r\n ):null}\r\n \r\n );\r\n });\r\n })}\r\n \r\n \r\n \r\n {lastSearches\r\n ?.sort((a,b) => b.timestamp-a.timestamp)\r\n .slice(0,5)\r\n .map((item) => {\r\n return (\r\n {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }}\r\n onSelect={(value) => {\r\n const search=value.replace(\"suggestion:\",\"\");\r\n setInputValue(`${search} `);\r\n setCurrentWord(\"\");\r\n }}\r\n className=\"group\"\r\n >\r\n {item.search}\r\n \r\n {formatDistanceToNow(item.timestamp,{\r\n addSuffix: true,\r\n })}\r\n \r\n {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }}\r\n onClick={(e) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n // TODO: extract into function\r\n setLastSearches(\r\n lastSearches.filter(\r\n (i) => i.search!==item.search,\r\n ),\r\n );\r\n }}\r\n className=\"ml-1 hidden rounded-md p-0.5 hover:bg-background group-aria-[selected=true]:block\"\r\n >\r\n \r\n \r\n \r\n );\r\n })}\r\n \r\n No results found.\r\n \r\n \r\n
\r\n \r\n Use {\" \"}\r\n to navigate\r\n \r\n \r\n Enter to query\r\n \r\n \r\n Esc to close\r\n \r\n \r\n \r\n Union: regions:a,b\r\n \r\n \r\n Range: p95:59-340\r\n \r\n \r\n Spaces: name:"a b"\r\n \r\n
\r\n {lastSearches.length? (\r\n {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n }}\r\n onClick={() => setLastSearches([])}\r\n >\r\n Clear suggestions\r\n \r\n ):null}\r\n
\r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n\r\n// function CommandItemType\r\n\r\nfunction CommandItemSuggestions({\r\n field,\r\n}: {\r\n field: DataTableFilterField;\r\n}) {\r\n const {table,getFacetedMinMaxValues,getFacetedUniqueValues}=\r\n useDataTable();\r\n const value=field.value as string;\r\n switch (field.type) {\r\n case \"checkbox\": {\r\n return (\r\n \r\n );\r\n }\r\n case \"slider\": {\r\n const [min,max]=getFacetedMinMaxValues?.(table,value)||[\r\n field.min,\r\n field.max,\r\n ];\r\n return (\r\n \r\n );\r\n }\r\n case \"input\": {\r\n return (\r\n \r\n );\r\n }\r\n default: {\r\n return null;\r\n }\r\n }\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-command/index.tsx" }, { "path": "src/components/data-table/data-table-filter-command/utils.ts", "content": "import {\r\n ARRAY_DELIMITER,\r\n RANGE_DELIMITER,\r\n SLIDER_DELIMITER,\r\n} from \"@/lib/delimiters\";\r\nimport {isArrayOfDates} from \"@/lib/is-array\";\r\nimport type {FieldBuilder,SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport type {ColumnFiltersState} from \"@tanstack/react-table\";\r\nimport type {DataTableFilterField} from \"../types\";\r\n\r\n/**\r\n * Extracts the word from the given string at the specified caret position.\r\n */\r\nexport function getWordByCaretPosition({\r\n value,\r\n caretPosition,\r\n}: {\r\n value: string;\r\n caretPosition: number;\r\n}) {\r\n let start=caretPosition;\r\n let end=caretPosition;\r\n\r\n while (start>0&&value[start-1]!==\" \") start--;\r\n while (end({\r\n prev,\r\n currentWord,\r\n optionValue,\r\n value,\r\n field,\r\n}: {\r\n prev: string;\r\n currentWord: string;\r\n optionValue?: string|number|boolean|undefined; // FIXME: use DataTableFilterField[\"options\"][number];\r\n value: string;\r\n field: DataTableFilterField;\r\n}) {\r\n switch (field.type) {\r\n case \"checkbox\": {\r\n if (currentWord.includes(ARRAY_DELIMITER)) {\r\n const words=currentWord.split(ARRAY_DELIMITER);\r\n words[words.length-1]=quoteIfNeeded(optionValue);\r\n const input=prev.replace(currentWord,words.join(ARRAY_DELIMITER));\r\n return `${input.trim()} `;\r\n }\r\n break;\r\n }\r\n case \"slider\": {\r\n if (currentWord.includes(SLIDER_DELIMITER)) {\r\n const words=currentWord.split(SLIDER_DELIMITER);\r\n words[words.length-1]=`${optionValue}`;\r\n const input=prev.replace(currentWord,words.join(SLIDER_DELIMITER));\r\n return `${input.trim()} `;\r\n }\r\n break;\r\n }\r\n case \"timerange\": {\r\n if (currentWord.includes(RANGE_DELIMITER)) {\r\n const words=currentWord.split(RANGE_DELIMITER);\r\n words[words.length-1]=`${optionValue}`;\r\n const input=prev.replace(currentWord,words.join(RANGE_DELIMITER));\r\n return `${input.trim()} `;\r\n }\r\n break;\r\n }\r\n default: {\r\n break;\r\n }\r\n }\r\n\r\n // Fallback / default behavior\r\n const quotedValue=quoteIfNeeded(optionValue)||value;\r\n const input=prev.replace(\r\n currentWord,\r\n `${String(field.value)}:${quotedValue}`,\r\n );\r\n return `${input.trim()} `;\r\n}\r\n\r\nexport function getFieldOptions({\r\n field,\r\n}: {\r\n field: DataTableFilterField;\r\n}) {\r\n switch (field.type) {\r\n case \"slider\": {\r\n return field.options?.length\r\n ? field.options\r\n .map(({value}) => value)\r\n .sort((a,b) => Number(a)-Number(b))\r\n .filter(notEmpty)\r\n :Array.from(\r\n {length: field.max-field.min+1},\r\n (_,i) => field.min+i,\r\n )||[];\r\n }\r\n default: {\r\n return field.options?.map(({value}) => value).filter(notEmpty)||[];\r\n }\r\n }\r\n}\r\n\r\nexport function getFilterValue({\r\n value,\r\n search,\r\n currentWord,\r\n}: {\r\n value: string;\r\n search: string;\r\n keywords?: string[]|undefined;\r\n currentWord: string;\r\n}): number {\r\n /**\r\n * @example value \"suggestion:public:true regions,ams,gru,fra\"\r\n */\r\n if (value.startsWith(\"suggestion:\")) {\r\n const rawValue=value.toLowerCase().replace(\"suggestion:\",\"\");\r\n if (rawValue.includes(search)) return 1;\r\n return 0;\r\n }\r\n\r\n /** */\r\n if (value.toLowerCase().includes(currentWord.toLowerCase())) return 1;\r\n\r\n /**\r\n * @example checkbox [filter, query] = [\"regions\", \"ams,gru,fra\"]\r\n * @example slider [filter, query] = [\"p95\", \"0-3000\"]\r\n * @example input [filter, query] = [\"name\", \"api\"]\r\n */\r\n const [filter,query]=currentWord.toLowerCase().split(\":\");\r\n if (query&&value.startsWith(`${filter}:`)) {\r\n if (query.includes(ARRAY_DELIMITER)) {\r\n /**\r\n * array of n elements\r\n * @example queries = [\"ams\", \"gru\", \"fra\"]\r\n */\r\n const queries=query.split(ARRAY_DELIMITER);\r\n const rawValue=value.toLowerCase().replace(`${filter}:`,\"\");\r\n if (\r\n queries.some((item,i) => item===rawValue&&i!==queries.length-1)\r\n )\r\n return 0;\r\n if (queries.some((item) => rawValue.includes(item))) return 1;\r\n }\r\n if (query.includes(SLIDER_DELIMITER)) {\r\n /**\r\n * range between 2 elements\r\n * @example queries = [\"0\", \"3000\"]\r\n */\r\n const queries=query.split(SLIDER_DELIMITER);\r\n const rawValue=value.toLowerCase().replace(`${filter}:`,\"\");\r\n\r\n const rawValueAsNumber=Number.parseInt(rawValue);\r\n const queryAsNumber=Number.parseInt(queries[0]);\r\n\r\n if (queryAsNumber({\r\n field,\r\n value,\r\n}: {\r\n field?: DataTableFilterField;\r\n value: unknown;\r\n}) {\r\n if (!field) return null;\r\n\r\n switch (field.type) {\r\n case \"slider\": {\r\n if (Array.isArray(value)) {\r\n return value.join(SLIDER_DELIMITER);\r\n }\r\n return value;\r\n }\r\n case \"checkbox\": {\r\n if (Array.isArray(value)) {\r\n return value.join(ARRAY_DELIMITER);\r\n }\r\n // REMINER: inversed logic\r\n if (typeof value===\"string\") {\r\n return value.split(ARRAY_DELIMITER);\r\n }\r\n return value;\r\n }\r\n case \"timerange\": {\r\n if (Array.isArray(value)) {\r\n if (isArrayOfDates(value)) {\r\n return value.map((date) => date.getTime()).join(RANGE_DELIMITER);\r\n }\r\n return value.join(RANGE_DELIMITER);\r\n }\r\n if (value instanceof Date) {\r\n return value.getTime();\r\n }\r\n return value;\r\n }\r\n default: {\r\n return value;\r\n }\r\n }\r\n}\r\n\r\nexport function notEmpty(\r\n value: TValue|null|undefined,\r\n): value is TValue {\r\n return value!==null&&value!==undefined;\r\n}\r\n\r\n/**\r\n * Tokenize input string, respecting quoted values\r\n *\r\n * Examples:\r\n * - `name:john regions:ams` → [[\"name\", \"john\"], [\"regions\", \"ams\"]]\r\n * - `name:\"john doe\" regions:ams` → [[\"name\", \"john doe\"], [\"regions\", \"ams\"]]\r\n * - `url:\"https://example.com/path with spaces\"` → [[\"url\", \"https://example.com/path with spaces\"]]\r\n */\r\nexport function tokenizeFilterInput(input: string): Array<[string,string]> {\r\n const results: Array<[string,string]>=[];\r\n const trimmed=input.trim();\r\n\r\n // Regex to match: key:\"quoted value\" or key:unquoted_value\r\n // This handles:\r\n // - key:\"value with spaces\"\r\n // - key:'value with spaces' (single quotes)\r\n // - key:valueWithoutSpaces\r\n const regex=/(\\w+):(?:\"([^\"]*)\"|'([^']*)'|(\\S+))/g;\r\n\r\n let match;\r\n while ((match=regex.exec(trimmed))!==null) {\r\n const key=match[1];\r\n // Value is in group 2 (double quotes), group 3 (single quotes), or group 4 (unquoted)\r\n const value=match[2]??match[3]??match[4];\r\n if (key&&value!==undefined) {\r\n results.push([key,value]);\r\n }\r\n }\r\n\r\n return results;\r\n}\r\n\r\n/**\r\n * Serialize a value, adding quotes if it contains spaces\r\n */\r\nexport function serializeFilterValue(value: string): string {\r\n if (value.includes(\" \")) {\r\n return `\"${value}\"`;\r\n }\r\n return value;\r\n}\r\n\r\n/**\r\n * Schema-based column filters parser for BYOS\r\n *\r\n * This parser works with the new schema system instead of nuqs ParserBuilder.\r\n */\r\nexport function columnFiltersParserFromSchema({\r\n schema,\r\n filterFields,\r\n}: {\r\n schema: SchemaDefinition;\r\n filterFields: DataTableFilterField[];\r\n}) {\r\n return {\r\n parse: (inputValue: string) => {\r\n // Use tokenizer that respects quoted values\r\n const tokens=tokenizeFilterInput(inputValue);\r\n const values=tokens.reduce(\r\n (prev,[name,value]) => {\r\n prev[name]=value;\r\n return prev;\r\n },\r\n {} as Record,\r\n );\r\n\r\n const searchParams=Object.entries(values).reduce(\r\n (prev,[key,value]) => {\r\n const fieldBuilder=schema[key] as FieldBuilder|undefined;\r\n if (!fieldBuilder) return prev;\r\n\r\n const parsed=fieldBuilder._config.parse(value);\r\n if (parsed!==null) {\r\n prev[key]=parsed;\r\n }\r\n return prev;\r\n },\r\n {} as Record,\r\n );\r\n\r\n return searchParams;\r\n },\r\n serialize: (columnFilters: ColumnFiltersState) => {\r\n const values=columnFilters.reduce((prev,curr) => {\r\n const {commandDisabled}=filterFields?.find(\r\n (field) => curr.id===field.value,\r\n )||{commandDisabled: true};\r\n const fieldBuilder=schema[curr.id] as\r\n |FieldBuilder\r\n |undefined;\r\n\r\n if (commandDisabled||!fieldBuilder) return prev;\r\n\r\n const serialized=fieldBuilder._config.serialize(curr.value);\r\n if (!serialized) return prev;\r\n\r\n // Wrap in quotes if value contains spaces\r\n const quotedValue=serializeFilterValue(serialized);\r\n return `${prev}${curr.id}:${quotedValue} `;\r\n },\"\");\r\n\r\n return values;\r\n },\r\n };\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-command/utils.ts" }, { "path": "src/components/data-table/data-table-filter-checkbox.tsx", "content": "\"use client\";\r\n\r\nimport {InputWithAddons} from \"@/components/custom/input-with-addons\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {Checkbox} from \"@/components/ui/checkbox\";\r\nimport {Label} from \"@/components/ui/label\";\r\nimport {Skeleton} from \"@/components/ui/skeleton\";\r\nimport {formatCompactNumber} from \"@/lib/format\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport {Search} from \"lucide-react\";\r\nimport {useState} from \"react\";\r\nimport type {DataTableCheckboxFilterField} from \"./types\";\r\n\r\nexport function DataTableFilterCheckbox({\r\n value: _value,\r\n options,\r\n component,\r\n}: DataTableCheckboxFilterField) {\r\n const value=_value as string;\r\n const [inputValue,setInputValue]=useState(\"\");\r\n const {table,columnFilters,isLoading,getFacetedUniqueValues}=\r\n useDataTable();\r\n const column=table.getColumn(value);\r\n // REMINDER: avoid using column?.getFilterValue()\r\n const filterValue=columnFilters.find((i) => i.id===value)?.value;\r\n const facetedValue=\r\n getFacetedUniqueValues?.(table,value)||column?.getFacetedUniqueValues();\r\n\r\n const Component=component;\r\n\r\n // filter out the options based on the input value\r\n const filterOptions=options?.filter(\r\n (option) =>\r\n inputValue===\"\"||\r\n option.label.toLowerCase().includes(inputValue.toLowerCase()),\r\n );\r\n\r\n // CHECK: it could be filterValue or searchValue\r\n const filters=filterValue\r\n ? Array.isArray(filterValue)\r\n ? filterValue\r\n :[filterValue]\r\n :[];\r\n\r\n // REMINDER: if no options are defined, while fetching data, we should show a skeleton\r\n if (isLoading&&!filterOptions?.length)\r\n return (\r\n
\r\n {Array.from({length: 3}).map((_,index) => (\r\n \r\n \r\n \r\n
\r\n ))}\r\n \r\n );\r\n\r\n return (\r\n
\r\n {options&&options.length>4? (\r\n }\r\n containerClassName=\"h-9 rounded-lg\"\r\n value={inputValue}\r\n onChange={(e) => setInputValue(e.target.value)}\r\n />\r\n ):null}\r\n {/* FIXME: due to the added max-h and overflow-y-auto, the hover state and border is laying on top of the scroll bar */}\r\n
\r\n {filterOptions\r\n // TODO: we shoudn't sort the options here, instead filterOptions should be sorted by default\r\n // .sort((a, b) => a.label.localeCompare(b.label))\r\n ?.map((option,index) => {\r\n const checked=filters.includes(option.value);\r\n\r\n return (\r\n \r\n {\r\n const newValue=checked\r\n ? [...(filters||[]),option.value]\r\n :filters?.filter((value) => option.value!==value);\r\n column?.setFilterValue(\r\n newValue?.length? newValue:undefined,\r\n );\r\n }}\r\n />\r\n \r\n {Component? (\r\n \r\n ):(\r\n {option.label}\r\n )}\r\n \r\n {isLoading? (\r\n \r\n ):facetedValue?.has(option.value)? (\r\n formatCompactNumber(facetedValue.get(option.value)||0)\r\n ):null}\r\n \r\n column?.setFilterValue([option.value])}\r\n className={cn(\r\n \"absolute inset-y-0 right-0 hidden font-normal text-muted-foreground backdrop-blur-sm hover:text-foreground group-hover:block\",\r\n \"rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\r\n )}\r\n >\r\n only\r\n \r\n \r\n
\r\n );\r\n })}\r\n
\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-checkbox.tsx" }, { "path": "src/components/data-table/data-table-filter-input.tsx", "content": "\"use client\";\r\n\r\nimport {InputWithAddons} from \"@/components/custom/input-with-addons\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {Label} from \"@/components/ui/label\";\r\nimport {useDebounce} from \"@/hooks/use-debounce\";\r\nimport {Search} from \"lucide-react\";\r\nimport {useEffect,useState} from \"react\";\r\nimport type {DataTableInputFilterField} from \"./types\";\r\n\r\nfunction getFilter(filterValue: unknown) {\r\n return typeof filterValue===\"string\"? filterValue:null;\r\n}\r\n\r\nexport function DataTableFilterInput({\r\n value: _value,\r\n}: DataTableInputFilterField) {\r\n const value=_value as string;\r\n const {table,columnFilters}=useDataTable();\r\n const column=table.getColumn(value);\r\n const filterValue=columnFilters.find((i) => i.id===value)?.value;\r\n const filters=getFilter(filterValue);\r\n const [input,setInput]=useState(filters);\r\n\r\n const debouncedInput=useDebounce(input,500);\r\n\r\n useEffect(() => {\r\n const newValue=debouncedInput?.trim()===\"\"? null:debouncedInput;\r\n if (debouncedInput===null) return;\r\n column?.setFilterValue(newValue);\r\n },[debouncedInput,column]);\r\n\r\n useEffect(() => {\r\n if (debouncedInput?.trim()!==filters) {\r\n // eslint-disable-next-line react-hooks/set-state-in-effect\r\n setInput(filters);\r\n }\r\n },[filters,debouncedInput]);\r\n\r\n return (\r\n
\r\n \r\n }\r\n containerClassName=\"h-9 rounded-lg\"\r\n name={value}\r\n id={value}\r\n value={input||\"\"}\r\n onChange={(e) => setInput(e.target.value)}\r\n />\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-input.tsx" }, { "path": "src/components/data-table/data-table-filter-slider.tsx", "content": "\"use client\";\r\n\r\nimport {InputWithAddons} from \"@/components/custom/input-with-addons\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {Label} from \"@/components/ui/label\";\r\nimport {Slider} from \"@/components/ui/slider\";\r\nimport {useDebounce} from \"@/hooks/use-debounce\";\r\nimport {isArrayOfNumbers} from \"@/lib/is-array\";\r\nimport {useEffect,useState} from \"react\";\r\nimport type {DataTableSliderFilterField} from \"./types\";\r\n\r\nfunction getFilter(filterValue: unknown) {\r\n return typeof filterValue===\"number\"\r\n ? [filterValue,filterValue]\r\n :Array.isArray(filterValue)&&isArrayOfNumbers(filterValue)\r\n ? filterValue.length===1\r\n ? [filterValue[0],filterValue[0]]\r\n :filterValue\r\n :null;\r\n}\r\n\r\n// TODO: discuss if we even need the `defaultMin` and `defaultMax`\r\nexport function DataTableFilterSlider({\r\n value: _value,\r\n min: defaultMin,\r\n max: defaultMax,\r\n}: DataTableSliderFilterField) {\r\n const value=_value as string;\r\n const {table,columnFilters,getFacetedMinMaxValues}=useDataTable();\r\n const column=table.getColumn(value);\r\n const filterValue=columnFilters.find((i) => i.id===value)?.value;\r\n const filters=getFilter(filterValue);\r\n const [input,setInput]=useState(filters);\r\n const [min,max]=getFacetedMinMaxValues?.(table,value)||\r\n column?.getFacetedMinMaxValues()||[defaultMin,defaultMax];\r\n\r\n const debouncedInput=useDebounce(input,500);\r\n\r\n useEffect(() => {\r\n if (debouncedInput?.length===2) {\r\n column?.setFilterValue(debouncedInput);\r\n }\r\n },[debouncedInput,column]);\r\n\r\n useEffect(() => {\r\n if (debouncedInput?.length!==2) {\r\n return;\r\n }\r\n if (!filters) {\r\n // eslint-disable-next-line react-hooks/set-state-in-effect\r\n setInput(null);\r\n } else if (\r\n debouncedInput[0]!==filters[0]||\r\n debouncedInput[1]!==filters[1]\r\n ) {\r\n // eslint-disable-next-line react-hooks/set-state-in-effect\r\n setInput(filters);\r\n }\r\n },[filters,debouncedInput]);\r\n\r\n return (\r\n
\r\n
\r\n
\r\n \r\n Min.\r\n \r\n \r\n setInput((prev) => [Number(e.target.value),prev?.[1]||max])\r\n }\r\n />\r\n
\r\n
\r\n \r\n Max.\r\n \r\n \r\n setInput((prev) => [prev?.[0]||min,Number(e.target.value)])\r\n }\r\n />\r\n
\r\n
\r\n setInput(values as number[])}\r\n />\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-slider.tsx" }, { "path": "src/components/data-table/data-table-filter-timerange.tsx", "content": "\"use client\";\r\n\r\nimport { DatePickerWithRange } from \"@/components/custom/date-picker-with-range\";\r\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\r\nimport { isArrayOfDates } from \"@/lib/is-array\";\r\nimport { useMemo } from \"react\";\r\nimport type { DateRange } from \"react-day-picker\";\r\nimport type { DataTableTimerangeFilterField } from \"./types\";\r\n\r\nexport function DataTableFilterTimerange({\r\n value: _value,\r\n presets,\r\n}: DataTableTimerangeFilterField) {\r\n const value = _value as string;\r\n const { table, columnFilters } = useDataTable();\r\n const column = table.getColumn(value);\r\n const filterValue = columnFilters.find((i) => i.id === value)?.value;\r\n\r\n const date: DateRange | undefined = useMemo(\r\n () =>\r\n filterValue instanceof Date\r\n ? { from: filterValue, to: undefined }\r\n : Array.isArray(filterValue) && isArrayOfDates(filterValue)\r\n ? { from: filterValue?.[0], to: filterValue?.[1] }\r\n : undefined,\r\n [filterValue],\r\n );\r\n\r\n const setDate = (date: DateRange | undefined) => {\r\n if (!date) return; // TODO: remove from search params if columnFilter is removed\r\n if (date.from && !date.to) {\r\n column?.setFilterValue([date.from]);\r\n }\r\n if (date.to && date.from) {\r\n column?.setFilterValue([date.from, date.to]);\r\n }\r\n };\r\n\r\n return ;\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-timerange.tsx" }, { "path": "src/components/data-table/data-table-filter-reset-button.tsx", "content": "\"use client\";\r\n\r\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { X } from \"lucide-react\";\r\nimport type { DataTableFilterField } from \"./types\";\r\n\r\nexport function DataTableFilterResetButton({\r\n value: _value,\r\n}: DataTableFilterField) {\r\n const { columnFilters, table } = useDataTable();\r\n const value = _value as string;\r\n const column = table.getColumn(value);\r\n const filterValue = columnFilters.find((f) => f.id === value)?.value;\r\n\r\n // TODO: check if we could useMemo\r\n const filters = filterValue\r\n ? Array.isArray(filterValue)\r\n ? filterValue\r\n : [filterValue]\r\n : [];\r\n\r\n if (filters.length === 0) return null;\r\n\r\n return (\r\n {\r\n e.stopPropagation();\r\n column?.setFilterValue(undefined);\r\n }}\r\n onKeyDown={(e) => {\r\n e.stopPropagation();\r\n if (e.code === \"Enter\") {\r\n column?.setFilterValue(undefined);\r\n }\r\n }}\r\n asChild\r\n >\r\n {/* REMINDER: `AccordionTrigger` is also a button(!) and we get Hydration error when rendering button within button */}\r\n
\r\n {filters.length}\r\n \r\n
\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-filter-reset-button.tsx" }, { "path": "src/components/data-table/data-table-reset-button.tsx", "content": "\"use client\";\r\n\r\nimport {Kbd} from \"@/components/custom/kbd\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport {useHotKey} from \"@/hooks/use-hot-key\";\r\nimport {X} from \"lucide-react\";\r\nimport {Button} from \"../ui/button\";\r\n\r\nexport function DataTableResetButton() {\r\n const {table}=useDataTable();\r\n useHotKey(table.resetColumnFilters,\"Escape\");\r\n\r\n return (\r\n \r\n \r\n \r\n table.resetColumnFilters()}\r\n >\r\n \r\n Reset\r\n \r\n \r\n \r\n

\r\n Reset filters with{\" \"}\r\n \r\n \r\n Esc\r\n \r\n

\r\n
\r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-reset-button.tsx" }, { "path": "src/components/data-table/data-table-column-header.tsx", "content": "import {Button,buttonVariants} from \"@/components/ui/button\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {Column} from \"@tanstack/react-table\";\r\nimport type {VariantProps} from 'class-variance-authority';\r\nimport {ChevronDown,ChevronUp} from \"lucide-react\";\r\n\r\nexport interface ButtonProps\r\n extends React.ButtonHTMLAttributes,\r\n VariantProps {\r\n asChild?: boolean;\r\n}\r\n\r\ninterface DataTableColumnHeaderProps extends ButtonProps {\r\n column: Column;\r\n title: string;\r\n}\r\n\r\nexport function DataTableColumnHeader({\r\n column,\r\n title,\r\n className,\r\n ...props\r\n}: DataTableColumnHeaderProps) {\r\n if (!column.getCanSort()) {\r\n return
{title}
;\r\n }\r\n\r\n return (\r\n {\r\n column.toggleSorting(undefined);\r\n }}\r\n className={cn(\r\n \"flex h-7 w-full items-center justify-between gap-2 px-0 py-0 hover:bg-transparent\",\r\n className,\r\n )}\r\n {...props}\r\n >\r\n {title}\r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-column-header.tsx" }, { "path": "src/components/data-table/data-table-skeleton.tsx", "content": "import {Skeleton} from \"@/components/ui/skeleton\";\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from \"@/components/ui/table\";\r\n\r\ninterface DataTableSkeletonProps {\r\n /**\r\n * Number of rows to render\r\n * @default 10\r\n */\r\n rows?: number;\r\n}\r\n\r\nexport function DataTableSkeleton({rows=10}: DataTableSkeletonProps) {\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {new Array(rows).fill(0).map((_,i) => (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n ))}\r\n \r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-skeleton.tsx" }, { "path": "src/components/data-table/data-table-view-options.tsx", "content": "\"use client\";\r\n\r\nimport {\r\n Sortable,\r\n SortableDragHandle,\r\n SortableItem,\r\n} from \"@/components/custom/sortable\";\r\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Check, GripVertical, Settings2 } from \"lucide-react\";\r\nimport { useMemo, useState } from \"react\";\r\n\r\nexport function DataTableViewOptions() {\r\n const { table, enableColumnOrdering } = useDataTable();\r\n const [open, setOpen] = useState(false);\r\n const [drag, setDrag] = useState(false);\r\n const [search, setSearch] = useState(\"\");\r\n\r\n const columnOrder = table.getState().columnOrder;\r\n\r\n const sortedColumns = useMemo(\r\n () =>\r\n table.getAllColumns().sort((a, b) => {\r\n return columnOrder.indexOf(a.id) - columnOrder.indexOf(b.id);\r\n }),\r\n [columnOrder, table],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n View\r\n \r\n \r\n \r\n \r\n \r\n \r\n No option found.\r\n \r\n ({ id: c.id }))}\r\n onValueChange={(items) =>\r\n table.setColumnOrder(items.map((c) => c.id))\r\n }\r\n overlay={
}\r\n onDragStart={() => setDrag(true)}\r\n onDragEnd={() => setDrag(false)}\r\n onDragCancel={() => setDrag(false)}\r\n >\r\n {sortedColumns\r\n .filter(\r\n (column) =>\r\n typeof column.accessorFn !== \"undefined\" &&\r\n column.getCanHide(),\r\n )\r\n .map((column) => (\r\n \r\n \r\n column.toggleVisibility(!column.getIsVisible())\r\n }\r\n className={\"capitalize\"}\r\n disabled={drag}\r\n >\r\n \r\n \r\n
\r\n {column.columnDef.meta?.label || column.id}\r\n {enableColumnOrdering && !search ? (\r\n \r\n \r\n \r\n ) : null}\r\n \r\n \r\n ))}\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-view-options.tsx" }, { "path": "src/components/data-table/data-table-store-sync.tsx", "content": "\"use client\";\r\n\r\n/**\r\n * DataTableStoreSync - Syncs React Table state to BYOS adapter (ONE-WAY)\r\n *\r\n * This component syncs changes from React Table's state (columnFilters, sorting,\r\n * rowSelection) to the BYOS adapter. Filter components update the table directly\r\n * via column.setFilterValue(), and this component propagates those changes\r\n * to the BYOS adapter for URL sync (nuqs) or state persistence (Zustand).\r\n *\r\n * IMPORTANT: This sync is ONE-WAY (Table → BYOS). We use refs to track what\r\n * we've sent to avoid depending on BYOS state, which would cause infinite loops.\r\n */\r\nimport {useStoreContext} from \"@/lib/store/context\";\r\nimport {useFilterActions} from \"@/lib/store/hooks/useFilterActions\";\r\nimport {useEffect,useRef} from \"react\";\r\nimport {useDataTable} from \"./data-table-provider\";\r\n\r\nexport function DataTableStoreSync() {\r\n const context=useStoreContext();\r\n const {table,filterFields,sorting,rowSelection}=useDataTable();\r\n const {setFilters}=useFilterActions();\r\n\r\n // Track what we've sent to avoid re-sending the same values\r\n const lastSentFiltersRef=useRef(\"\");\r\n const lastSentSortRef=useRef(\"\");\r\n const lastSentUuidRef=useRef(\"\");\r\n const isInitialMount=useRef(true);\r\n\r\n // Get current state from table\r\n const columnFilters=table.getState().columnFilters;\r\n\r\n // Sync column filters (Table → URL)\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) {\r\n // On initial mount, just capture current state without syncing\r\n isInitialMount.current=false;\r\n\r\n // Initialize refs with current state\r\n const filterFieldKeys=new Set(\r\n filterFields?.map((f) => f.value as string)||[],\r\n );\r\n const currentFilters: Record={};\r\n for (const filter of columnFilters) {\r\n if (filterFieldKeys.has(filter.id)) {\r\n currentFilters[filter.id]=filter.value;\r\n }\r\n }\r\n lastSentFiltersRef.current=JSON.stringify(currentFilters);\r\n lastSentSortRef.current=JSON.stringify(sorting?.[0]||null);\r\n const selectedKeys=Object.keys(rowSelection||{});\r\n lastSentUuidRef.current=selectedKeys.length>0? selectedKeys[0]:\"\";\r\n return;\r\n }\r\n\r\n const filterFieldKeys=new Set(\r\n filterFields?.map((f) => f.value as string)||[],\r\n );\r\n\r\n // Build current filter state\r\n const currentFilters: Record={};\r\n for (const filter of columnFilters) {\r\n if (filterFieldKeys.has(filter.id)) {\r\n currentFilters[filter.id]=filter.value;\r\n }\r\n }\r\n\r\n // Check if filters have actually changed from what we last sent\r\n const currentFiltersJson=JSON.stringify(currentFilters);\r\n if (currentFiltersJson===lastSentFiltersRef.current) {\r\n return; // No change, skip\r\n }\r\n\r\n // Calculate what needs to be updated (including nulls for removed filters)\r\n const updates: Record={...currentFilters};\r\n\r\n // Check for removed filters (keys that were in last sent but not in current)\r\n try {\r\n const lastSent=JSON.parse(lastSentFiltersRef.current||\"{}\");\r\n for (const key of Object.keys(lastSent)) {\r\n if (!(key in currentFilters)) {\r\n updates[key]=null;\r\n }\r\n }\r\n } catch {\r\n // Ignore parse errors\r\n }\r\n\r\n // Also check filterFieldKeys for nulls (filter was cleared)\r\n for (const key of filterFieldKeys) {\r\n if (!(key in currentFilters)) {\r\n updates[key]=null;\r\n }\r\n }\r\n\r\n // Update ref BEFORE calling setFilters to prevent re-entry\r\n lastSentFiltersRef.current=currentFiltersJson;\r\n\r\n // Send updates\r\n setFilters(updates);\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n },[columnFilters,context,filterFields,setFilters]);\r\n\r\n // Sync sorting (Table → URL)\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) return; // Skip initial mount (handled above)\r\n\r\n const newSort=sorting?.[0]||null;\r\n const newSortJson=JSON.stringify(newSort);\r\n\r\n if (newSortJson===lastSentSortRef.current) {\r\n return; // No change, skip\r\n }\r\n\r\n lastSentSortRef.current=newSortJson;\r\n setFilters({sort: newSort});\r\n },[sorting,context,setFilters]);\r\n\r\n // Sync row selection/uuid (Table → URL)\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) return; // Skip initial mount (handled above)\r\n\r\n const selectedKeys=Object.keys(rowSelection||{});\r\n const newUuid=selectedKeys.length>0? selectedKeys[0]:null;\r\n const newUuidStr=newUuid||\"\";\r\n\r\n if (newUuidStr===lastSentUuidRef.current) {\r\n return; // No change, skip\r\n }\r\n\r\n lastSentUuidRef.current=newUuidStr;\r\n setFilters({uuid: newUuid});\r\n },[rowSelection,context,setFilters]);\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Hook version for more control - syncs filters, sorting, and selection\r\n */\r\nexport function useDataTableStoreSync() {\r\n const context=useStoreContext();\r\n const {table,filterFields,sorting,rowSelection}=useDataTable();\r\n const {setFilters}=useFilterActions();\r\n\r\n const lastSentFiltersRef=useRef(\"\");\r\n const lastSentSortRef=useRef(\"\");\r\n const lastSentUuidRef=useRef(\"\");\r\n const isInitialMount=useRef(true);\r\n\r\n const columnFilters=table.getState().columnFilters;\r\n\r\n // Sync column filters\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) {\r\n isInitialMount.current=false;\r\n const filterFieldKeys=new Set(\r\n filterFields?.map((f) => f.value as string)||[],\r\n );\r\n const currentFilters: Record={};\r\n for (const filter of columnFilters) {\r\n if (filterFieldKeys.has(filter.id)) {\r\n currentFilters[filter.id]=filter.value;\r\n }\r\n }\r\n lastSentFiltersRef.current=JSON.stringify(currentFilters);\r\n lastSentSortRef.current=JSON.stringify(sorting?.[0]||null);\r\n const selectedKeys=Object.keys(rowSelection||{});\r\n lastSentUuidRef.current=selectedKeys.length>0? selectedKeys[0]:\"\";\r\n return;\r\n }\r\n\r\n const filterFieldKeys=new Set(\r\n filterFields?.map((f) => f.value as string)||[],\r\n );\r\n\r\n const currentFilters: Record={};\r\n for (const filter of columnFilters) {\r\n if (filterFieldKeys.has(filter.id)) {\r\n currentFilters[filter.id]=filter.value;\r\n }\r\n }\r\n\r\n const currentFiltersJson=JSON.stringify(currentFilters);\r\n if (currentFiltersJson===lastSentFiltersRef.current) {\r\n return;\r\n }\r\n\r\n const updates: Record={...currentFilters};\r\n\r\n try {\r\n const lastSent=JSON.parse(lastSentFiltersRef.current||\"{}\");\r\n for (const key of Object.keys(lastSent)) {\r\n if (!(key in currentFilters)) {\r\n updates[key]=null;\r\n }\r\n }\r\n } catch {\r\n // Ignore parse errors\r\n }\r\n\r\n for (const key of filterFieldKeys) {\r\n if (!(key in currentFilters)) {\r\n updates[key]=null;\r\n }\r\n }\r\n\r\n lastSentFiltersRef.current=currentFiltersJson;\r\n setFilters(updates);\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n },[columnFilters,context,filterFields,setFilters]);\r\n\r\n // Sync sorting\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) return;\r\n\r\n const newSort=sorting?.[0]||null;\r\n const newSortJson=JSON.stringify(newSort);\r\n\r\n if (newSortJson===lastSentSortRef.current) {\r\n return;\r\n }\r\n\r\n lastSentSortRef.current=newSortJson;\r\n setFilters({sort: newSort});\r\n },[sorting,context,setFilters]);\r\n\r\n // Sync row selection\r\n useEffect(() => {\r\n if (!context) return;\r\n if (isInitialMount.current) return;\r\n\r\n const selectedKeys=Object.keys(rowSelection||{});\r\n const newUuid=selectedKeys.length>0? selectedKeys[0]:null;\r\n const newUuidStr=newUuid||\"\";\r\n\r\n if (newUuidStr===lastSentUuidRef.current) {\r\n return;\r\n }\r\n\r\n lastSentUuidRef.current=newUuidStr;\r\n setFilters({uuid: newUuid});\r\n },[rowSelection,context,setFilters]);\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-store-sync.tsx" }, { "path": "src/components/data-table/data-table-refresh-button.tsx", "content": "\"use client\";\r\n\r\nimport { useDataTable } from \"@/components/data-table/data-table-provider\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { LoaderCircle, RefreshCcw } from \"lucide-react\";\r\n\r\ninterface DataTableRefreshButtonProps {\r\n onClick: () => void;\r\n}\r\n\r\nexport function DataTableRefreshButton({ onClick }: DataTableRefreshButtonProps) {\r\n const { isLoading } = useDataTable();\r\n\r\n return (\r\n \r\n {isLoading ? (\r\n \r\n ) : (\r\n \r\n )}\r\n Refresh data\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-refresh-button.tsx" }, { "path": "src/components/data-table/data-table-sheet/data-table-sheet-content.tsx", "content": "\"use client\";\r\n\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {Table} from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\nimport type {DataTableFilterField,SheetField} from \"../types\";\r\nimport {DataTableSheetRowAction} from \"./data-table-sheet-row-action\";\r\nimport {SheetDetailsContentSkeleton} from \"./data-table-sheet-skeleton\";\r\n\r\ninterface DataTableSheetContentProps\r\n extends React.HTMLAttributes {\r\n data?: TData;\r\n table: Table;\r\n fields: SheetField[];\r\n filterFields: DataTableFilterField[];\r\n // totalRows: number;\r\n // filterRows: number;\r\n // totalRowsFetched: number;\r\n metadata?: TMeta&{\r\n totalRows: number;\r\n filterRows: number;\r\n totalRowsFetched: number;\r\n };\r\n}\r\n\r\nexport function DataTableSheetContent({\r\n data,\r\n table,\r\n className,\r\n fields,\r\n filterFields,\r\n metadata,\r\n ...props\r\n}: DataTableSheetContentProps) {\r\n if (!data) return ;\r\n\r\n return (\r\n
\r\n {fields.map((field) => {\r\n if (field.condition&&!field.condition(data)) return null;\r\n\r\n const Component=field.component;\r\n const value=String(data[field.id]);\r\n\r\n return (\r\n
\r\n {field.type===\"readonly\"? (\r\n \r\n
\r\n {field.label}\r\n
\r\n
\r\n {Component? (\r\n \r\n ):(\r\n value\r\n )}\r\n
\r\n
\r\n ):(\r\n \r\n
\r\n {field.label}\r\n
\r\n
\r\n {Component? (\r\n \r\n ):(\r\n value\r\n )}\r\n
\r\n \r\n )}\r\n \r\n );\r\n })}\r\n
\r\n );\r\n}\r\n\r\nexport const MemoizedDataTableSheetContent=React.memo(\r\n DataTableSheetContent,\r\n (prev,next) => {\r\n // REMINDER: only check if data is the same, rest is useless\r\n return prev.data===next.data;\r\n },\r\n) as typeof DataTableSheetContent;\r\n", "type": "registry:component", "target": "components/data-grid/data-table-sheet/data-table-sheet-content.tsx" }, { "path": "src/components/data-table/data-table-sheet/data-table-sheet-details.tsx", "content": "\"use client\";\r\n\r\nimport {Kbd} from \"@/components/custom/kbd\";\r\nimport {useDataTable} from \"@/components/data-table/data-table-provider\";\r\nimport {Button} from \"@/components/ui/button\";\r\nimport {Separator} from \"@/components/ui/separator\";\r\nimport {\r\n Sheet,\r\n SheetClose,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n} from \"@/components/ui/sheet\";\r\nimport {Skeleton} from \"@/components/ui/skeleton\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport {ChevronDown,ChevronUp,X} from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nexport interface DataTableSheetDetailsProps {\r\n title?: React.ReactNode;\r\n titleClassName?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nexport function DataTableSheetDetails({\r\n title,\r\n titleClassName,\r\n children,\r\n}: DataTableSheetDetailsProps) {\r\n const {table,rowSelection,isLoading}=useDataTable();\r\n\r\n const selectedRowKey=Object.keys(rowSelection)?.[0];\r\n\r\n const selectedRow=React.useMemo(() => {\r\n if (isLoading&&!selectedRowKey) return;\r\n return table\r\n .getCoreRowModel()\r\n .flatRows.find((row) => row.id===selectedRowKey);\r\n },[selectedRowKey,isLoading,table]);\r\n\r\n const index=table\r\n .getCoreRowModel()\r\n .flatRows.findIndex((row) => row.id===selectedRow?.id);\r\n\r\n const nextId=React.useMemo(\r\n () => table.getCoreRowModel().flatRows[index+1]?.id,\r\n [index,isLoading,table],\r\n );\r\n\r\n const prevId=React.useMemo(\r\n () => table.getCoreRowModel().flatRows[index-1]?.id,\r\n [index,isLoading,table],\r\n );\r\n\r\n const onPrev=React.useCallback(() => {\r\n if (prevId) table.setRowSelection({[prevId]: true});\r\n },[prevId,isLoading,table]);\r\n\r\n const onNext=React.useCallback(() => {\r\n if (nextId) table.setRowSelection({[nextId]: true});\r\n },[nextId,isLoading,table]);\r\n\r\n React.useEffect(() => {\r\n const down=(e: KeyboardEvent) => {\r\n if (!selectedRowKey) return;\r\n\r\n // REMINDER: prevent dropdown navigation inside of sheet to change row selection\r\n const activeElement=document.activeElement;\r\n const isMenuActive=activeElement?.closest('[role=\"menu\"]');\r\n\r\n if (isMenuActive) return;\r\n\r\n if (e.key===\"ArrowUp\") {\r\n e.preventDefault();\r\n onPrev();\r\n }\r\n if (e.key===\"ArrowDown\") {\r\n e.preventDefault();\r\n onNext();\r\n }\r\n };\r\n\r\n document.addEventListener(\"keydown\",down);\r\n return () => document.removeEventListener(\"keydown\",down);\r\n },[selectedRowKey,onNext,onPrev]);\r\n\r\n return (\r\n {\r\n // REMINDER: focus back to the row that was selected\r\n // We need to manually focus back due to missing Trigger component\r\n const el=selectedRowKey\r\n ? document.getElementById(selectedRowKey)\r\n :null;\r\n table.resetRowSelection();\r\n\r\n // REMINDER: when navigating between tabs in the sheet and exit the sheet, the tab gets lost\r\n // We need a minimal delay to allow the sheet to close before focusing back to the row\r\n setTimeout(() => el?.focus(),0);\r\n }}\r\n >\r\n e.preventDefault()}\r\n className=\"overflow-y-auto p-0 sm:max-w-md\"\r\n >\r\n \r\n
\r\n \r\n {isLoading&&!selectedRowKey? (\r\n \r\n ):(\r\n title\r\n )}\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n Previous\r\n \r\n \r\n \r\n

\r\n Navigate \r\n

\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n Next\r\n \r\n \r\n \r\n

\r\n Navigate \r\n

\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n Selected row details\r\n \r\n
{children}
\r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-sheet/data-table-sheet-details.tsx" }, { "path": "src/components/data-table/data-table-sheet/data-table-sheet-row-action.tsx", "content": "import {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuGroup,\r\n DropdownMenuItem,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport {useCopyToClipboard} from \"@/hooks/use-copy-to-clipboard\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {Table} from \"@tanstack/react-table\";\r\nimport {endOfDay,endOfHour,startOfDay,startOfHour} from \"date-fns\";\r\nimport {\r\n CalendarClock,\r\n CalendarDays,\r\n CalendarSearch,\r\n ChevronLeft,\r\n ChevronRight,\r\n Copy,\r\n Equal,\r\n Search,\r\n} from \"lucide-react\";\r\nimport type {DataTableFilterField} from \"../types\";\r\n\r\ninterface DataTableSheetRowActionProps<\r\n TData,\r\n TFields extends DataTableFilterField,\r\n> extends React.ComponentPropsWithRef {\r\n fieldValue: TFields[\"value\"];\r\n filterFields: TFields[];\r\n value: string|number;\r\n table: Table;\r\n}\r\n\r\nexport function DataTableSheetRowAction<\r\n TData,\r\n TFields extends DataTableFilterField,\r\n>({\r\n fieldValue,\r\n filterFields,\r\n value,\r\n children,\r\n className,\r\n table,\r\n onKeyDown,\r\n ...props\r\n}: DataTableSheetRowActionProps) {\r\n const {copy,isCopied}=useCopyToClipboard();\r\n const field=filterFields.find((field) => field.value===fieldValue);\r\n const column=table.getColumn(fieldValue.toString());\r\n\r\n if (!field||!column) return null;\r\n\r\n function renderOptions() {\r\n if (!field) return null;\r\n switch (field.type) {\r\n case \"checkbox\":\r\n return (\r\n {\r\n // FIXME:\r\n const filterValue=column?.getFilterValue() as\r\n |undefined\r\n |Array;\r\n const newValue=filterValue?.includes(value)\r\n ? filterValue\r\n :[...(filterValue||[]),value];\r\n\r\n column?.setFilterValue(newValue);\r\n }}\r\n >\r\n \r\n Include\r\n \r\n );\r\n case \"input\":\r\n return (\r\n column?.setFilterValue(value)}>\r\n \r\n Include\r\n \r\n );\r\n case \"slider\":\r\n return (\r\n \r\n column?.setFilterValue([0,value])}\r\n >\r\n {/* FIXME: change icon as it is not clear */}\r\n \r\n Less or equal than\r\n \r\n column?.setFilterValue([value,5000])}\r\n >\r\n {/* FIXME: change icon as it is not clear */}\r\n \r\n Greater or equal than\r\n \r\n column?.setFilterValue([value])}>\r\n \r\n Equal to\r\n \r\n \r\n );\r\n case \"timerange\":\r\n {\r\n const date=new Date(value);\r\n return (\r\n \r\n column?.setFilterValue([date])}>\r\n \r\n Exact timestamp\r\n \r\n {\r\n const start=startOfHour(date);\r\n const end=endOfHour(date);\r\n column?.setFilterValue([start,end]);\r\n }}\r\n >\r\n \r\n Same hour\r\n \r\n {\r\n const start=startOfDay(date);\r\n const end=endOfDay(date);\r\n column?.setFilterValue([start,end]);\r\n }}\r\n >\r\n \r\n Same day\r\n \r\n \r\n );\r\n }\r\n default:\r\n return null;\r\n }\r\n }\r\n\r\n return (\r\n \r\n {\r\n if (e.key===\"ArrowDown\") {\r\n // REMINDER: default behavior is to open the dropdown menu\r\n // But because we use it to navigate between rows, we need to prevent it\r\n // and only use \"Enter\" to select the option\r\n e.preventDefault();\r\n }\r\n onKeyDown?.(e);\r\n }}\r\n {...props}\r\n >\r\n {children}\r\n {isCopied? (\r\n
\r\n Value copied\r\n
\r\n ):null}\r\n \r\n \r\n {renderOptions()}\r\n \r\n copy(String(value),{timeout: 1000})}\r\n >\r\n \r\n Copy value\r\n \r\n \r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-sheet/data-table-sheet-row-action.tsx" }, { "path": "src/components/data-table/data-table-sheet/data-table-sheet-skeleton.tsx", "content": "import {Skeleton} from \"@/components/ui/skeleton\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {SheetField} from \"../types\";\r\n\r\ninterface SheetDetailsContentSkeletonProps {\r\n fields: SheetField[];\r\n}\r\n\r\nexport function SheetDetailsContentSkeleton({\r\n fields,\r\n}: SheetDetailsContentSkeletonProps) {\r\n return (\r\n
\r\n {fields.map((field) => (\r\n \r\n
{field.label}
\r\n
\r\n \r\n
\r\n \r\n ))}\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-sheet/data-table-sheet-skeleton.tsx" }, { "path": "src/components/data-table/data-table-cell/index.tsx", "content": "export { DataTableCellText } from \"./data-table-cell-text\";\r\nexport { DataTableCellCode } from \"./data-table-cell-code\";\r\nexport { DataTableCellNumber } from \"./data-table-cell-number\";\r\nexport { DataTableCellTimestamp } from \"./data-table-cell-timestamp\";\r\nexport { DataTableCellBadge } from \"./data-table-cell-badge\";\r\nexport { DataTableCellBoolean } from \"./data-table-cell-boolean\";\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/index.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-badge.tsx", "content": "export function DataTableCellBadge({ value }: { value: string | number }) {\r\n return (\r\n \r\n {value}\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-badge.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-boolean.tsx", "content": "import { Check, Minus } from \"lucide-react\";\r\n\r\nexport function DataTableCellBoolean({ value }: { value: boolean }) {\r\n if (value) {\r\n return ;\r\n }\r\n return ;\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-boolean.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-code.tsx", "content": "export function DataTableCellCode({ value }: { value: string | number }) {\r\n return {value};\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-code.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-number.tsx", "content": "export function DataTableCellNumber({\r\n value,\r\n unit,\r\n}: {\r\n value: number;\r\n unit?: string;\r\n}) {\r\n const formatted = new Intl.NumberFormat(\"en-US\", {\r\n maximumFractionDigits: 3,\r\n }).format(value);\r\n\r\n return (\r\n \r\n {formatted}\r\n {unit && {unit}}\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-number.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-text.tsx", "content": "import { TextWithTooltip } from \"@/components/custom/text-with-tooltip\";\r\n\r\nexport function DataTableCellText({ value }: { value: string | number }) {\r\n return ;\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-text.tsx" }, { "path": "src/components/data-table/data-table-cell/data-table-cell-timestamp.tsx", "content": "\"use client\";\r\n\r\nimport {\r\n HoverCard,\r\n HoverCardContent,\r\n HoverCardTrigger,\r\n} from \"@/components/ui/hover-card\";\r\nimport {useCopyToClipboard} from \"@/hooks/use-copy-to-clipboard\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport {UTCDate} from \"@date-fns/utc\";\r\nimport {format,formatDistanceToNowStrict} from \"date-fns\";\r\nimport {Check,Copy} from \"lucide-react\";\r\nimport type {ComponentPropsWithoutRef} from \"react\";\r\n\r\ntype HoverCardContentProps=ComponentPropsWithoutRef;\r\n\r\ninterface HoverCardTimestampProps {\r\n date: Date;\r\n side?: HoverCardContentProps[\"side\"];\r\n sideOffset?: HoverCardContentProps[\"sideOffset\"];\r\n align?: HoverCardContentProps[\"align\"];\r\n alignOffset?: HoverCardContentProps[\"alignOffset\"];\r\n className?: string;\r\n}\r\n\r\nexport function DataTableCellTimestamp({\r\n date,\r\n side=\"right\",\r\n align=\"start\",\r\n alignOffset=-4,\r\n sideOffset,\r\n className,\r\n}: HoverCardTimestampProps) {\r\n const timezone=Intl.DateTimeFormat().resolvedOptions().timeZone;\r\n\r\n return (\r\n \r\n \r\n
\r\n {format(date,\"LLL dd, y HH:mm:ss\")}\r\n
\r\n
\r\n {/* REMINDER: allows us to port the content to the document.body, which is helpful when using opacity-50 on the row element */}\r\n \r\n
\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n );\r\n}\r\n\r\nfunction Row({value,label}: {value: string; label: string}) {\r\n const {copy,isCopied}=useCopyToClipboard();\r\n\r\n return (\r\n {\r\n e.stopPropagation();\r\n copy(value);\r\n }}\r\n >\r\n
{label}
\r\n
\r\n \r\n {!isCopied? (\r\n \r\n ):(\r\n \r\n )}\r\n \r\n {value}\r\n
\r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-cell/data-table-cell-timestamp.tsx" }, { "path": "src/components/data-table/types.ts", "content": "import type { JSX } from \"react\";\r\n\r\nexport type SearchParams = {\r\n [key: string]: string | string[] | undefined;\r\n};\r\n\r\nexport type DatePreset = {\r\n label: string;\r\n from: Date;\r\n to: Date;\r\n shortcut: string;\r\n};\r\n\r\n// TODO: we could type the value(!) especially when using enums\r\nexport type Option = {\r\n label: string;\r\n value: string | boolean | number | undefined;\r\n};\r\n\r\nexport type Input = {\r\n type: \"input\";\r\n options?: Option[];\r\n};\r\n\r\nexport type Checkbox = {\r\n type: \"checkbox\";\r\n component?: (props: Option) => JSX.Element | null;\r\n options?: Option[];\r\n};\r\n\r\nexport type Slider = {\r\n type: \"slider\";\r\n min: number;\r\n max: number;\r\n // if options is undefined, we will provide all the steps between min and max\r\n options?: Option[];\r\n};\r\n\r\nexport type Timerange = {\r\n type: \"timerange\";\r\n options?: Option[]; // required for TS\r\n presets?: DatePreset[];\r\n};\r\n\r\nexport type Base = {\r\n label: string;\r\n value: keyof TData;\r\n /**\r\n * Defines if the accordion in the filter bar is open by default\r\n */\r\n defaultOpen?: boolean;\r\n /**\r\n * Defines if the command input is disabled for this field\r\n */\r\n commandDisabled?: boolean;\r\n};\r\n\r\nexport type DataTableCheckboxFilterField = Base & Checkbox;\r\nexport type DataTableSliderFilterField = Base & Slider;\r\nexport type DataTableInputFilterField = Base & Input;\r\nexport type DataTableTimerangeFilterField = Base & Timerange;\r\n\r\nexport type DataTableFilterField =\r\n | DataTableCheckboxFilterField\r\n | DataTableSliderFilterField\r\n | DataTableInputFilterField\r\n | DataTableTimerangeFilterField;\r\n\r\n/** ----------------------------------------- */\r\n\r\nexport type SheetField> = {\r\n id: keyof TData;\r\n label: string;\r\n // FIXME: rethink that! I dont think we need this as there is no input type\r\n // REMINDER: readonly if we only want to copy the value (e.g. uuid)\r\n // TODO: we might have some values that are not in the data but can be computed\r\n type: \"readonly\" | \"input\" | \"checkbox\" | \"slider\" | \"timerange\";\r\n component?: (\r\n // REMINDER: this is used to pass additional data like the `InfiniteQueryMeta`\r\n props: TData & {\r\n metadata?: {\r\n totalRows: number;\r\n filterRows: number;\r\n totalRowsFetched: number;\r\n } & TMeta;\r\n },\r\n ) => JSX.Element | null | string;\r\n condition?: (props: TData) => boolean;\r\n className?: string;\r\n skeletonClassName?: string;\r\n};\r\n\r\n/** Generic chart data row: timestamp + any numeric keys (e.g. error, warning, success counts). */\r\nexport type BaseChartSchema = { timestamp: number; [key: string]: number };\r\n", "type": "registry:lib", "target": "components/data-grid/types.ts" }, { "path": "src/components/data-table/utils.ts", "content": "// TODO: check if we can move to /data-table-filter-command/utils.ts\r\nimport {\r\n ARRAY_DELIMITER,\r\n RANGE_DELIMITER,\r\n SLIDER_DELIMITER,\r\n} from \"@/lib/delimiters\";\r\nimport type { ColumnFiltersState } from \"@tanstack/react-table\";\r\nimport { z } from \"zod\";\r\nimport type { DataTableFilterField } from \"./types\";\r\n\r\nexport function deserialize(schema: T) {\r\n const castToSchema = z.preprocess((val) => {\r\n if (typeof val !== \"string\") return val;\r\n return val\r\n .trim()\r\n .split(\" \")\r\n .reduce(\r\n (prev, curr) => {\r\n const [name, value] = curr.split(\":\");\r\n if (!value || !name) return prev;\r\n prev[name] = value;\r\n return prev;\r\n },\r\n {} as Record,\r\n );\r\n }, schema);\r\n return (value: string) => castToSchema.safeParse(value);\r\n}\r\n\r\n// export function serialize(schema: T) {\r\n// return (value: z.infer) =>\r\n// schema\r\n// .transform((val) => {\r\n// Object.keys(val).reduce((prev, curr) => {\r\n// if (Array.isArray(val[curr])) {\r\n// return `${prev}${curr}:${val[curr].join(\",\")} `;\r\n// }\r\n// return `${prev}${curr}:${val[curr]} `;\r\n// }, \"\");\r\n// })\r\n// .safeParse(value);\r\n// }\r\n\r\nexport function serializeColumFilters(\r\n columnFilters: ColumnFiltersState,\r\n filterFields?: DataTableFilterField[],\r\n) {\r\n return columnFilters.reduce((prev, curr) => {\r\n const { type, commandDisabled } = filterFields?.find(\r\n (field) => curr.id === field.value,\r\n ) || { commandDisabled: true }; // if column filter is not found, disable the command by default\r\n\r\n if (commandDisabled) return prev;\r\n\r\n if (Array.isArray(curr.value)) {\r\n if (type === \"slider\") {\r\n return `${prev}${curr.id}:${curr.value.join(SLIDER_DELIMITER)} `;\r\n }\r\n if (type === \"checkbox\") {\r\n return `${prev}${curr.id}:${curr.value.join(ARRAY_DELIMITER)} `;\r\n }\r\n if (type === \"timerange\") {\r\n return `${prev}${curr.id}:${curr.value.join(RANGE_DELIMITER)} `;\r\n }\r\n }\r\n\r\n return `${prev}${curr.id}:${curr.value} `;\r\n }, \"\");\r\n}\r\n", "type": "registry:lib", "target": "components/data-grid/utils.ts" }, { "path": "src/lib/store/index.ts", "content": "/**\r\n * BYOS (Bring Your Own Store) - Main Exports\r\n *\r\n * This module provides a pluggable state management system for data tables.\r\n *\r\n * @example\r\n * ```typescript\r\n * // 1. Define schema\r\n * import { createSchema, field } from '@/lib/store';\r\n *\r\n * const schema = createSchema({\r\n * regions: field.array(field.string()).default([]).delimiter(','),\r\n * latency: field.array(field.number()).delimiter('-'),\r\n * host: field.string(),\r\n * live: field.boolean().default(false),\r\n * });\r\n *\r\n * // 2. Create adapter (nuqs for URL state)\r\n * import { useNuqsAdapter } from '@/lib/store/adapters/nuqs';\r\n *\r\n * const adapter = useNuqsAdapter(schema.definition, { id: 'my-table' });\r\n *\r\n * // 3. Use in component\r\n * import { DataTableStoreProvider, useFilterState, useFilterActions } from '@/lib/store';\r\n *\r\n * function MyTable() {\r\n * return (\r\n * \r\n * \r\n * \r\n * \r\n * );\r\n * }\r\n *\r\n * function FilterControls() {\r\n * const state = useFilterState();\r\n * const { setFilter, resetAllFilters } = useFilterActions();\r\n * // ...\r\n * }\r\n * ```\r\n */\r\n\r\n// Schema\r\nexport {createSchema,field,getSchemaDefaults,isStateEqual,mergeWithDefaults,parseState,serializeState,stateToSearchString,validateState} from \"./schema/schema\";\r\nexport type {\r\n AdapterOptions,FieldBuilder,\r\n FieldConfig,InferSchemaType,Schema,\r\n SchemaDefinition,StoreSnapshot\r\n} from \"./schema/schema\";\r\n\r\n// Adapter Interface\r\nexport type {\r\n AdapterFactory,\r\n AdapterType,CreateAdapterOptions,StoreAdapter\r\n} from \"./adapter/adapterTypes\";\r\n\r\n// Provider\r\nexport {DataTableStoreProvider} from \"./provider/DataTableStoreProvider\";\r\nexport type {DataTableStoreProviderProps} from \"./provider/DataTableStoreProvider\";\r\n\r\n// Hooks\r\nexport {useFilterActions,type FilterActions} from \"./hooks/useFilterActions\";\r\nexport {useFilterField,type FilterFieldResult} from \"./hooks/useFilterField\";\r\nexport {useFilterState} from \"./hooks/useFilterState\";\r\nexport {useReactTableSync} from \"./hooks/useReactTableSync\";\r\n\r\n// Context (for advanced use cases)\r\nexport {\r\n StoreContext,\r\n useStoreContext,\r\n type StoreContextValue\r\n} from \"./context\";\r\n\r\n// Text Parser\r\nexport {createTextParser} from \"./parser/text-parser\";\r\nexport type {TextParser,TextParserOptions} from \"./parser/types\";\r\n\r\n", "type": "registry:lib", "target": "lib/data-grid/store/index.ts" }, { "path": "src/lib/store/context.ts", "content": "/**\r\n * React Context for BYOS Store\r\n */\r\n\r\n\"use client\";\r\n\r\nimport {createContext,useContext} from \"react\";\r\nimport type {StoreAdapter} from \"./adapter/adapterTypes\";\r\nimport type {SchemaDefinition} from \"./schema/schemaTypes\";\r\n\r\n/**\r\n * Context value for the store provider\r\n */\r\nexport interface StoreContextValue> {\r\n adapter: StoreAdapter;\r\n schema: SchemaDefinition;\r\n tableId: string;\r\n}\r\n\r\n/**\r\n * React context for the store adapter\r\n * @internal\r\n */\r\nexport const StoreContext=createContext\r\n>|null>(null);\r\n\r\n/**\r\n * Hook to access the store context\r\n * Returns null if used outside of DataTableStoreProvider\r\n */\r\nexport function useStoreContext<\r\n T extends Record=Record,\r\n>(): StoreContextValue {\r\n const context=useContext(StoreContext) as StoreContextValue|null;\r\n if (!context) {\r\n throw new Error(\r\n \"useStoreContext must be used within a DataTableStoreProvider\",\r\n );\r\n }\r\n return context;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/context.ts" }, { "path": "src/lib/store/schema/field.ts", "content": "/**\r\n * Field Builders for Schema Definition\r\n *\r\n * Provides a fluent API for defining filter field types with serialization.\r\n */\r\n\r\nimport {\r\n ARRAY_DELIMITER,\r\n SLIDER_DELIMITER,\r\n SORT_DELIMITER,\r\n} from \"@/lib/delimiters\";\r\nimport type {FieldBuilder,FieldConfig} from \"./schemaTypes\";\r\n\r\n// Helper to create a field builder from config\r\nfunction createFieldBuilder(config: FieldConfig): FieldBuilder {\r\n const builder: FieldBuilder={\r\n default(value: T) {\r\n return createFieldBuilder({...config,defaultValue: value});\r\n },\r\n\r\n delimiter(separator: string) {\r\n // Update serialize/parse functions when delimiter changes for arrays\r\n if (config.type===\"array\"&&config.itemConfig) {\r\n const itemConfig=config.itemConfig as FieldConfig;\r\n return createFieldBuilder({\r\n ...config,\r\n delimiter: separator,\r\n serialize: (value: T) => {\r\n if (!Array.isArray(value)) return \"\";\r\n return value\r\n .map((item) => itemConfig.serialize(item))\r\n .join(separator);\r\n },\r\n parse: (str: string) => {\r\n if (!str) return config.defaultValue;\r\n const items=str.split(separator);\r\n const parsed=items\r\n .map((item) => itemConfig.parse(item))\r\n .filter(\r\n (item): item is NonNullable => item!==null,\r\n );\r\n return parsed as T;\r\n },\r\n });\r\n }\r\n return createFieldBuilder({...config,delimiter: separator});\r\n },\r\n\r\n serialize(fn: (value: T) => string) {\r\n return createFieldBuilder({...config,serialize: fn});\r\n },\r\n\r\n parse(fn: (value: string) => T|null) {\r\n return createFieldBuilder({...config,parse: fn});\r\n },\r\n\r\n get _config() {\r\n return config;\r\n },\r\n };\r\n\r\n return builder;\r\n}\r\n\r\n// String field\r\nfunction string(): FieldBuilder {\r\n return createFieldBuilder({\r\n type: \"string\",\r\n defaultValue: null,\r\n delimiter: \"\",\r\n serialize: (value) => (value===null? \"\":String(value)),\r\n parse: (str) => (str===\"\"? null:str),\r\n });\r\n}\r\n\r\n// Number field (integer)\r\nfunction number(): FieldBuilder {\r\n return createFieldBuilder({\r\n type: \"number\",\r\n defaultValue: null,\r\n delimiter: \"\",\r\n serialize: (value) => (value===null? \"\":String(value)),\r\n parse: (str) => {\r\n if (str===\"\") return null;\r\n const num=parseInt(str,10);\r\n return isNaN(num)? null:num;\r\n },\r\n });\r\n}\r\n\r\n// Boolean field\r\nfunction boolean(): FieldBuilder {\r\n return createFieldBuilder({\r\n type: \"boolean\",\r\n defaultValue: null,\r\n delimiter: \"\",\r\n serialize: (value) => (value===null? \"\":String(value)),\r\n parse: (str) => {\r\n if (str===\"\") return null;\r\n if (str===\"true\") return true;\r\n if (str===\"false\") return false;\r\n return null;\r\n },\r\n });\r\n}\r\n\r\n// Timestamp field (Date)\r\nfunction timestamp(): FieldBuilder {\r\n return createFieldBuilder({\r\n type: \"timestamp\",\r\n defaultValue: null,\r\n delimiter: \"\",\r\n serialize: (value) => (value===null? \"\":String(value.getTime())),\r\n parse: (str) => {\r\n if (str===\"\") return null;\r\n const time=parseInt(str,10);\r\n if (isNaN(time)) return null;\r\n const date=new Date(time);\r\n return isNaN(date.getTime())? null:date;\r\n },\r\n });\r\n}\r\n\r\n// String literal field (enum-like)\r\nfunction stringLiteral(\r\n literals: T,\r\n): FieldBuilder {\r\n return createFieldBuilder({\r\n type: \"stringLiteral\",\r\n defaultValue: null,\r\n delimiter: \"\",\r\n literals,\r\n serialize: (value) => (value===null? \"\":String(value)),\r\n parse: (str) => {\r\n if (str===\"\") return null;\r\n return literals.includes(str as T[number])? (str as T[number]):null;\r\n },\r\n });\r\n}\r\n\r\n// Array field\r\nfunction array(itemBuilder: FieldBuilder): FieldBuilder {\r\n const itemConfig=itemBuilder._config;\r\n const defaultDelimiter=\r\n itemConfig.type===\"number\"? SLIDER_DELIMITER:ARRAY_DELIMITER;\r\n\r\n return createFieldBuilder({\r\n type: \"array\",\r\n defaultValue: [],\r\n delimiter: defaultDelimiter,\r\n itemConfig: itemConfig as FieldConfig,\r\n serialize: (value) => {\r\n if (!Array.isArray(value)||value.length===0) return \"\";\r\n return value\r\n .map((item) => itemConfig.serialize(item))\r\n .join(defaultDelimiter);\r\n },\r\n parse: (str) => {\r\n if (!str) return [];\r\n const items=str.split(defaultDelimiter);\r\n const parsed=items\r\n .map((item) => itemConfig.parse(item))\r\n .filter((item): item is T => item!==null);\r\n return parsed;\r\n },\r\n });\r\n}\r\n\r\n// Sort field { id: string, desc: boolean }\r\nfunction sort(): FieldBuilder<{id: string; desc: boolean}|null> {\r\n return createFieldBuilder<{id: string; desc: boolean}|null>({\r\n type: \"sort\",\r\n defaultValue: null,\r\n delimiter: SORT_DELIMITER,\r\n serialize: (value) => {\r\n if (value===null) return \"\";\r\n return `${value.id}${SORT_DELIMITER}${value.desc? \"desc\":\"asc\"}`;\r\n },\r\n parse: (str) => {\r\n if (!str) return null;\r\n const [id,desc]=str.split(SORT_DELIMITER);\r\n if (!id) return null;\r\n return {id,desc: desc===\"desc\"};\r\n },\r\n });\r\n}\r\n\r\n// Export field builders\r\nexport const field={\r\n string,\r\n number,\r\n boolean,\r\n timestamp,\r\n stringLiteral,\r\n array,\r\n sort,\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/store/schema/field.ts" }, { "path": "src/lib/store/schema/schema.ts", "content": "import type {Schema,SchemaDefinition} from \"./schemaTypes\";\r\nimport {getSchemaDefaults} from \"./serialization\";\r\n\r\n/**\r\n * Schema System for BYOS (Bring Your Own Store)\r\n *\r\n * Provides a fluent API for defining filter schemas with type inference.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { createSchema, field } from '@/lib/store/schema';\r\n *\r\n * const schema = createSchema({\r\n * regions: field.array(field.string()).default([]).delimiter(','),\r\n * latency: field.array(field.number()).delimiter('-'),\r\n * host: field.string().default(''),\r\n * live: field.boolean().default(false),\r\n * });\r\n *\r\n * type FilterState = typeof schema._type;\r\n * ```\r\n */\r\n\r\nexport {field} from \"./field\";\r\nexport type {\r\n AdapterOptions,FieldBuilder,\r\n FieldConfig,InferSchemaType,\r\n Schema,SchemaDefinition,StoreSnapshot\r\n} from \"./schemaTypes\";\r\nexport {\r\n getSchemaDefaults,isStateEqual,mergeWithDefaults,parseState,serializeState,stateToSearchString,validateState\r\n} from \"./serialization\";\r\n\r\n/**\r\n * Create a schema from field definitions\r\n *\r\n * @param definition - Object mapping field names to field builders\r\n * @returns Schema object with definition, defaults, and type inference\r\n *\r\n * @example\r\n * ```typescript\r\n * const schema = createSchema({\r\n * level: field.array(field.stringLiteral(['error', 'warn', 'info'])).default([]),\r\n * latency: field.array(field.number()).delimiter('-'),\r\n * host: field.string(),\r\n * });\r\n * ```\r\n */\r\nexport function createSchema(\r\n definition: T,\r\n): Schema {\r\n const defaults=getSchemaDefaults(definition);\r\n\r\n return {\r\n definition,\r\n defaults,\r\n // This is a type-only property for inference\r\n _type: defaults,\r\n };\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/schema/schema.ts" }, { "path": "src/lib/store/schema/serialization.ts", "content": "/**\r\n * Serialization Utilities for Schema\r\n *\r\n * Functions for serializing state to strings and parsing strings back to state.\r\n */\r\n\r\nimport type {FieldBuilder,InferSchemaType,SchemaDefinition} from \"./schemaTypes\";\r\n\r\n/**\r\n * Get default values from a schema definition\r\n */\r\nexport function getSchemaDefaults(\r\n schema: T,\r\n): InferSchemaType {\r\n const defaults: Record={};\r\n\r\n for (const [key,fieldBuilder] of Object.entries(schema)) {\r\n defaults[key]=fieldBuilder._config.defaultValue;\r\n }\r\n\r\n return defaults as InferSchemaType;\r\n}\r\n\r\n/**\r\n * Serialize state to a string map (for URL params or storage)\r\n */\r\nexport function serializeState(\r\n schema: T,\r\n state: Partial>,\r\n): Record {\r\n const result: Record={};\r\n\r\n for (const [key,value] of Object.entries(state)) {\r\n const fieldBuilder=schema[key] as FieldBuilder|undefined;\r\n if (!fieldBuilder) continue;\r\n\r\n const serialized=fieldBuilder._config.serialize(value);\r\n if (serialized!==\"\") {\r\n result[key]=serialized;\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Parse a string map back to state\r\n */\r\nexport function parseState(\r\n schema: T,\r\n stringMap: Record,\r\n): Partial> {\r\n const result: Record={};\r\n\r\n for (const [key,fieldBuilder] of Object.entries(schema)) {\r\n const rawValue=stringMap[key];\r\n if (rawValue===undefined) continue;\r\n\r\n // Handle array values from URL (Next.js can pass arrays)\r\n const strValue=Array.isArray(rawValue)? rawValue.join(\",\"):rawValue;\r\n\r\n const parsed=(fieldBuilder as FieldBuilder)._config.parse(\r\n strValue,\r\n );\r\n if (parsed!==null) {\r\n result[key]=parsed;\r\n }\r\n }\r\n\r\n return result as Partial>;\r\n}\r\n\r\n/**\r\n * Validate and coerce state, returning defaults for invalid values\r\n */\r\nexport function validateState(\r\n schema: T,\r\n state: unknown,\r\n): InferSchemaType {\r\n const defaults=getSchemaDefaults(schema);\r\n\r\n if (!state||typeof state!==\"object\") {\r\n return defaults;\r\n }\r\n\r\n const result: Record={...defaults};\r\n\r\n for (const [key,fieldBuilder] of Object.entries(schema)) {\r\n const value=(state as Record)[key];\r\n if (value===undefined) continue;\r\n\r\n // Serialize and re-parse to validate\r\n const config=(fieldBuilder as FieldBuilder)._config;\r\n const serialized=config.serialize(value);\r\n const parsed=config.parse(serialized);\r\n\r\n if (parsed!==null) {\r\n result[key]=parsed;\r\n }\r\n // If invalid, default is already in result\r\n }\r\n\r\n return result as InferSchemaType;\r\n}\r\n\r\n/**\r\n * Merge partial state with defaults\r\n */\r\nexport function mergeWithDefaults(\r\n schema: T,\r\n partial: Partial>,\r\n): InferSchemaType {\r\n const defaults=getSchemaDefaults(schema);\r\n return {...defaults,...partial};\r\n}\r\n\r\n/**\r\n * Check if two states are equal (shallow comparison for primitives, deep for arrays)\r\n */\r\nexport function isStateEqual>(\r\n a: T,\r\n b: T,\r\n): boolean {\r\n const keysA=Object.keys(a);\r\n const keysB=Object.keys(b);\r\n\r\n if (keysA.length!==keysB.length) return false;\r\n\r\n for (const key of keysA) {\r\n const valA=a[key];\r\n const valB=b[key];\r\n\r\n if (valA===valB) continue;\r\n\r\n // Handle arrays\r\n if (Array.isArray(valA)&&Array.isArray(valB)) {\r\n if (valA.length!==valB.length) return false;\r\n for (let i=0;i;\r\n const objB=valB as Record;\r\n if (!isStateEqual(objA,objB)) return false;\r\n continue;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n return true;\r\n}\r\n\r\n/**\r\n * Create a URL search string from state\r\n */\r\nexport function stateToSearchString(\r\n schema: T,\r\n state: Partial>,\r\n): string {\r\n const serialized=serializeState(schema,state);\r\n const params=new URLSearchParams();\r\n\r\n for (const [key,value] of Object.entries(serialized)) {\r\n if (value) {\r\n params.set(key,value);\r\n }\r\n }\r\n\r\n const str=params.toString();\r\n return str? `?${str}`:\"\";\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/schema/serialization.ts" }, { "path": "src/lib/store/schema/schemaTypes.ts", "content": "/**\r\n * Schema Types for BYOS (Bring Your Own Store)\r\n *\r\n * These types define the structure for filter schemas with a fluent API.\r\n */\r\n\r\n// Primitive field types\r\nexport type PrimitiveType =\r\n | \"string\"\r\n | \"number\"\r\n | \"boolean\"\r\n | \"timestamp\"\r\n | \"stringLiteral\";\r\n\r\n// Field configuration stored internally\r\nexport interface FieldConfig {\r\n type: PrimitiveType | \"array\" | \"sort\";\r\n defaultValue: T;\r\n delimiter: string;\r\n serialize: (value: T) => string;\r\n parse: (value: string) => T | null;\r\n // For stringLiteral type\r\n literals?: readonly string[];\r\n // For array type\r\n itemConfig?: FieldConfig;\r\n}\r\n\r\n// Field builder interface (fluent API)\r\nexport interface FieldBuilder {\r\n /**\r\n * Set the default value for this field\r\n */\r\n default(value: T): FieldBuilder;\r\n\r\n /**\r\n * Set the delimiter for serialization (used for arrays)\r\n */\r\n delimiter(separator: string): FieldBuilder;\r\n\r\n /**\r\n * Custom serialization function\r\n */\r\n serialize(fn: (value: T) => string): FieldBuilder;\r\n\r\n /**\r\n * Custom parse function\r\n */\r\n parse(fn: (value: string) => T | null): FieldBuilder;\r\n\r\n /**\r\n * Internal config - do not use directly\r\n * @internal\r\n */\r\n readonly _config: FieldConfig;\r\n}\r\n\r\n// Schema definition as a record of field builders\r\n\r\nexport type SchemaDefinition = Record>;\r\n\r\n// Infer the TypeScript type from a schema definition\r\nexport type InferSchemaType = {\r\n [K in keyof T]: T[K] extends FieldBuilder ? U : never;\r\n};\r\n\r\n// Schema object returned by createSchema\r\nexport interface Schema {\r\n /**\r\n * The raw schema definition\r\n */\r\n readonly definition: T;\r\n\r\n /**\r\n * Get default values for all fields\r\n */\r\n readonly defaults: InferSchemaType;\r\n\r\n /**\r\n * Inferred TypeScript type (for type inference only)\r\n */\r\n readonly _type: InferSchemaType;\r\n}\r\n\r\n// Store snapshot with version for change detection\r\nexport interface StoreSnapshot {\r\n state: T;\r\n version: number;\r\n}\r\n\r\n// Adapter options passed during creation\r\nexport interface AdapterOptions {\r\n /**\r\n * Unique ID for this table (required for multi-table support)\r\n */\r\n id: string;\r\n\r\n /**\r\n * Initial state to override defaults\r\n */\r\n initialState?: Record;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/schema/schemaTypes.ts" }, { "path": "src/lib/store/adapter/adapterTypes.ts", "content": "/**\r\n * Adapter Interface for BYOS (Bring Your Own Store)\r\n *\r\n * This interface defines the contract that all store adapters must implement.\r\n * It is designed to be compatible with React 18's useSyncExternalStore.\r\n */\r\n\r\nimport type {SchemaDefinition,StoreSnapshot} from \"../schema/schemaTypes\";\r\n\r\n/**\r\n * Store adapter interface that all adapters must implement\r\n */\r\nexport interface StoreAdapter> {\r\n /**\r\n * Subscribe to state changes.\r\n * Compatible with useSyncExternalStore's subscribe parameter.\r\n *\r\n * @param listener - Callback to invoke when state changes\r\n * @returns Unsubscribe function\r\n */\r\n subscribe(listener: () => void): () => void;\r\n\r\n /**\r\n * Get the current state snapshot.\r\n * Compatible with useSyncExternalStore's getSnapshot parameter.\r\n *\r\n * @returns Current state snapshot with version\r\n */\r\n getSnapshot(): StoreSnapshot;\r\n\r\n /**\r\n * Get the server-side state snapshot (for SSR).\r\n * Only implemented by URL-based adapters.\r\n *\r\n * @returns Server state snapshot\r\n */\r\n getServerSnapshot?(): StoreSnapshot;\r\n\r\n /**\r\n * Update state with partial values.\r\n * Must use immutable updates (new references for changed values).\r\n *\r\n * @param partial - Partial state to merge\r\n */\r\n setState(partial: Partial): void;\r\n\r\n /**\r\n * Update a single field value.\r\n *\r\n * @param key - Field key\r\n * @param value - New value\r\n */\r\n setField(key: K,value: T[K]): void;\r\n\r\n /**\r\n * Reset state to defaults.\r\n *\r\n * @param fields - Optional array of fields to reset. If omitted, resets all.\r\n */\r\n reset(fields?: (keyof T)[]): void;\r\n\r\n /**\r\n * Pause state updates (for live mode).\r\n * While paused, setState calls are queued.\r\n */\r\n pause(): void;\r\n\r\n /**\r\n * Resume state updates.\r\n * Applies any queued state changes.\r\n */\r\n resume(): void;\r\n\r\n /**\r\n * Check if updates are paused.\r\n */\r\n isPaused(): boolean;\r\n\r\n /**\r\n * Cleanup resources when adapter is destroyed.\r\n */\r\n destroy(): void;\r\n\r\n /**\r\n * Get the unique table ID for this adapter.\r\n */\r\n getTableId(): string;\r\n\r\n /**\r\n * Get the schema definition used by this adapter.\r\n */\r\n getSchema(): SchemaDefinition;\r\n\r\n /**\r\n * Get the default values from the schema.\r\n */\r\n getDefaults(): T;\r\n}\r\n\r\n/**\r\n * Options for creating an adapter\r\n */\r\nexport interface CreateAdapterOptions> {\r\n /**\r\n * Unique ID for this table (required for multi-table support)\r\n */\r\n id: string;\r\n\r\n /**\r\n * Initial state to override defaults\r\n */\r\n initialState?: Partial;\r\n}\r\n\r\n/**\r\n * Factory function type for creating adapters\r\n */\r\nexport type AdapterFactory>=(\r\n schema: SchemaDefinition,\r\n options: CreateAdapterOptions,\r\n) => StoreAdapter;\r\n\r\n/**\r\n * Available adapter types\r\n */\r\nexport type AdapterType=\"nuqs\"|\"zustand\";\r\n\r\n/**\r\n * Internal adapter with additional methods for providers\r\n * @internal\r\n */\r\nexport interface InternalStoreAdapter>\r\n extends StoreAdapter {\r\n /**\r\n * Internal method to sync external state (used by nuqs adapter)\r\n * @internal\r\n */\r\n _syncState?(state: T): void;\r\n\r\n /**\r\n * Internal method to get the state setter (used by nuqs adapter)\r\n * @internal\r\n */\r\n _getStateSetter?(): ((partial: Partial) => void)|null;\r\n\r\n /**\r\n * Internal method to set the state setter (used by nuqs adapter)\r\n * @internal\r\n */\r\n _setStateSetter?(setter: (partial: Partial) => void): void;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/adapter/adapterTypes.ts" }, { "path": "src/lib/store/adapters/nuqs/nuqsTypes.ts", "content": "/**\r\n * nuqs Adapter Types\r\n */\r\n\r\nimport type {CreateAdapterOptions} from \"../../adapter/adapterTypes\";\r\n\r\n/**\r\n * nuqs-specific adapter options\r\n */\r\nexport interface NuqsAdapterOptions>\r\n extends CreateAdapterOptions {\r\n /**\r\n * Use shallow routing (default: true)\r\n */\r\n shallow?: boolean;\r\n\r\n /**\r\n * History mode: 'push' | 'replace' (default: 'push')\r\n */\r\n history?: \"push\"|\"replace\";\r\n\r\n /**\r\n * Scroll to top on change (default: false)\r\n */\r\n scroll?: boolean;\r\n\r\n /**\r\n * Throttle URL updates in milliseconds (default: 50)\r\n */\r\n throttleMs?: number;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/adapters/nuqs/nuqsTypes.ts" }, { "path": "src/lib/store/adapters/nuqs/server.ts", "content": "/**\r\n * nuqs Server Utilities for BYOS\r\n *\r\n * Server-side utilities for creating search params cache and serializers\r\n * from BYOS schema definitions. Import this from server components or API routes.\r\n *\r\n * @example\r\n * ```typescript\r\n * // In your schema file (e.g., search-params.ts)\r\n * import { createNuqsSearchParams } from '@/lib/store/adapters/nuqs/server';\r\n * import { filterSchema } from './schema';\r\n *\r\n * // Create search params utilities from schema\r\n * export const {\r\n * searchParamsParser,\r\n * searchParamsCache,\r\n * searchParamsSerializer,\r\n * } = createNuqsSearchParams(filterSchema.definition, {\r\n * // Add extra parsers for pagination, etc.\r\n * extraParsers: {\r\n * size: parseAsInteger.withDefault(40),\r\n * cursor: parseAsTimestamp,\r\n * },\r\n * });\r\n * ```\r\n */\r\n\r\nimport {\r\n createSearchParamsCache,\r\n createSerializer,\r\n type ParserBuilder,\r\n} from \"nuqs/server\";\r\nimport type {SchemaDefinition} from \"../../schema/schemaTypes\";\r\nimport {schemaToNuqsParsers,type SchemaToNuqsParsers} from \"./parser-bridge\";\r\n\r\nexport type {SchemaToNuqsParsers} from \"./parser-bridge\";\r\n\r\n/**\r\n * Options for creating nuqs search params utilities\r\n */\r\nexport interface CreateNuqsSearchParamsOptions<\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n TExtra extends Record>={},\r\n> {\r\n /**\r\n * Additional parsers to include (e.g., pagination params)\r\n */\r\n extraParsers?: TExtra;\r\n}\r\n\r\n/**\r\n * Result of createNuqsSearchParams\r\n */\r\nexport interface NuqsSearchParamsResult<\r\n TSchema extends SchemaDefinition,\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n TExtra extends Record>={},\r\n> {\r\n /**\r\n * Combined parser object for useQueryStates\r\n */\r\n searchParamsParser: SchemaToNuqsParsers&TExtra;\r\n\r\n /**\r\n * Search params cache for server-side parsing\r\n */\r\n searchParamsCache: ReturnType<\r\n typeof createSearchParamsCache&TExtra>\r\n >;\r\n\r\n /**\r\n * Serializer for converting state to URL string\r\n */\r\n searchParamsSerializer: ReturnType<\r\n typeof createSerializer&TExtra>\r\n >;\r\n}\r\n\r\n/**\r\n * Create nuqs search params utilities from a BYOS schema\r\n *\r\n * This creates searchParamsParser, searchParamsCache, and searchParamsSerializer\r\n * from a schema definition, with optional extra parsers for pagination etc.\r\n *\r\n * @example\r\n * ```typescript\r\n * import { createNuqsSearchParams } from '@/lib/store/adapters/nuqs/server';\r\n * import { parseAsInteger, parseAsTimestamp } from 'nuqs/server';\r\n *\r\n * export const {\r\n * searchParamsParser,\r\n * searchParamsCache,\r\n * searchParamsSerializer,\r\n * } = createNuqsSearchParams(filterSchema.definition, {\r\n * extraParsers: {\r\n * size: parseAsInteger.withDefault(40),\r\n * start: parseAsInteger.withDefault(0),\r\n * cursor: parseAsTimestamp,\r\n * },\r\n * });\r\n * ```\r\n */\r\nexport function createNuqsSearchParams<\r\n TSchema extends SchemaDefinition,\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n TExtra extends Record>={},\r\n>(\r\n schema: TSchema,\r\n options: CreateNuqsSearchParamsOptions={},\r\n): NuqsSearchParamsResult {\r\n const {extraParsers={} as TExtra}=options;\r\n\r\n // Generate parsers from schema\r\n const schemaParsers=schemaToNuqsParsers(schema);\r\n\r\n // Combine with extra parsers\r\n const searchParamsParser={\r\n ...schemaParsers,\r\n ...extraParsers,\r\n } as SchemaToNuqsParsers&TExtra;\r\n\r\n // Create cache and serializer\r\n const searchParamsCache=createSearchParamsCache(searchParamsParser);\r\n const searchParamsSerializer=createSerializer(searchParamsParser);\r\n\r\n return {\r\n searchParamsParser,\r\n searchParamsCache,\r\n searchParamsSerializer,\r\n };\r\n}\r\n\r\n// Re-export commonly used nuqs parsers for convenience\r\nexport {\r\n createParser,parseAsArrayOf,\r\n parseAsBoolean,\r\n parseAsInteger,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n parseAsTimestamp,type inferParserType,type ParserBuilder\r\n} from \"nuqs/server\";\r\n\r\n// Re-export schema parser utilities\r\nexport {createSchemaSerializer,schemaToNuqsParsers} from \"./parser-bridge\";\r\n\r\n", "type": "registry:lib", "target": "lib/data-grid/store/adapters/nuqs/server.ts" }, { "path": "src/lib/store/adapters/nuqs/parser-bridge.ts", "content": "/**\r\n * Parser Bridge - Converts BYOS schema to nuqs parsers\r\n *\r\n * This module bridges our schema field definitions to nuqs parser format.\r\n */\r\n\r\nimport {SORT_DELIMITER} from \"@/lib/delimiters\";\r\nimport {\r\n createParser,\r\n parseAsArrayOf,\r\n parseAsBoolean,\r\n parseAsInteger,\r\n parseAsString,\r\n parseAsStringLiteral,\r\n parseAsTimestamp,\r\n type ParserBuilder,\r\n} from \"nuqs/server\";\r\nimport type {\r\n FieldBuilder,\r\n FieldConfig,\r\n SchemaDefinition,\r\n} from \"../../schema/schemaTypes\";\r\n\r\n/**\r\n * Type mapping: Schema field type → nuqs ParserBuilder type\r\n */\r\nexport type SchemaToNuqsParsers={\r\n [K in keyof T]: T[K] extends FieldBuilder\r\n ? ParserBuilder\r\n :ParserBuilder;\r\n};\r\n\r\n/**\r\n * Convert a single field config to a nuqs parser\r\n */\r\nfunction fieldConfigToParser(\r\n config: FieldConfig,\r\n): ParserBuilder {\r\n switch (config.type) {\r\n case \"string\":\r\n return parseAsString as ParserBuilder;\r\n\r\n case \"number\":\r\n return parseAsInteger as ParserBuilder;\r\n\r\n case \"boolean\":\r\n return parseAsBoolean as ParserBuilder;\r\n\r\n case \"timestamp\":\r\n return parseAsTimestamp as ParserBuilder;\r\n\r\n case \"stringLiteral\":\r\n if (config.literals) {\r\n return parseAsStringLiteral(\r\n config.literals as unknown as readonly string[],\r\n ) as ParserBuilder;\r\n }\r\n return parseAsString as ParserBuilder;\r\n\r\n case \"array\":\r\n if (config.itemConfig) {\r\n const itemParser=fieldConfigToParser(config.itemConfig);\r\n return parseAsArrayOf(\r\n itemParser as ParserBuilder,\r\n config.delimiter,\r\n ) as ParserBuilder;\r\n }\r\n return parseAsArrayOf(\r\n parseAsString,\r\n config.delimiter,\r\n ) as ParserBuilder;\r\n\r\n case \"sort\":\r\n return createParser({\r\n parse(queryValue: string) {\r\n const [id,desc]=queryValue.split(SORT_DELIMITER);\r\n if (!id) return null;\r\n return {id,desc: desc===\"desc\"};\r\n },\r\n serialize(value: {id: string; desc: boolean}) {\r\n return `${value.id}${SORT_DELIMITER}${value.desc? \"desc\":\"asc\"}`;\r\n },\r\n }) as ParserBuilder;\r\n\r\n default:\r\n return parseAsString as ParserBuilder;\r\n }\r\n}\r\n\r\n/**\r\n * Apply default value to a nuqs parser\r\n */\r\nfunction applyDefault(\r\n parser: ParserBuilder,\r\n defaultValue: unknown,\r\n): ParserBuilder {\r\n if (defaultValue!==null&&defaultValue!==undefined) {\r\n // Arrays default to empty array, not worth adding .withDefault\r\n if (Array.isArray(defaultValue)&&defaultValue.length===0) {\r\n return parser;\r\n }\r\n // Only apply withDefault for non-null primitive defaults\r\n if (\r\n typeof defaultValue===\"string\"||\r\n typeof defaultValue===\"number\"||\r\n typeof defaultValue===\"boolean\"||\r\n defaultValue instanceof Date\r\n ) {\r\n return (\r\n parser as ParserBuilder&{\r\n withDefault: (d: unknown) => ParserBuilder;\r\n }\r\n ).withDefault(defaultValue);\r\n }\r\n }\r\n return parser;\r\n}\r\n\r\n/**\r\n * Convert a schema definition to nuqs parsers\r\n *\r\n * @example\r\n * ```typescript\r\n * const schema = createSchema({\r\n * level: field.array(field.string()),\r\n * latency: field.number(),\r\n * });\r\n *\r\n * // Type is inferred: { level: ParserBuilder, latency: ParserBuilder }\r\n * const parsers = schemaToNuqsParsers(schema.definition);\r\n * ```\r\n */\r\nexport function schemaToNuqsParsers(\r\n schema: T,\r\n): SchemaToNuqsParsers {\r\n const parsers: Record>={};\r\n\r\n for (const [key,fieldBuilder] of Object.entries(schema)) {\r\n const config=fieldBuilder._config;\r\n let parser=fieldConfigToParser(config);\r\n parser=applyDefault(parser,config.defaultValue);\r\n parsers[key]=parser;\r\n }\r\n\r\n return parsers as SchemaToNuqsParsers;\r\n}\r\n\r\n/**\r\n * Create a nuqs cache serializer from schema\r\n */\r\nexport function createSchemaSerializer(schema: SchemaDefinition) {\r\n const parsers=schemaToNuqsParsers(schema);\r\n\r\n return (state: Record): string => {\r\n const params=new URLSearchParams();\r\n\r\n for (const [key,parser] of Object.entries(parsers)) {\r\n const value=state[key];\r\n if (value===null||value===undefined) continue;\r\n if (Array.isArray(value)&&value.length===0) continue;\r\n\r\n try {\r\n const serialized=(\r\n parser as {serialize?: (v: unknown) => string}\r\n ).serialize?.(value);\r\n if (serialized) {\r\n params.set(key,serialized);\r\n }\r\n } catch {\r\n // Skip invalid values\r\n }\r\n }\r\n\r\n const str=params.toString();\r\n return str? `?${str}`:\"\";\r\n };\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/adapters/nuqs/parser-bridge.ts" }, { "path": "src/lib/store/adapters/nuqs/index.ts", "content": "/**\r\n * nuqs Adapter for BYOS\r\n *\r\n * This adapter uses nuqs for URL-based state management.\r\n * It supports SSR and URL synchronization.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport {useQueryStates,type ParserBuilder} from \"nuqs\";\r\nimport {useEffect,useMemo,useRef} from \"react\";\r\nimport type {InternalStoreAdapter} from \"../../adapter/adapterTypes\";\r\nimport type {SchemaDefinition,StoreSnapshot} from \"../../schema/schemaTypes\";\r\nimport {getSchemaDefaults,validateState} from \"../../schema/serialization\";\r\nimport type {NuqsAdapterOptions} from \"./nuqsTypes\";\r\nimport {schemaToNuqsParsers} from \"./parser-bridge\";\r\n\r\n/**\r\n * Create a nuqs adapter for URL-based state management\r\n *\r\n * @example\r\n * ```typescript\r\n * const schema = createSchema({\r\n * regions: field.array(field.string()).default([]),\r\n * host: field.string(),\r\n * });\r\n *\r\n * function MyComponent() {\r\n * const adapter = useNuqsAdapter(schema.definition, { id: 'my-table' });\r\n * return (\r\n * \r\n * \r\n * \r\n * );\r\n * }\r\n * ```\r\n */\r\nexport function useNuqsAdapter>(\r\n schema: SchemaDefinition,\r\n options: NuqsAdapterOptions,\r\n): InternalStoreAdapter {\r\n const {\r\n id,\r\n initialState,\r\n shallow=true,\r\n history=\"push\",\r\n scroll=false,\r\n throttleMs=50,\r\n }=options;\r\n\r\n const parsers=useMemo(() => schemaToNuqsParsers(schema),[schema]);\r\n const defaults=useMemo(() => getSchemaDefaults(schema) as T,[schema]);\r\n\r\n // Use nuqs hook\r\n const [nuqsState,setNuqsState]=useQueryStates(\r\n parsers as Record>,\r\n {\r\n shallow,\r\n history,\r\n scroll,\r\n throttleMs,\r\n },\r\n );\r\n\r\n // Store state and version\r\n const stateRef=useRef(defaults);\r\n const versionRef=useRef(0);\r\n const listenersRef=useRef(new Set<() => void>());\r\n const pausedRef=useRef(false);\r\n const pendingStateRef=useRef|null>(null);\r\n\r\n // Cache server snapshot to avoid infinite loop with useSyncExternalStore\r\n const serverSnapshotRef=useRef>({\r\n state: {...defaults,...initialState} as T,\r\n version: 0,\r\n });\r\n\r\n // Sync nuqs state to our state ref synchronously (needed for first render)\r\n const validated=validateState(schema,nuqsState) as T;\r\n const currentState={...defaults,...initialState,...validated};\r\n if (stateRef.current!==currentState) {\r\n stateRef.current=currentState;\r\n }\r\n\r\n // Also update via effect to trigger listeners on subsequent changes\r\n useEffect(() => {\r\n const validated=validateState(schema,nuqsState) as T;\r\n const merged={...defaults,...initialState,...validated};\r\n stateRef.current=merged;\r\n versionRef.current++;\r\n listenersRef.current.forEach((listener) => listener());\r\n },[nuqsState,schema,defaults,initialState]);\r\n\r\n // Create stable adapter reference\r\n const adapter=useMemo>(() => {\r\n return {\r\n subscribe(listener: () => void) {\r\n listenersRef.current.add(listener);\r\n return () => {\r\n listenersRef.current.delete(listener);\r\n };\r\n },\r\n\r\n getSnapshot(): StoreSnapshot {\r\n return {\r\n state: stateRef.current,\r\n version: versionRef.current,\r\n };\r\n },\r\n\r\n getServerSnapshot(): StoreSnapshot {\r\n // Return cached snapshot to avoid infinite loop with useSyncExternalStore\r\n return serverSnapshotRef.current;\r\n },\r\n\r\n setState(partial: Partial) {\r\n if (pausedRef.current) {\r\n pendingStateRef.current={\r\n ...pendingStateRef.current,\r\n ...partial,\r\n };\r\n return;\r\n }\r\n\r\n // Convert undefined values to null for nuqs\r\n const nuqsPartial: Record={};\r\n for (const [key,value] of Object.entries(partial)) {\r\n nuqsPartial[key]=value===undefined? null:value;\r\n }\r\n\r\n setNuqsState((prev) => ({...prev,...nuqsPartial}));\r\n },\r\n\r\n setField(key: K,value: T[K]) {\r\n this.setState({[key]: value} as unknown as Partial);\r\n },\r\n\r\n reset(fields?: (keyof T)[]) {\r\n if (fields) {\r\n const resetPartial: Partial={};\r\n for (const field of fields) {\r\n resetPartial[field]=defaults[field];\r\n }\r\n this.setState(resetPartial);\r\n } else {\r\n this.setState(defaults);\r\n }\r\n },\r\n\r\n pause() {\r\n pausedRef.current=true;\r\n },\r\n\r\n resume() {\r\n pausedRef.current=false;\r\n if (pendingStateRef.current) {\r\n this.setState(pendingStateRef.current);\r\n pendingStateRef.current=null;\r\n }\r\n },\r\n\r\n isPaused() {\r\n return pausedRef.current;\r\n },\r\n\r\n destroy() {\r\n listenersRef.current.clear();\r\n },\r\n\r\n getTableId() {\r\n return id;\r\n },\r\n\r\n getSchema() {\r\n return schema;\r\n },\r\n\r\n getDefaults() {\r\n return defaults;\r\n },\r\n\r\n // Internal methods for provider sync\r\n _syncState(state: T) {\r\n stateRef.current=state;\r\n versionRef.current++;\r\n listenersRef.current.forEach((listener) => listener());\r\n },\r\n };\r\n },[id,schema,defaults,initialState,setNuqsState]);\r\n\r\n return adapter;\r\n}\r\n\r\n// Re-export parser bridge utilities for advanced use cases\r\nexport {createSchemaSerializer,schemaToNuqsParsers} from \"./parser-bridge\";\r\n\r\n// Re-export nuqs types for components that need them\r\n// This allows components to import from the adapter layer instead of nuqs directly\r\nexport type {ParserBuilder} from \"nuqs\";\r\n\r\n", "type": "registry:lib", "target": "lib/data-grid/store/adapters/nuqs/index.ts" }, { "path": "src/lib/store/parser/text-parser.ts", "content": "/**\r\n * Text Parser Implementation\r\n *\r\n * Standalone module for parsing filter command text input.\r\n */\r\n\r\nimport type {\r\n FieldBuilder,\r\n InferSchemaType,\r\n SchemaDefinition,\r\n} from \"../schema/schemaTypes\";\r\nimport type {TextParser,TextParserOptions} from \"./types\";\r\n\r\n/**\r\n * Create a text parser for filter command input\r\n *\r\n * @example\r\n * ```typescript\r\n * import { createTextParser } from '@/lib/store/parser';\r\n *\r\n * const parser = createTextParser(schema.definition, {\r\n * aliases: { l: 'level', r: 'regions' },\r\n * });\r\n *\r\n * // Parse text to state\r\n * const state = parser.parse('level:error,warn regions:ams');\r\n *\r\n * // Serialize state to text\r\n * const text = parser.serialize({ level: ['error', 'warn'], regions: ['ams'] });\r\n * ```\r\n */\r\nexport function createTextParser(\r\n schema: T,\r\n options: TextParserOptions={},\r\n): TextParser {\r\n const {\r\n aliases={},\r\n fieldDelimiter=\" \",\r\n keyValueDelimiter=\":\",\r\n }=options;\r\n\r\n // Build reverse alias map\r\n const reverseAliases: Record={};\r\n for (const [alias,field] of Object.entries(aliases)) {\r\n reverseAliases[field]=alias;\r\n }\r\n\r\n // Resolve alias to field name\r\n const resolveAlias=(key: string): string => {\r\n return aliases[key]||key;\r\n };\r\n\r\n return {\r\n parse(input: string): Partial> {\r\n const result: Record={};\r\n\r\n if (!input.trim()) {\r\n return result as Partial>;\r\n }\r\n\r\n // Split by field delimiter, but handle quoted values\r\n const parts=input\r\n .trim()\r\n .split(new RegExp(`\\\\s*${escapeRegex(fieldDelimiter)}\\\\s*`));\r\n\r\n for (const part of parts) {\r\n if (!part) continue;\r\n\r\n const colonIndex=part.indexOf(keyValueDelimiter);\r\n if (colonIndex===-1) continue;\r\n\r\n const rawKey=part.slice(0,colonIndex).trim();\r\n const rawValue=part.slice(colonIndex+1).trim();\r\n\r\n if (!rawKey||!rawValue) continue;\r\n\r\n const fieldKey=resolveAlias(rawKey);\r\n const fieldBuilder=schema[fieldKey] as\r\n |FieldBuilder\r\n |undefined;\r\n\r\n if (!fieldBuilder) continue;\r\n\r\n try {\r\n const parsed=fieldBuilder._config.parse(rawValue);\r\n if (parsed!==null) {\r\n result[fieldKey]=parsed;\r\n }\r\n } catch {\r\n // Skip invalid values\r\n }\r\n }\r\n\r\n return result as Partial>;\r\n },\r\n\r\n serialize(state: Partial>): string {\r\n const parts: string[]=[];\r\n\r\n for (const [key,value] of Object.entries(state)) {\r\n if (value===null||value===undefined) continue;\r\n if (Array.isArray(value)&&value.length===0) continue;\r\n\r\n const fieldBuilder=schema[key] as FieldBuilder|undefined;\r\n if (!fieldBuilder) continue;\r\n\r\n try {\r\n const serialized=fieldBuilder._config.serialize(value);\r\n if (serialized) {\r\n parts.push(`${key}${keyValueDelimiter}${serialized}`);\r\n }\r\n } catch {\r\n // Skip invalid values\r\n }\r\n }\r\n\r\n return parts.join(fieldDelimiter);\r\n },\r\n\r\n getWordAtCaret(input: string,caretPosition: number) {\r\n // Find word boundaries\r\n let start=caretPosition;\r\n let end=caretPosition;\r\n\r\n // Move start backwards to find word start\r\n while (start>0&&input[start-1]!==fieldDelimiter) {\r\n start--;\r\n }\r\n\r\n // Move end forwards to find word end\r\n while (end,\r\n ) {\r\n const {word,field,value}=this.getWordAtCaret(input,caretPosition);\r\n\r\n // If we have a field and are typing a value\r\n if (field&&value!==null) {\r\n const options=fieldOptions[field]||[];\r\n const filtered=options.filter((opt) =>\r\n opt.toLowerCase().includes(value.toLowerCase()),\r\n );\r\n\r\n return {\r\n type: \"value\" as const,\r\n field,\r\n suggestions: filtered,\r\n };\r\n }\r\n\r\n // Otherwise, suggest field names\r\n const schemaKeys=Object.keys(schema);\r\n const aliasKeys=Object.keys(aliases);\r\n const allFields=[...schemaKeys,...aliasKeys];\r\n\r\n const filtered=word\r\n ? allFields.filter((f) =>\r\n f.toLowerCase().startsWith(word.toLowerCase()),\r\n )\r\n :allFields;\r\n\r\n return {\r\n type: \"field\" as const,\r\n suggestions: filtered,\r\n };\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Escape special regex characters\r\n */\r\nfunction escapeRegex(str: string): string {\r\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g,\"\\\\$&\");\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/parser/text-parser.ts" }, { "path": "src/lib/store/parser/types.ts", "content": "/**\r\n * Text Parser Types\r\n */\r\n\r\nimport type {InferSchemaType,SchemaDefinition} from \"../schema/schemaTypes\";\r\n\r\n/**\r\n * Options for creating a text parser\r\n */\r\nexport interface TextParserOptions {\r\n /**\r\n * Field aliases for shorthand (e.g., { l: 'level', r: 'regions' })\r\n */\r\n aliases?: Record;\r\n\r\n /**\r\n * Delimiter between field:value pairs (default: ' ')\r\n */\r\n fieldDelimiter?: string;\r\n\r\n /**\r\n * Delimiter between field and value (default: ':')\r\n */\r\n keyValueDelimiter?: string;\r\n}\r\n\r\n/**\r\n * Text parser interface\r\n */\r\nexport interface TextParser {\r\n /**\r\n * Parse a text input string into filter state\r\n *\r\n * @example\r\n * ```typescript\r\n * parser.parse('regions:ams,gru latency:0-1000');\r\n * // => { regions: ['ams', 'gru'], latency: [0, 1000] }\r\n * ```\r\n */\r\n parse(input: string): Partial>;\r\n\r\n /**\r\n * Serialize filter state to text format\r\n *\r\n * @example\r\n * ```typescript\r\n * parser.serialize({ regions: ['ams', 'gru'], latency: [0, 1000] });\r\n * // => 'regions:ams,gru latency:0-1000'\r\n * ```\r\n */\r\n serialize(state: Partial>): string;\r\n\r\n /**\r\n * Get the current word at caret position\r\n */\r\n getWordAtCaret(\r\n input: string,\r\n caretPosition: number,\r\n ): {\r\n word: string;\r\n start: number;\r\n end: number;\r\n field: string|null;\r\n value: string|null;\r\n };\r\n\r\n /**\r\n * Replace the current word at caret position\r\n */\r\n replaceWordAtCaret(\r\n input: string,\r\n caretPosition: number,\r\n replacement: string,\r\n ): {\r\n newInput: string;\r\n newCaretPosition: number;\r\n };\r\n\r\n /**\r\n * Get suggestions for autocomplete\r\n */\r\n getSuggestions(\r\n input: string,\r\n caretPosition: number,\r\n fieldOptions: Record,\r\n ): {\r\n type: \"field\"|\"value\";\r\n field?: string;\r\n suggestions: string[];\r\n };\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/parser/types.ts" }, { "path": "src/lib/store/provider/DataTableStoreProvider.tsx", "content": "/**\r\n * DataTableStoreProvider - Main provider for BYOS\r\n *\r\n * Wraps components with store adapter context.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport React,{useEffect,useMemo} from \"react\";\r\nimport type {StoreAdapter} from \"../adapter/adapterTypes\";\r\nimport {StoreContext,type StoreContextValue} from \"../context\";\r\n\r\nexport interface DataTableStoreProviderProps<\r\n T extends Record,\r\n> {\r\n /**\r\n * The store adapter to use\r\n */\r\n adapter: StoreAdapter;\r\n\r\n /**\r\n * Child components\r\n */\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * Provider component for data table store\r\n *\r\n * @example\r\n * ```typescript\r\n * const adapter = useNuqsAdapter(schema.definition, { id: 'my-table' });\r\n *\r\n * return (\r\n * \r\n * \r\n * \r\n * );\r\n * ```\r\n */\r\nexport function DataTableStoreProvider>({\r\n adapter,\r\n children,\r\n}: DataTableStoreProviderProps) {\r\n const value=useMemo>(\r\n () => ({\r\n adapter,\r\n schema: adapter.getSchema(),\r\n tableId: adapter.getTableId(),\r\n }),\r\n [adapter],\r\n );\r\n\r\n // Cleanup on unmount\r\n useEffect(() => {\r\n return () => {\r\n adapter.destroy();\r\n };\r\n },[adapter]);\r\n\r\n return (\r\n >}\r\n >\r\n {children}\r\n \r\n );\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/provider/DataTableStoreProvider.tsx" }, { "path": "src/lib/store/hooks/useFilterState.ts", "content": "/**\r\n * useFilterState Hook\r\n *\r\n * Read filter state from the store adapter.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport { useCallback, useSyncExternalStore } from \"react\";\r\nimport { useStoreContext } from \"../context\";\r\n\r\n/**\r\n * Hook to read filter state from the adapter\r\n *\r\n * @example\r\n * ```typescript\r\n * // Read entire state\r\n * const state = useFilterState();\r\n *\r\n * // Read with selector (for performance)\r\n * const regions = useFilterState(s => s.regions);\r\n * ```\r\n */\r\nexport function useFilterState, R = T>(\r\n selector?: (state: T) => R,\r\n): R {\r\n const context = useStoreContext();\r\n\r\n if (!context) {\r\n throw new Error(\r\n \"useFilterState must be used within a DataTableStoreProvider\",\r\n );\r\n }\r\n\r\n const { adapter } = context;\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => adapter.subscribe(onStoreChange),\r\n [adapter],\r\n );\r\n\r\n const getSnapshot = useCallback(() => {\r\n const snapshot = adapter.getSnapshot();\r\n const state = snapshot.state as T;\r\n return selector ? selector(state) : (state as unknown as R);\r\n }, [adapter, selector]);\r\n\r\n const getServerSnapshot = useCallback(() => {\r\n const snapshot = adapter.getServerSnapshot?.() ?? adapter.getSnapshot();\r\n const state = snapshot.state as T;\r\n return selector ? selector(state) : (state as unknown as R);\r\n }, [adapter, selector]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/hooks/useFilterState.ts" }, { "path": "src/lib/store/hooks/useFilterActions.ts", "content": "/**\r\n * useFilterActions Hook\r\n *\r\n * Get actions to modify filter state.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport {useStoreContext} from \"@/lib/store/context\";\r\nimport {useCallback,useMemo} from \"react\";\r\n\r\n/**\r\n * Actions returned by useFilterActions\r\n */\r\nexport interface FilterActions> {\r\n /**\r\n * Set a single filter field value\r\n */\r\n setFilter: (key: K,value: T[K]) => void;\r\n\r\n /**\r\n * Set multiple filter fields at once\r\n */\r\n setFilters: (partial: Partial) => void;\r\n\r\n /**\r\n * Reset a single filter field to its default value\r\n */\r\n resetFilter: (key: keyof T) => void;\r\n\r\n /**\r\n * Reset all filters to default values\r\n */\r\n resetAllFilters: () => void;\r\n\r\n /**\r\n * Pause state updates (for live mode)\r\n */\r\n pause: () => void;\r\n\r\n /**\r\n * Resume state updates\r\n */\r\n resume: () => void;\r\n\r\n /**\r\n * Check if updates are paused\r\n */\r\n isPaused: () => boolean;\r\n}\r\n\r\n/**\r\n * Hook to get filter actions\r\n *\r\n * @example\r\n * ```typescript\r\n * const { setFilter, setFilters, resetAllFilters, pause, resume } = useFilterActions();\r\n *\r\n * // Set single field\r\n * setFilter('regions', ['ams', 'gru']);\r\n *\r\n * // Set multiple fields\r\n * setFilters({ regions: ['ams'], host: 'api.example.com' });\r\n *\r\n * // Reset all\r\n * resetAllFilters();\r\n *\r\n * // Pause/resume for live mode\r\n * pause();\r\n * resume();\r\n * ```\r\n */\r\nexport function useFilterActions>(): FilterActions {\r\n const context=useStoreContext();\r\n\r\n if (!context) {\r\n throw new Error(\r\n \"useFilterActions must be used within a DataTableStoreProvider\",\r\n );\r\n }\r\n\r\n const {adapter}=context;\r\n\r\n const setFilter=useCallback(\r\n (key: K,value: T[K]) => {\r\n adapter.setField(key as string,value);\r\n },\r\n [adapter],\r\n );\r\n\r\n const setFilters=useCallback(\r\n (partial: Partial) => {\r\n adapter.setState(partial as Partial>);\r\n },\r\n [adapter],\r\n );\r\n\r\n const resetFilter=useCallback(\r\n (key: keyof T) => {\r\n adapter.reset([key as string]);\r\n },\r\n [adapter],\r\n );\r\n\r\n const resetAllFilters=useCallback(() => {\r\n adapter.reset();\r\n },[adapter]);\r\n\r\n const pause=useCallback(() => {\r\n adapter.pause();\r\n },[adapter]);\r\n\r\n const resume=useCallback(() => {\r\n adapter.resume();\r\n },[adapter]);\r\n\r\n const isPaused=useCallback(() => {\r\n return adapter.isPaused();\r\n },[adapter]);\r\n\r\n return useMemo(\r\n () => ({\r\n setFilter,\r\n setFilters,\r\n resetFilter,\r\n resetAllFilters,\r\n pause,\r\n resume,\r\n isPaused,\r\n }),\r\n [\r\n setFilter,\r\n setFilters,\r\n resetFilter,\r\n resetAllFilters,\r\n pause,\r\n resume,\r\n isPaused,\r\n ],\r\n );\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/hooks/useFilterActions.ts" }, { "path": "src/lib/store/hooks/useFilterField.ts", "content": "/**\r\n * useFilterField Hook\r\n *\r\n * Hook for working with a single filter field.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport { useCallback, useMemo, useSyncExternalStore } from \"react\";\r\nimport { useStoreContext } from \"../context\";\r\n\r\n/**\r\n * Return type for useFilterField\r\n */\r\nexport interface FilterFieldResult {\r\n /**\r\n * Current value of the field\r\n */\r\n value: T;\r\n\r\n /**\r\n * Set the field value\r\n */\r\n setValue: (value: T) => void;\r\n\r\n /**\r\n * Reset the field to its default value\r\n */\r\n reset: () => void;\r\n}\r\n\r\n/**\r\n * Hook to work with a single filter field\r\n *\r\n * @example\r\n * ```typescript\r\n * const { value, setValue, reset } = useFilterField('regions');\r\n *\r\n * // Read value\r\n * console.log(value); // ['ams', 'gru']\r\n *\r\n * // Update value\r\n * setValue(['ams', 'gru', 'fra']);\r\n *\r\n * // Reset to default\r\n * reset();\r\n * ```\r\n */\r\nexport function useFilterField<\r\n T extends Record,\r\n K extends keyof T,\r\n>(key: K): FilterFieldResult {\r\n const context = useStoreContext();\r\n\r\n if (!context) {\r\n throw new Error(\r\n \"useFilterField must be used within a DataTableStoreProvider\",\r\n );\r\n }\r\n\r\n const { adapter } = context;\r\n\r\n const subscribe = useCallback(\r\n (onStoreChange: () => void) => adapter.subscribe(onStoreChange),\r\n [adapter],\r\n );\r\n\r\n const getSnapshot = useCallback(() => {\r\n const snapshot = adapter.getSnapshot();\r\n return (snapshot.state as T)[key];\r\n }, [adapter, key]);\r\n\r\n const getServerSnapshot = useCallback(() => {\r\n const snapshot = adapter.getServerSnapshot?.() ?? adapter.getSnapshot();\r\n return (snapshot.state as T)[key];\r\n }, [adapter, key]);\r\n\r\n const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\r\n\r\n const setValue = useCallback(\r\n (newValue: T[K]) => {\r\n adapter.setField(key as string, newValue);\r\n },\r\n [adapter, key],\r\n );\r\n\r\n const reset = useCallback(() => {\r\n adapter.reset([key as string]);\r\n }, [adapter, key]);\r\n\r\n return useMemo(() => ({ value, setValue, reset }), [value, setValue, reset]);\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/hooks/useFilterField.ts" }, { "path": "src/lib/store/hooks/useReactTableSync.ts", "content": "/**\r\n * useReactTableSync Hook\r\n *\r\n * Syncs BYOS filter state with React Table's columnFilters.\r\n * This hook provides bidirectional synchronization between the adapter state\r\n * and React Table's internal state.\r\n */\r\n\r\n\"use client\";\r\n\r\nimport type { DataTableFilterField } from \"@/components/data-table/types\";\r\nimport type { ColumnFiltersState, Table } from \"@tanstack/react-table\";\r\nimport { useCallback, useEffect, useRef } from \"react\";\r\nimport { useFilterActions } from \"./useFilterActions\";\r\nimport { useFilterState } from \"./useFilterState\";\r\n\r\ninterface UseReactTableSyncOptions {\r\n /**\r\n * React Table instance\r\n */\r\n table: Table;\r\n\r\n /**\r\n * Filter field definitions (to know which fields to sync)\r\n */\r\n filterFields: DataTableFilterField[];\r\n\r\n /**\r\n * Callback when column filters change (from table)\r\n */\r\n onColumnFiltersChange?: (filters: ColumnFiltersState) => void;\r\n}\r\n\r\n/**\r\n * Hook to synchronize BYOS adapter state with React Table\r\n *\r\n * @example\r\n * ```typescript\r\n * const table = useReactTable({ ... });\r\n *\r\n * useReactTableSync({\r\n * table,\r\n * filterFields,\r\n * });\r\n * ```\r\n */\r\nexport function useReactTableSync({\r\n table,\r\n filterFields,\r\n onColumnFiltersChange,\r\n}: UseReactTableSyncOptions) {\r\n const filterState = useFilterState>();\r\n const { setFilters } = useFilterActions>();\r\n\r\n // Track if we're currently syncing to avoid loops\r\n const isSyncingRef = useRef(false);\r\n\r\n // Sync BYOS state → React Table\r\n useEffect(() => {\r\n if (isSyncingRef.current) return;\r\n\r\n isSyncingRef.current = true;\r\n\r\n // Convert filter state to column filters format\r\n for (const field of filterFields) {\r\n const fieldKey = field.value as string;\r\n const value = filterState[fieldKey];\r\n const column = table.getColumn(fieldKey);\r\n\r\n if (column) {\r\n // Only update if value is different\r\n const currentValue = column.getFilterValue();\r\n if (!isEqual(currentValue, value)) {\r\n column.setFilterValue(value ?? undefined);\r\n }\r\n }\r\n }\r\n\r\n isSyncingRef.current = false;\r\n }, [filterState, filterFields, table]);\r\n\r\n // Sync React Table → BYOS state (via callback)\r\n const syncFromTable = useCallback(() => {\r\n if (isSyncingRef.current) return;\r\n\r\n isSyncingRef.current = true;\r\n\r\n const columnFilters = table.getState().columnFilters;\r\n const updates: Record = {};\r\n\r\n for (const field of filterFields) {\r\n const fieldKey = field.value as string;\r\n const filter = columnFilters.find((f) => f.id === fieldKey);\r\n updates[fieldKey] = filter?.value ?? null;\r\n }\r\n\r\n setFilters(updates);\r\n onColumnFiltersChange?.(columnFilters);\r\n\r\n isSyncingRef.current = false;\r\n }, [table, filterFields, setFilters, onColumnFiltersChange]);\r\n\r\n return {\r\n syncFromTable,\r\n };\r\n}\r\n\r\n/**\r\n * Simple equality check for filter values\r\n */\r\nfunction isEqual(a: unknown, b: unknown): boolean {\r\n if (a === b) return true;\r\n if (a === null || b === null) return a === b;\r\n if (a === undefined || b === undefined) return a === b;\r\n\r\n if (Array.isArray(a) && Array.isArray(b)) {\r\n if (a.length !== b.length) return false;\r\n return a.every((val, i) => isEqual(val, b[i]));\r\n }\r\n\r\n if (a instanceof Date && b instanceof Date) {\r\n return a.getTime() === b.getTime();\r\n }\r\n\r\n if (typeof a === \"object\" && typeof b === \"object\") {\r\n const keysA = Object.keys(a as object);\r\n const keysB = Object.keys(b as object);\r\n if (keysA.length !== keysB.length) return false;\r\n return keysA.every((key) =>\r\n isEqual(\r\n (a as Record)[key],\r\n (b as Record)[key],\r\n ),\r\n );\r\n }\r\n\r\n return false;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/store/hooks/useReactTableSync.ts" }, { "path": "src/lib/store/hooks/index.ts", "content": "/**\r\n * BYOS Hooks Exports\r\n */\r\n\r\nexport { useFilterState } from \"./useFilterState\";\r\nexport { useFilterActions, type FilterActions } from \"./useFilterActions\";\r\nexport { useFilterField, type FilterFieldResult } from \"./useFilterField\";\r\nexport { useReactTableSync } from \"./useReactTableSync\";\r\n", "type": "registry:lib", "target": "lib/data-grid/store/hooks/index.ts" }, { "path": "src/lib/is-array.ts", "content": "export function isArrayOfNumbers(arr: unknown): arr is number[] {\r\n if (!Array.isArray(arr)) return false;\r\n return arr.every((item) => typeof item===\"number\");\r\n}\r\n\r\nexport function isArrayOfDates(arr: unknown): arr is Date[] {\r\n if (!Array.isArray(arr)) return false;\r\n return arr.every((item) => item instanceof Date);\r\n}\r\n\r\nexport function isArrayOfStrings(arr: unknown): arr is string[] {\r\n if (!Array.isArray(arr)) return false;\r\n return arr.every((item) => typeof item===\"string\");\r\n}\r\n\r\nexport function isArrayOfBooleans(arr: unknown): arr is boolean[] {\r\n if (!Array.isArray(arr)) return false;\r\n return arr.every((item) => typeof item===\"boolean\");\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/is-array.ts" }, { "path": "src/lib/delimiters.ts", "content": "export const ARRAY_DELIMITER = \",\";\r\nexport const SLIDER_DELIMITER = \"-\";\r\nexport const SPACE_DELIMITER = \"_\";\r\nexport const RANGE_DELIMITER = \"-\";\r\nexport const SORT_DELIMITER = \".\";\r\n", "type": "registry:lib", "target": "lib/data-grid/delimiters.ts" }, { "path": "src/lib/compose-refs.ts", "content": "// @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx\r\n\r\nimport * as React from \"react\";\r\n\r\ntype PossibleRef = React.Ref | undefined;\r\n\r\n/**\r\n * Set a given ref to a given value\r\n * This utility takes care of different types of refs: callback refs and RefObject(s)\r\n */\r\nfunction setRef(ref: PossibleRef, value: T) {\r\n if (typeof ref === \"function\") {\r\n ref(value);\r\n } else if (ref !== null && ref !== undefined) {\r\n (ref as React.MutableRefObject).current = value;\r\n }\r\n}\r\n\r\n/**\r\n * A utility to compose multiple refs together\r\n * Accepts callback refs and RefObject(s)\r\n */\r\nfunction composeRefs(...refs: PossibleRef[]) {\r\n return (node: T) => refs.forEach((ref) => setRef(ref, node));\r\n}\r\n\r\n/**\r\n * A custom hook that composes multiple refs\r\n * Accepts callback refs and RefObject(s)\r\n */\r\nfunction useComposedRefs(...refs: PossibleRef[]) {\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n return React.useCallback(composeRefs(...refs), refs);\r\n}\r\n\r\nexport { composeRefs, useComposedRefs };\r\n", "type": "registry:lib", "target": "lib/data-grid/compose-refs.ts" }, { "path": "src/components/data-table/data-table-infinite.tsx", "content": "\"use client\";\r\n\r\n// REMINDER: React Compiler is not compatible with TanStack Table v8\r\n// https://github.com/TanStack/table/issues/5567\r\n\"use no memo\";\r\n\r\nimport {DataTableFilterCommand} from \"@/components/data-table/data-table-filter-command/index\";\r\nimport {DataTableFilterControls} from \"@/components/data-table/data-table-filter-controls\";\r\nimport {DataTableProvider} from \"@/components/data-table/data-table-provider\";\r\nimport {DataTableResetButton} from \"@/components/data-table/data-table-reset-button\";\r\nimport {MemoizedDataTableSheetContent} from \"@/components/data-table/data-table-sheet/data-table-sheet-content\";\r\nimport {DataTableSheetDetails} from \"@/components/data-table/data-table-sheet/data-table-sheet-details\";\r\nimport {DataTableToolbar} from \"@/components/data-table/data-table-toolbar\";\r\nimport type {BaseChartSchema,DataTableFilterField,SheetField} from \"@/components/data-table/types\";\r\nimport {Button} from \"@/components/ui/button\";\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from \"@/components/ui/table\";\r\nimport {useHotKey} from \"@/hooks/use-hot-key\";\r\nimport {useLocalStorage} from \"@/hooks/use-local-storage\";\r\nimport {\r\n getColumnOrderKey,\r\n getColumnVisibilityKey,\r\n} from \"@/lib/constants/local-storage\";\r\nimport {formatCompactNumber} from \"@/lib/format\";\r\nimport {useFilterState} from \"@/lib/store/hooks/useFilterState\";\r\nimport type {SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport {arrSome,inDateRange} from \"@/lib/table/filterfns\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {\r\n FetchNextPageOptions,\r\n FetchPreviousPageOptions,\r\n RefetchOptions,\r\n} from \"@tanstack/react-query\";\r\nimport type {\r\n ColumnDef,\r\n ColumnFiltersState,\r\n Row,\r\n RowSelectionState,\r\n SortingState,\r\n TableOptions,\r\n Table as TTable,\r\n VisibilityState,\r\n} from \"@tanstack/react-table\";\r\nimport {\r\n flexRender,\r\n getCoreRowModel,\r\n getFacetedRowModel,\r\n getFilteredRowModel,\r\n getSortedRowModel,\r\n getFacetedMinMaxValues as getTTableFacetedMinMaxValues,\r\n getFacetedUniqueValues as getTTableFacetedUniqueValues,\r\n useReactTable,\r\n} from \"@tanstack/react-table\";\r\nimport {LoaderCircle} from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nexport interface DataTableInfiniteProps {\r\n // ── Core ──────────────────────────────────────────────────────────────────\r\n columns: ColumnDef[];\r\n data: TData[];\r\n schema: SchemaDefinition;\r\n tableId?: string;\r\n\r\n // ── Required scroll props ─────────────────────────────────────────────────\r\n fetchNextPage: (options?: FetchNextPageOptions) => Promise;\r\n refetch: (options?: RefetchOptions) => void;\r\n meta: TMeta;\r\n\r\n // ── State defaults ────────────────────────────────────────────────────────\r\n defaultColumnFilters?: ColumnFiltersState;\r\n defaultColumnSorting?: SortingState;\r\n defaultRowSelection?: RowSelectionState;\r\n defaultColumnVisibility?: VisibilityState;\r\n\r\n // ── Filter / facet ────────────────────────────────────────────────────────\r\n filterFields?: DataTableFilterField[];\r\n sheetFields?: SheetField[];\r\n getFacetedUniqueValues?: (table: TTable,columnId: string) => Map;\r\n getFacetedMinMaxValues?: (table: TTable,columnId: string) => [number,number]|undefined;\r\n\r\n // ── Row options ───────────────────────────────────────────────────────────\r\n getRowClassName?: (row: Row) => string;\r\n getRowId?: TableOptions[\"getRowId\"];\r\n\r\n // ── Counts / loading ──────────────────────────────────────────────────────\r\n totalRows?: number;\r\n filterRows?: number;\r\n totalRowsFetched?: number;\r\n isFetching?: boolean;\r\n isLoading?: boolean;\r\n hasNextPage?: boolean;\r\n fetchPreviousPage?: (options?: FetchPreviousPageOptions) => Promise;\r\n\r\n // ── Chart ─────────────────────────────────────────────────────────────────\r\n /** @deprecated Use renderChart instead. Kept for backwards compatibility. */\r\n chartData?: BaseChartSchema[];\r\n chartDataColumnId?: string;\r\n\r\n // ── Render slots ──────────────────────────────────────────────────────────\r\n renderLiveRow?: (props?: {row: Row}) => React.ReactNode;\r\n renderSheetTitle: (props: {row?: Row}) => React.ReactNode;\r\n /** Renders below the command bar, above the table header */\r\n renderChart?: () => React.ReactNode;\r\n /** Passed to DataTableToolbar — renders after reset button, before view options */\r\n renderActions?: () => React.ReactNode;\r\n /** Renders at the bottom of the sidebar */\r\n renderSidebarFooter?: () => React.ReactNode;\r\n}\r\n\r\nexport function DataTableInfinite({\r\n columns,\r\n getRowClassName,\r\n getRowId,\r\n data,\r\n defaultColumnFilters=[],\r\n defaultColumnSorting=[],\r\n defaultRowSelection={},\r\n defaultColumnVisibility={},\r\n filterFields=[],\r\n sheetFields=[],\r\n isFetching,\r\n isLoading,\r\n fetchNextPage,\r\n hasNextPage,\r\n fetchPreviousPage,\r\n refetch,\r\n totalRows=0,\r\n filterRows=0,\r\n totalRowsFetched=0,\r\n getFacetedUniqueValues,\r\n getFacetedMinMaxValues,\r\n meta,\r\n renderLiveRow,\r\n renderSheetTitle,\r\n renderChart,\r\n renderActions,\r\n renderSidebarFooter,\r\n schema,\r\n tableId=\"infinite\",\r\n}: DataTableInfiniteProps) {\r\n const [columnFilters,setColumnFilters]=\r\n React.useState(defaultColumnFilters);\r\n const [sorting,setSorting]=\r\n React.useState(defaultColumnSorting);\r\n const [rowSelection,setRowSelection]=\r\n React.useState(defaultRowSelection);\r\n const [columnOrder,setColumnOrder]=useLocalStorage(\r\n getColumnOrderKey(tableId),\r\n [],\r\n );\r\n const [columnVisibility,setColumnVisibility]=\r\n useLocalStorage(\r\n getColumnVisibilityKey(tableId),\r\n defaultColumnVisibility,\r\n );\r\n const topBarRef=React.useRef(null);\r\n const tableRef=React.useRef(null);\r\n const [topBarHeight,setTopBarHeight]=React.useState(0);\r\n\r\n const onScroll=React.useCallback(\r\n (e: React.UIEvent) => {\r\n const onPageBottom=\r\n Math.ceil(e.currentTarget.scrollTop+e.currentTarget.clientHeight)>=\r\n e.currentTarget.scrollHeight;\r\n if (onPageBottom&&!isFetching&&totalRowsFetched {\r\n const observer=new ResizeObserver(() => {\r\n const rect=topBarRef.current?.getBoundingClientRect();\r\n if (rect) setTopBarHeight(rect.height);\r\n });\r\n const topBar=topBarRef.current;\r\n if (!topBar) return;\r\n observer.observe(topBar);\r\n return () => observer.unobserve(topBar);\r\n },[topBarRef]);\r\n\r\n const table=useReactTable({\r\n data,\r\n columns,\r\n state: {columnFilters,sorting,columnVisibility,rowSelection,columnOrder},\r\n enableMultiRowSelection: false,\r\n columnResizeMode: \"onChange\",\r\n getRowId,\r\n onColumnVisibilityChange: setColumnVisibility,\r\n onColumnFiltersChange: setColumnFilters,\r\n onRowSelectionChange: setRowSelection,\r\n onSortingChange: setSorting,\r\n onColumnOrderChange: setColumnOrder,\r\n getSortedRowModel: getSortedRowModel(),\r\n getCoreRowModel: getCoreRowModel(),\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getFacetedRowModel: getFacetedRowModel(),\r\n getFacetedUniqueValues: getTTableFacetedUniqueValues(),\r\n getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),\r\n filterFns: {inDateRange,arrSome},\r\n debugAll: true,\r\n meta: {getRowClassName},\r\n });\r\n\r\n const selectedRow=React.useMemo(() => {\r\n if ((isLoading||isFetching)&&!data.length) return;\r\n const selectedRowKey=Object.keys(rowSelection)?.[0];\r\n return table.getCoreRowModel().flatRows.find((row) => row.id===selectedRowKey);\r\n },[rowSelection,table,isLoading,isFetching,data]);\r\n\r\n React.useEffect(() => {\r\n if (isLoading||isFetching) return;\r\n if (Object.keys(rowSelection)?.length&&!selectedRow) {\r\n setRowSelection({});\r\n }\r\n },[rowSelection,selectedRow,isLoading,isFetching]);\r\n\r\n const columnSizeVars=React.useMemo(() => {\r\n const headers=table.getFlatHeaders();\r\n const colSizes: {[key: string]: string}={};\r\n for (let i=0;i {\r\n setColumnOrder([]);\r\n setColumnVisibility(defaultColumnVisibility);\r\n },\"u\");\r\n\r\n const visibleColumnIds=React.useMemo(\r\n () => table.getVisibleLeafColumns().map((c) => c.id).join(\",\"),\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n [table.getState().columnVisibility],\r\n );\r\n const columnOrderString=React.useMemo(() => columnOrder.join(\",\"),[columnOrder]);\r\n\r\n return (\r\n \r\n \r\n \r\n
\r\n
\r\n

Filters

\r\n
\r\n {table.getState().columnFilters.length? :null}\r\n
\r\n
\r\n
\r\n
\r\n \r\n
\r\n {renderSidebarFooter&&(\r\n
\r\n {renderSidebarFooter()}\r\n
\r\n )}\r\n \r\n \r\n \r\n \r\n \r\n {renderChart?.()}\r\n \r\n
\r\n
\r\n \r\n \r\n {table.getHeaderGroups().map((headerGroup) => (\r\n :not(:last-child)]:border-r\",\r\n )}\r\n >\r\n {headerGroup.headers.map((header) => (\r\n .cursor-col-resize]:last:opacity-0\",\r\n header.column.columnDef.meta?.headerClassName,\r\n )}\r\n aria-sort={\r\n header.column.getIsSorted()===\"asc\"\r\n ? \"ascending\"\r\n :header.column.getIsSorted()===\"desc\"\r\n ? \"descending\"\r\n :\"none\"\r\n }\r\n >\r\n {header.isPlaceholder\r\n ? null\r\n :flexRender(header.column.columnDef.header,header.getContext())}\r\n {header.column.getCanResize()&&(\r\n header.column.resetSize()}\r\n onMouseDown={header.getResizeHandler()}\r\n onTouchStart={header.getResizeHandler()}\r\n className={cn(\r\n \"user-select-none absolute -right-2 top-0 z-10 flex h-full w-4 cursor-col-resize touch-none justify-center\",\r\n \"before:absolute before:inset-y-0 before:w-px before:translate-x-px before:bg-border\",\r\n )}\r\n />\r\n )}\r\n \r\n ))}\r\n \r\n ))}\r\n \r\n \r\n {table.getRowModel().rows?.length? (\r\n table.getRowModel().rows.map((row) => (\r\n \r\n {renderLiveRow?.({row})}\r\n \r\n \r\n ))\r\n ):(\r\n \r\n {renderLiveRow?.()}\r\n \r\n \r\n No results.\r\n \r\n \r\n \r\n )}\r\n \r\n \r\n {hasNextPage||isFetching||isLoading? (\r\n fetchNextPage()}\r\n size=\"sm\"\r\n variant=\"outline\"\r\n >\r\n {isFetching? (\r\n \r\n ):null}\r\n Load More\r\n \r\n ):(\r\n

\r\n No more data to load (\r\n \r\n {formatCompactNumber(filterRows)}\r\n {\" \"}\r\n of{\" \"}\r\n \r\n {formatCompactNumber(totalRows)}\r\n {\" \"}\r\n rows)\r\n

\r\n )}\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n\r\nfunction Row({\r\n row,\r\n table,\r\n selected,\r\n visibleColumnIds,\r\n columnOrder,\r\n}: {\r\n row: Row;\r\n table: TTable;\r\n selected?: boolean;\r\n visibleColumnIds: string;\r\n columnOrder: string;\r\n}) {\r\n useFilterState((s: Record) => s.live);\r\n return (\r\n row.toggleSelected()}\r\n onKeyDown={(event) => {\r\n if (event.key===\"Enter\") {\r\n event.preventDefault();\r\n row.toggleSelected();\r\n }\r\n }}\r\n className={cn(\r\n \"[&>:not(:last-child)]:border-r\",\r\n \"outline-1 -outline-offset-1 outline-primary transition-colors focus-visible:bg-muted/50 focus-visible:outline data-[state=selected]:outline\",\r\n table.options.meta?.getRowClassName?.(row),\r\n )}\r\n >\r\n {row.getVisibleCells().map((cell) => (\r\n \r\n {flexRender(cell.column.columnDef.cell,cell.getContext())}\r\n \r\n ))}\r\n \r\n );\r\n}\r\n\r\nconst MemoizedRow=React.memo(\r\n Row,\r\n (prev,next) =>\r\n prev.row.id===next.row.id&&\r\n prev.selected===next.selected&&\r\n prev.visibleColumnIds===next.visibleColumnIds&&\r\n prev.columnOrder===next.columnOrder,\r\n) as typeof Row;\r\n", "type": "registry:component", "target": "components/data-grid/data-table-infinite.tsx" }, { "path": "src/components/data-table/data-table-tree.tsx", "content": "\"use client\";\r\n\r\n// REMINDER: React Compiler is not compatible with TanStack Table v8\r\n// https://github.com/TanStack/table/issues/5567\r\n\"use no memo\";\r\n\r\nimport {DataTableFilterCommand} from \"@/components/data-table/data-table-filter-command/index\";\r\nimport {DataTableFilterControls} from \"@/components/data-table/data-table-filter-controls\";\r\nimport {DataTablePagination} from \"@/components/data-table/data-table-pagination\";\r\nimport {DataTableProvider} from \"@/components/data-table/data-table-provider\";\r\nimport {DataTableToolbar} from \"@/components/data-table/data-table-toolbar\";\r\nimport type {DataTableFilterField,SheetField} from \"@/components/data-table/types\";\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from \"@/components/ui/table\";\r\nimport {useLocalStorage} from \"@/hooks/use-local-storage\";\r\nimport {getColumnVisibilityKey} from \"@/lib/constants/local-storage\";\r\nimport type {SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {FetchNextPageOptions,FetchPreviousPageOptions,RefetchOptions} from \"@tanstack/react-query\";\r\nimport type {\r\n ColumnDef,\r\n ColumnFiltersState,\r\n ExpandedState,\r\n PaginationState,\r\n Row,\r\n RowSelectionState,\r\n SortingState,\r\n TableOptions,\r\n Table as TTable,\r\n VisibilityState,\r\n} from \"@tanstack/react-table\";\r\nimport {\r\n flexRender,\r\n getCoreRowModel,\r\n getExpandedRowModel,\r\n getFacetedMinMaxValues,\r\n getFacetedRowModel,\r\n getFacetedUniqueValues,\r\n getFilteredRowModel,\r\n getPaginationRowModel,\r\n getSortedRowModel,\r\n useReactTable,\r\n} from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\n\r\n/**\r\n * DataTableTreeProps extends the full infinite-capable props interface but makes\r\n * fetchNextPage / refetch / meta optional — tree tables work with static data.\r\n * Pass those props to enable infinite-scroll or manual-pagination in a tree table.\r\n */\r\nexport interface DataTableTreeProps> {\r\n // ── Core ──────────────────────────────────────────────────────────────────\r\n data: TData[];\r\n columns: ColumnDef[];\r\n filterFields?: DataTableFilterField[];\r\n schema: SchemaDefinition;\r\n tableId: string;\r\n\r\n // ── Tree-specific ─────────────────────────────────────────────────────────\r\n /** Return the children for a given row. Defaults to `(row) => (row as any).children`. */\r\n getSubRows?: (row: TData) => TData[]|undefined;\r\n /** Keep ancestor rows visible when any descendant matches. Defaults to true. */\r\n filterFromLeafRows?: boolean;\r\n\r\n // ── State defaults ────────────────────────────────────────────────────────\r\n defaultColumnFilters?: ColumnFiltersState;\r\n defaultSorting?: SortingState;\r\n defaultColumnVisibility?: VisibilityState;\r\n defaultRowSelection?: RowSelectionState;\r\n defaultPagination?: PaginationState;\r\n\r\n // ── Row behaviour ─────────────────────────────────────────────────────────\r\n getRowId?: TableOptions[\"getRowId\"];\r\n getRowClassName?: (row: Row) => string;\r\n\r\n // ── Server-side facets ────────────────────────────────────────────────────\r\n getFacetedUniqueValues?: (table: TTable,columnId: string) => Map;\r\n getFacetedMinMaxValues?: (table: TTable,columnId: string) => [number,number]|undefined;\r\n\r\n // ── Column features ───────────────────────────────────────────────────────\r\n enableColumnOrdering?: boolean;\r\n enableColumnResizing?: boolean;\r\n\r\n // ── Loading / fetch state ─────────────────────────────────────────────────\r\n isLoading?: boolean;\r\n isFetching?: boolean;\r\n totalRows?: number;\r\n filterRows?: number;\r\n totalRowsFetched?: number;\r\n\r\n // ── Infinite scroll support (optional — enables scrolling in tree tables) ─\r\n hasNextPage?: boolean;\r\n fetchNextPage?: (options?: FetchNextPageOptions) => Promise;\r\n fetchPreviousPage?: (options?: FetchPreviousPageOptions) => Promise;\r\n refetch?: (options?: RefetchOptions) => void;\r\n meta?: TMeta;\r\n\r\n // ── Sheet / detail panel ──────────────────────────────────────────────────\r\n sheetFields?: SheetField[];\r\n renderSheetTitle?: (props: {row?: Row}) => React.ReactNode;\r\n\r\n // ── Render slots ──────────────────────────────────────────────────────────\r\n renderActions?: () => React.ReactNode;\r\n renderChart?: () => React.ReactNode;\r\n renderSidebarFooter?: () => React.ReactNode;\r\n}\r\n\r\nexport function DataTableTree>({\r\n columns,\r\n data,\r\n defaultColumnFilters=[],\r\n defaultSorting=[],\r\n defaultColumnVisibility={},\r\n defaultPagination={pageIndex: 0,pageSize: 10},\r\n filterFields=[],\r\n getFacetedUniqueValues: externalGetFacetedUniqueValues,\r\n getFacetedMinMaxValues: externalGetFacetedMinMaxValues,\r\n getSubRows,\r\n filterFromLeafRows=true,\r\n isLoading,\r\n schema,\r\n tableId,\r\n renderActions,\r\n renderChart,\r\n renderSidebarFooter,\r\n}: DataTableTreeProps) {\r\n const [columnFilters,setColumnFilters]=\r\n React.useState(defaultColumnFilters);\r\n const [sorting,setSorting]=\r\n React.useState(defaultSorting);\r\n const [pagination,setPagination]=\r\n React.useState(defaultPagination);\r\n const [expanded,setExpanded]=React.useState({});\r\n const [columnVisibility,setColumnVisibility]=\r\n useLocalStorage(\r\n getColumnVisibilityKey(tableId),\r\n defaultColumnVisibility,\r\n );\r\n\r\n // Reset pagination to page 0 when filters change\r\n React.useEffect(() => {\r\n setPagination((prev) => ({...prev,pageIndex: 0}));\r\n },[columnFilters]);\r\n\r\n // Custom getFacetedUniqueValues that handles array values\r\n const customGetFacetedUniqueValues=React.useCallback(\r\n (table: TTable,columnId: string) => () => {\r\n const facets=getFacetedUniqueValues()(table,columnId)();\r\n const customFacets=new Map();\r\n for (const [key,value] of facets as Map) {\r\n if (Array.isArray(key)) {\r\n for (const k of key) {\r\n customFacets.set(k,(customFacets.get(k)||0)+value);\r\n }\r\n } else {\r\n customFacets.set(key,(customFacets.get(key)||0)+value);\r\n }\r\n }\r\n return customFacets;\r\n },\r\n [],\r\n );\r\n\r\n const table=useReactTable({\r\n data,\r\n columns,\r\n state: {columnFilters,sorting,columnVisibility,pagination,expanded},\r\n onColumnVisibilityChange: setColumnVisibility,\r\n onColumnFiltersChange: setColumnFilters,\r\n onSortingChange: setSorting,\r\n onPaginationChange: setPagination,\r\n onExpandedChange: setExpanded,\r\n // ── Tree options ──────────────────────────────────────────────────────────\r\n getSubRows: getSubRows??((row) => (row as Record).children as TData[]|undefined),\r\n filterFromLeafRows,\r\n getExpandedRowModel: getExpandedRowModel(),\r\n // ── Standard row models ───────────────────────────────────────────────────\r\n getSortedRowModel: getSortedRowModel(),\r\n getCoreRowModel: getCoreRowModel(),\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getFacetedRowModel: getFacetedRowModel(),\r\n getPaginationRowModel: getPaginationRowModel(),\r\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\r\n getFacetedUniqueValues: customGetFacetedUniqueValues,\r\n enableFilters: true,\r\n enableColumnFilters: true,\r\n });\r\n\r\n const getFacetedUniqueValuesForProvider=React.useCallback(\r\n (table: TTable,columnId: string): Map => {\r\n if (externalGetFacetedUniqueValues) {\r\n return externalGetFacetedUniqueValues(table,columnId);\r\n }\r\n return customGetFacetedUniqueValues(table,columnId)();\r\n },\r\n [customGetFacetedUniqueValues,externalGetFacetedUniqueValues],\r\n );\r\n\r\n return (\r\n \r\n
\r\n \r\n \r\n {renderSidebarFooter?.()}\r\n
\r\n
\r\n \r\n {renderChart?.()}\r\n \r\n
\r\n \r\n \r\n {table.getHeaderGroups().map((headerGroup) => (\r\n \r\n {headerGroup.headers.map((header) => (\r\n \r\n {header.isPlaceholder\r\n ? null\r\n :flexRender(\r\n header.column.columnDef.header,\r\n header.getContext(),\r\n )}\r\n \r\n ))}\r\n \r\n ))}\r\n \r\n \r\n {table.getRowModel().rows?.length? (\r\n table.getRowModel().rows.map((row) => (\r\n \r\n {row.getVisibleCells().map((cell) => (\r\n \r\n {flexRender(\r\n cell.column.columnDef.cell,\r\n cell.getContext(),\r\n )}\r\n \r\n ))}\r\n \r\n ))\r\n ):(\r\n \r\n \r\n No results.\r\n \r\n \r\n )}\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/data-table-tree.tsx" }, { "path": "src/components/custom/kbd.tsx", "content": "// Copy Pasta from: https://github.com/sadmann7/shadcn-table/blob/main/src/components/kbd.tsx#L54\r\nimport { cn } from \"@/lib/utils\";\r\nimport { cva, type VariantProps } from \"class-variance-authority\";\r\nimport * as React from \"react\";\r\n\r\nexport const kbdVariants = cva(\r\n \"select-none rounded border px-1.5 py-px font-mono text-[0.7rem] font-normal shadow-sm disabled:opacity-50\",\r\n {\r\n variants: {\r\n variant: {\r\n default: \"bg-accent text-accent-foreground\",\r\n outline: \"bg-background text-foreground\",\r\n },\r\n },\r\n defaultVariants: {\r\n variant: \"default\",\r\n },\r\n },\r\n);\r\n\r\nexport interface KbdProps\r\n extends React.ComponentPropsWithoutRef<\"kbd\">,\r\n VariantProps {\r\n /**\r\n * The title of the `abbr` element inside the `kbd` element.\r\n * @default undefined\r\n * @type string | undefined\r\n * @example title=\"Command\"\r\n */\r\n abbrTitle?: string;\r\n}\r\n\r\nconst Kbd = React.forwardRef(\r\n ({ abbrTitle, children, className, variant, ...props }, ref) => {\r\n return (\r\n \r\n {abbrTitle ? (\r\n \r\n {children}\r\n \r\n ) : (\r\n children\r\n )}\r\n \r\n );\r\n },\r\n);\r\nKbd.displayName = \"Kbd\";\r\n\r\nexport { Kbd };\r\n", "type": "registry:component", "target": "components/data-grid/custom/kbd.tsx" }, { "path": "src/components/custom/sortable.tsx", "content": "\"use client\";\r\n\r\nimport {Button,buttonVariants} from \"@/components/ui/button\";\r\nimport {composeRefs} from \"@/lib/compose-refs\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport type {\r\n DndContextProps,\r\n DraggableSyntheticListeners,\r\n DropAnimation,\r\n UniqueIdentifier,\r\n} from \"@dnd-kit/core\";\r\nimport {\r\n closestCenter,\r\n defaultDropAnimationSideEffects,\r\n DndContext,\r\n DragOverlay,\r\n KeyboardSensor,\r\n MouseSensor,\r\n TouchSensor,\r\n useSensor,\r\n useSensors,\r\n} from \"@dnd-kit/core\";\r\nimport {\r\n restrictToHorizontalAxis,\r\n restrictToParentElement,\r\n restrictToVerticalAxis,\r\n} from \"@dnd-kit/modifiers\";\r\nimport {\r\n arrayMove,\r\n horizontalListSortingStrategy,\r\n SortableContext,\r\n useSortable,\r\n verticalListSortingStrategy,\r\n type SortableContextProps,\r\n} from \"@dnd-kit/sortable\";\r\nimport {CSS} from \"@dnd-kit/utilities\";\r\nimport {Slot,type SlotProps} from \"@radix-ui/react-slot\";\r\nimport type {VariantProps} from \"class-variance-authority\";\r\nimport * as React from \"react\";\r\nimport {createPortal} from \"react-dom\";\r\n\r\ninterface ButtonProps\r\n extends React.ButtonHTMLAttributes,\r\n VariantProps {\r\n asChild?: boolean;\r\n}\r\n\r\nconst orientationConfig={\r\n vertical: {\r\n modifiers: [restrictToVerticalAxis,restrictToParentElement],\r\n strategy: verticalListSortingStrategy,\r\n },\r\n horizontal: {\r\n modifiers: [restrictToHorizontalAxis,restrictToParentElement],\r\n strategy: horizontalListSortingStrategy,\r\n },\r\n mixed: {\r\n modifiers: [restrictToParentElement],\r\n strategy: undefined,\r\n },\r\n};\r\n\r\ninterface SortableProps\r\n extends DndContextProps {\r\n /**\r\n * An array of data items that the sortable component will render.\r\n * @example\r\n * value={[\r\n * { id: 1, name: 'Item 1' },\r\n * { id: 2, name: 'Item 2' },\r\n * ]}\r\n */\r\n value: TData[];\r\n\r\n /**\r\n * An optional callback function that is called when the order of the data items changes.\r\n * It receives the new array of items as its argument.\r\n * @example\r\n * onValueChange={(items) => console.log(items)}\r\n */\r\n onValueChange?: (items: TData[]) => void;\r\n\r\n /**\r\n * An optional callback function that is called when an item is moved.\r\n * It receives an event object with `activeIndex` and `overIndex` properties, representing the original and new positions of the moved item.\r\n * This will override the default behavior of updating the order of the data items.\r\n * @type (event: { activeIndex: number; overIndex: number }) => void\r\n * @example\r\n * onMove={(event) => console.log(`Item moved from index ${event.activeIndex} to index ${event.overIndex}`)}\r\n */\r\n onMove?: (event: {activeIndex: number; overIndex: number}) => void;\r\n\r\n /**\r\n * A collision detection strategy that will be used to determine the closest sortable item.\r\n * @default closestCenter\r\n * @type DndContextProps[\"collisionDetection\"]\r\n */\r\n collisionDetection?: DndContextProps[\"collisionDetection\"];\r\n\r\n /**\r\n * An array of modifiers that will be used to modify the behavior of the sortable component.\r\n * @default\r\n * [restrictToVerticalAxis, restrictToParentElement]\r\n * @type Modifier[]\r\n */\r\n modifiers?: DndContextProps[\"modifiers\"];\r\n\r\n /**\r\n * A sorting strategy that will be used to determine the new order of the data items.\r\n * @default verticalListSortingStrategy\r\n * @type SortableContextProps[\"strategy\"]\r\n */\r\n strategy?: SortableContextProps[\"strategy\"];\r\n\r\n /**\r\n * Specifies the axis for the drag-and-drop operation. It can be \"vertical\", \"horizontal\", or \"both\".\r\n * @default \"vertical\"\r\n * @type \"vertical\" | \"horizontal\" | \"mixed\"\r\n */\r\n orientation?: \"vertical\"|\"horizontal\"|\"mixed\";\r\n\r\n /**\r\n * An optional React node that is rendered on top of the sortable component.\r\n * It can be used to display additional information or controls.\r\n * @default null\r\n * @type React.ReactNode | null\r\n * @example\r\n * overlay={}\r\n */\r\n overlay?: React.ReactNode|null;\r\n}\r\n\r\nfunction Sortable({\r\n value,\r\n onValueChange,\r\n onDragStart,\r\n onDragEnd,\r\n onDragCancel,\r\n collisionDetection=closestCenter,\r\n modifiers,\r\n strategy,\r\n onMove,\r\n orientation=\"vertical\",\r\n overlay,\r\n children,\r\n ...props\r\n}: SortableProps) {\r\n const [activeId,setActiveId]=React.useState(null);\r\n const sensors=useSensors(\r\n useSensor(MouseSensor),\r\n useSensor(TouchSensor),\r\n useSensor(KeyboardSensor),\r\n );\r\n\r\n const config=orientationConfig[orientation];\r\n\r\n return (\r\n {\r\n setActiveId(event.active.id);\r\n onDragStart?.(event);\r\n }}\r\n onDragEnd={(event) => {\r\n const {active,over}=event;\r\n if (over&&active.id!==over?.id) {\r\n const activeIndex=value.findIndex((item) => item.id===active.id);\r\n const overIndex=value.findIndex((item) => item.id===over.id);\r\n\r\n if (onMove) {\r\n onMove({activeIndex,overIndex});\r\n } else {\r\n onValueChange?.(arrayMove(value,activeIndex,overIndex));\r\n }\r\n }\r\n setActiveId(null);\r\n onDragEnd?.(event);\r\n }}\r\n onDragCancel={(event) => {\r\n setActiveId?.(null);\r\n onDragCancel?.(event);\r\n }}\r\n collisionDetection={collisionDetection}\r\n {...props}\r\n >\r\n \r\n {children}\r\n \r\n {overlay\r\n ? // https://docs.dndkit.com/api-documentation/draggable/drag-overlay#portals\r\n createPortal(\r\n {overlay},\r\n document.body,\r\n )\r\n :null}\r\n \r\n );\r\n}\r\n\r\nconst dropAnimationOpts: DropAnimation={\r\n sideEffects: defaultDropAnimationSideEffects({\r\n styles: {\r\n active: {\r\n opacity: \"0.4\",\r\n },\r\n },\r\n }),\r\n};\r\n\r\ninterface SortableOverlayProps\r\n extends React.ComponentPropsWithRef {\r\n activeId?: UniqueIdentifier|null;\r\n}\r\n\r\nconst SortableOverlay=React.forwardRef(\r\n (\r\n {activeId,dropAnimation=dropAnimationOpts,children,...props},\r\n ref,\r\n ) => {\r\n return (\r\n \r\n {activeId? (\r\n \r\n {children}\r\n \r\n ):null}\r\n \r\n );\r\n },\r\n);\r\nSortableOverlay.displayName=\"SortableOverlay\";\r\n\r\ninterface SortableItemContextProps {\r\n attributes: React.HTMLAttributes;\r\n listeners: DraggableSyntheticListeners|undefined;\r\n isDragging?: boolean;\r\n}\r\n\r\nconst SortableItemContext=React.createContext({\r\n attributes: {},\r\n listeners: undefined,\r\n isDragging: false,\r\n});\r\n\r\nfunction useSortableItem() {\r\n const context=React.useContext(SortableItemContext);\r\n\r\n if (!context) {\r\n throw new Error(\"useSortableItem must be used within a SortableItem\");\r\n }\r\n\r\n return context;\r\n}\r\n\r\ninterface SortableItemProps extends SlotProps {\r\n /**\r\n * The unique identifier of the item.\r\n * @example \"item-1\"\r\n * @type UniqueIdentifier\r\n */\r\n value: UniqueIdentifier;\r\n\r\n /**\r\n * Specifies whether the item should act as a trigger for the drag-and-drop action.\r\n * @default false\r\n * @type boolean | undefined\r\n */\r\n asTrigger?: boolean;\r\n\r\n /**\r\n * Merges the item's props into its immediate child.\r\n * @default false\r\n * @type boolean | undefined\r\n */\r\n asChild?: boolean;\r\n}\r\n\r\nconst SortableItem=React.forwardRef(\r\n ({value,asTrigger,asChild,className,...props},ref) => {\r\n const {\r\n attributes,\r\n listeners,\r\n setNodeRef,\r\n transform,\r\n transition,\r\n isDragging,\r\n }=useSortable({id: value});\r\n\r\n const context=React.useMemo(\r\n () => ({\r\n attributes,\r\n listeners,\r\n isDragging,\r\n }),\r\n [attributes,listeners,isDragging],\r\n );\r\n const style: React.CSSProperties={\r\n opacity: isDragging? 0.5:1,\r\n transform: CSS.Translate.toString(transform),\r\n transition,\r\n };\r\n\r\n const Comp=asChild? Slot:\"div\";\r\n\r\n return (\r\n \r\n )}\r\n style={style}\r\n {...(asTrigger? attributes:{})}\r\n {...(asTrigger? listeners:{})}\r\n {...props}\r\n />\r\n \r\n );\r\n },\r\n);\r\nSortableItem.displayName=\"SortableItem\";\r\n\r\ninterface SortableDragHandleProps extends ButtonProps {\r\n withHandle?: boolean;\r\n}\r\n\r\nconst SortableDragHandle=React.forwardRef<\r\n HTMLButtonElement,\r\n SortableDragHandleProps\r\n>(({className,...props},ref) => {\r\n const {attributes,listeners,isDragging}=useSortableItem();\r\n\r\n return (\r\n \r\n );\r\n});\r\nSortableDragHandle.displayName=\"SortableDragHandle\";\r\n\r\nexport {Sortable,SortableDragHandle,SortableItem,SortableOverlay};\r\n\r\n", "type": "registry:component", "target": "components/data-grid/custom/sortable.tsx" }, { "path": "src/hooks/use-hot-key.ts", "content": "import {useEffect,useLayoutEffect,useRef} from \"react\";\r\n\r\nexport function useHotKey(callback: () => void,key: string): void {\r\n // Use ref to always have the latest callback without re-registering the listener\r\n const callbackRef=useRef(callback);\r\n useLayoutEffect(() => {\r\n callbackRef.current=callback;\r\n });\r\n\r\n useEffect(() => {\r\n function handler(e: KeyboardEvent) {\r\n if (e.key===key&&(e.metaKey||e.ctrlKey)) {\r\n // e.preventDefault();\r\n callbackRef.current();\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\",handler);\r\n return () => {\r\n window.removeEventListener(\"keydown\",handler);\r\n };\r\n },[key]);\r\n}\r\n", "type": "registry:hook", "target": "hooks/use-hot-key.ts" }, { "path": "src/lib/format.ts", "content": "import { format } from \"date-fns\";\r\n\r\nexport function formatLatency(ms: number): string {\r\n if (ms >= 1000) {\r\n return (\r\n new Intl.NumberFormat(\"en-US\", {\r\n minimumFractionDigits: 1,\r\n maximumFractionDigits: 1,\r\n }).format(ms / 1000) + \"s\"\r\n );\r\n }\r\n\r\n return (\r\n new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 3 }).format(ms) +\r\n \"ms\"\r\n );\r\n}\r\n\r\nexport function formatMilliseconds(value: number) {\r\n return new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 3 }).format(\r\n value,\r\n );\r\n}\r\n\r\nexport function formatDate(value: Date | string) {\r\n return format(new Date(`${value}`), \"LLL dd, y HH:mm\");\r\n}\r\n\r\nexport function formatCompactNumber(value: number) {\r\n if (value >= 100 && value < 1000) {\r\n return value.toString(); // Keep the number as is if it's in the hundreds\r\n } else if (value >= 1000 && value < 1000000) {\r\n return (value / 1000).toFixed(1) + \"k\"; // Convert to 'k' for thousands\r\n } else if (value >= 1000000) {\r\n return (value / 1000000).toFixed(1) + \"M\"; // Convert to 'M' for millions\r\n } else {\r\n return value.toString(); // Optionally handle numbers less than 100 if needed\r\n }\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/format.ts" }, { "path": "src/providers/controls.tsx", "content": "import { useLocalStorage } from \"@/hooks/use-local-storage\";\r\nimport { CONTROLS_KEY } from \"@/lib/constants/local-storage\";\r\nimport { createContext, useContext } from \"react\";\r\n\r\ninterface ControlsContextType {\r\n open: boolean;\r\n setOpen: React.Dispatch>;\r\n}\r\n\r\nexport const ControlsContext = createContext(null);\r\n\r\nexport function ControlsProvider({ children }: { children: React.ReactNode }) {\r\n const [open, setOpen] = useLocalStorage(CONTROLS_KEY, true);\r\n\r\n return (\r\n \r\n \r\n {children}\r\n \r\n \r\n );\r\n}\r\n\r\nexport function useControls() {\r\n const context = useContext(ControlsContext);\r\n\r\n if (!context) {\r\n throw new Error(\"useControls must be used within a ControlsProvider\");\r\n }\r\n\r\n return context as ControlsContextType;\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/providers/controls.tsx" }, { "path": "src/lib/constants/local-storage.ts", "content": "// Column visibility state per table\r\nexport const getColumnVisibilityKey = (tableId: string) =>\r\n `data-table-visibility-${tableId}`;\r\n\r\n// Column order state per table\r\nexport const getColumnOrderKey = (tableId: string) =>\r\n `data-table-column-order-${tableId}`;\r\n\r\n// Filter command search history per table\r\nexport const getCommandHistoryKey = (tableId: string) =>\r\n `data-table-command-${tableId}`;\r\n\r\n// Controls panel open/close state (global)\r\nexport const CONTROLS_KEY = \"data-table-controls\";\r\n", "type": "registry:lib", "target": "lib/data-grid/constants/local-storage.ts" }, { "path": "src/constants/region.ts", "content": "export const REGIONS = [\"ams\", \"fra\", \"gru\", \"hkg\", \"iad\", \"syd\"] as const;\r\n\r\nexport const VERCEL_EDGE_REGIONS = [\r\n \"hnd1\",\r\n \"sin1\",\r\n \"cpt1\",\r\n \"fra1\",\r\n \"hkg1\",\r\n \"syd1\",\r\n \"gru1\",\r\n \"dub1\",\r\n \"sfo1\",\r\n \"cdg1\",\r\n \"icn1\",\r\n \"kix1\",\r\n \"iad1\",\r\n \"arn1\",\r\n \"bom1\",\r\n \"lhr1\",\r\n \"cle1\",\r\n] as const;\r\n\r\nexport const regions: Record = {\r\n // REGIONS\r\n ams: { label: \"Amsterdam\", flag: \"🇳🇱\" },\r\n fra: { label: \"Frankfurt\", flag: \"🇩🇪\" },\r\n gru: { label: \"Sao Paulo\", flag: \"🇧🇷\" },\r\n hkg: { label: \"Hong Kong\", flag: \"🇭🇰\" },\r\n iad: { label: \"Washington D.C.\", flag: \"🇺🇸\" },\r\n syd: { label: \"Sydney\", flag: \"🇦🇺\" },\r\n // VERCEL EDGE REGIONS\r\n hnd1: { label: \"Tokyo\", flag: \"🇯🇵\" },\r\n sin1: { label: \"Singapore\", flag: \"🇸🇬\" },\r\n cpt1: { label: \"Cape Town\", flag: \"🇿🇦\" },\r\n fra1: { label: \"Paris\", flag: \"🇫🇷\" },\r\n hkg1: { label: \"Hong Kong\", flag: \"🇭🇰\" },\r\n syd1: { label: \"Sydney\", flag: \"🇦🇺\" },\r\n gru1: { label: \"Sao Paulo\", flag: \"🇧🇷\" },\r\n dub1: { label: \"Dublin\", flag: \"🇮🇪\" },\r\n sfo1: { label: \"San Francisco\", flag: \"🇺🇸\" },\r\n cdg1: { label: \"Paris\", flag: \"🇫🇷\" },\r\n icn1: { label: \"Seoul\", flag: \"🇰🇷\" },\r\n kix1: { label: \"Osaka\", flag: \"🇯🇵\" },\r\n iad1: { label: \"Washington D.C.\", flag: \"🇺🇸\" },\r\n arn1: { label: \"Stockholm\", flag: \"🇸🇪\" },\r\n bom1: { label: \"Mumbai\", flag: \"🇮🇳\" },\r\n lhr1: { label: \"London\", flag: \"🇬🇧\" },\r\n cle1: { label: \"Cleveland\", flag: \"🇺🇸\" },\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/constants/region.ts" }, { "path": "src/constants/date-preset.ts", "content": "import type {DatePreset} from \"@/components/data-table/types\";\r\nimport {addDays,addHours,endOfDay,startOfDay} from \"date-fns\";\r\n\r\nexport const presets=[\r\n {\r\n label: \"Today\",\r\n from: startOfDay(new Date()),\r\n to: endOfDay(new Date()),\r\n shortcut: \"d\", // day\r\n },\r\n {\r\n label: \"Yesterday\",\r\n from: startOfDay(addDays(new Date(),-1)),\r\n to: endOfDay(addDays(new Date(),-1)),\r\n shortcut: \"y\",\r\n },\r\n {\r\n label: \"Last hour\",\r\n from: addHours(new Date(),-1),\r\n to: new Date(),\r\n shortcut: \"h\",\r\n },\r\n {\r\n label: \"Last 7 days\",\r\n from: startOfDay(addDays(new Date(),-7)),\r\n to: endOfDay(new Date()),\r\n shortcut: \"w\",\r\n },\r\n {\r\n label: \"Last 14 days\",\r\n from: startOfDay(addDays(new Date(),-14)),\r\n to: endOfDay(new Date()),\r\n shortcut: \"b\", // bi-weekly\r\n },\r\n {\r\n label: \"Last 30 days\",\r\n from: startOfDay(addDays(new Date(),-30)),\r\n to: endOfDay(new Date()),\r\n shortcut: \"m\",\r\n },\r\n] satisfies DatePreset[];\r\n", "type": "registry:lib", "target": "lib/data-grid/constants/date-preset.ts" }, { "path": "src/hooks/use-local-storage.ts", "content": "\"use client\";\r\n\r\nimport { useCallback, useState } from \"react\";\r\n\r\nfunction getItemFromLocalStorage(key: string, fallback: T): T {\r\n if (typeof window === \"undefined\") return fallback;\r\n try {\r\n const item = window.localStorage.getItem(key);\r\n return item ? JSON.parse(item) : fallback;\r\n } catch {\r\n return fallback;\r\n }\r\n}\r\n\r\nexport function useLocalStorage(\r\n key: string,\r\n initialValue: T,\r\n): [T, React.Dispatch>] {\r\n // Initialize directly from localStorage to avoid hydration mismatch\r\n const [storedValue, setStoredValue] = useState(() =>\r\n getItemFromLocalStorage(key, initialValue),\r\n );\r\n\r\n const setValue: React.Dispatch> = useCallback(\r\n (value) => {\r\n setStoredValue((prev) => {\r\n const newValue = value instanceof Function ? value(prev) : value;\r\n // Save to localStorage asynchronously to avoid blocking UI\r\n queueMicrotask(() => {\r\n try {\r\n window.localStorage.setItem(key, JSON.stringify(newValue));\r\n } catch {\r\n // Ignore localStorage errors (quota exceeded, etc.)\r\n }\r\n });\r\n return newValue;\r\n });\r\n },\r\n [key],\r\n );\r\n\r\n return [storedValue, setValue];\r\n}\r\n", "type": "registry:hook", "target": "hooks/use-local-storage.ts" }, { "path": "src/hooks/use-media-query.ts", "content": "import * as React from \"react\";\r\n\r\nexport function useMediaQuery(query: string) {\r\n const [value, setValue] = React.useState(false);\r\n\r\n React.useEffect(() => {\r\n function onChange(event: MediaQueryListEvent) {\r\n setValue(event.matches);\r\n }\r\n\r\n const result = matchMedia(query);\r\n result.addEventListener(\"change\", onChange);\r\n setValue(result.matches);\r\n\r\n return () => result.removeEventListener(\"change\", onChange);\r\n }, [query]);\r\n\r\n return value;\r\n}\r\n", "type": "registry:hook", "target": "hooks/use-media-query.ts" }, { "path": "src/components/custom/input-with-addons.tsx", "content": "import { cn } from \"@/lib/utils\";\r\nimport * as React from \"react\";\r\n\r\nexport interface InputWithAddonsProps\r\n extends React.InputHTMLAttributes {\r\n leading?: React.ReactNode;\r\n trailing?: React.ReactNode;\r\n containerClassName?: string;\r\n}\r\n\r\nconst InputWithAddons = React.forwardRef<\r\n HTMLInputElement,\r\n InputWithAddonsProps\r\n>(({ leading, trailing, containerClassName, className, ...props }, ref) => {\r\n return (\r\n \r\n {leading ? (\r\n
\r\n {leading}\r\n
\r\n ) : null}\r\n \r\n {trailing ? (\r\n
\r\n {trailing}\r\n
\r\n ) : null}\r\n \r\n );\r\n});\r\nInputWithAddons.displayName = \"InputWithAddons\";\r\n\r\nexport { InputWithAddons };\r\n", "type": "registry:component", "target": "components/data-grid/custom/input-with-addons.tsx" }, { "path": "src/hooks/use-debounce.ts", "content": "import * as React from \"react\";\r\n\r\n// consider using https://github.com/xnimorz/use-debounce\r\nexport function useDebounce(value: T, delay?: number): T {\r\n const [debouncedValue, setDebouncedValue] = React.useState(value);\r\n\r\n React.useEffect(() => {\r\n const timer = setTimeout(() => setDebouncedValue(value), delay ?? 500);\r\n\r\n return () => {\r\n clearTimeout(timer);\r\n };\r\n }, [value, delay]);\r\n\r\n return debouncedValue;\r\n}\r\n", "type": "registry:hook", "target": "hooks/use-debounce.ts" }, { "path": "src/components/custom/date-picker-with-range.tsx", "content": "\"use client\";\r\n\r\nimport { kbdVariants } from \"@/components/custom/kbd\";\r\nimport type { DatePreset } from \"@/components/data-table/types\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectGroup,\r\n SelectItem,\r\n SelectLabel,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { presets as defaultPresets } from \"@/constants/date-preset\";\r\nimport { useDebounce } from \"@/hooks/use-debounce\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { format } from \"date-fns\";\r\nimport { Calendar as CalendarIcon } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport type { DateRange } from \"react-day-picker\";\r\n\r\ninterface DatePickerWithRangeProps\r\n extends React.HTMLAttributes {\r\n date: DateRange | undefined;\r\n setDate: (date: DateRange | undefined) => void;\r\n presets?: DatePreset[];\r\n}\r\n\r\nexport function DatePickerWithRange({\r\n className,\r\n date,\r\n setDate,\r\n presets = defaultPresets,\r\n}: DatePickerWithRangeProps) {\r\n const [open, setOpen] = React.useState(false);\r\n React.useEffect(() => {\r\n const down = (e: KeyboardEvent) => {\r\n if (!open) return;\r\n\r\n presets.map((preset) => {\r\n if (preset.shortcut === e.key) {\r\n setDate({ from: preset.from, to: preset.to });\r\n }\r\n });\r\n };\r\n document.addEventListener(\"keydown\", down);\r\n return () => document.removeEventListener(\"keydown\", down);\r\n }, [setDate, presets, open]);\r\n\r\n return (\r\n
\r\n \r\n \r\n \r\n \r\n {date?.from ? (\r\n date.to ? (\r\n \r\n {format(date.from, \"LLL dd, y\")} -{\" \"}\r\n {format(date.to, \"LLL dd, y\")}\r\n \r\n ) : (\r\n format(date.from, \"LLL dd, y\")\r\n )\r\n ) : (\r\n Pick a date\r\n )}\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n \r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n );\r\n}\r\n\r\nfunction DatePresets({\r\n selected,\r\n onSelect,\r\n presets,\r\n}: {\r\n selected: DateRange | undefined;\r\n onSelect: (date: DateRange | undefined) => void;\r\n presets: DatePreset[];\r\n}) {\r\n return (\r\n
\r\n

Date Range

\r\n
\r\n {presets.map(({ label, shortcut, from, to }) => {\r\n const isActive = selected?.from === from && selected?.to === to;\r\n return (\r\n onSelect({ from, to })}\r\n className={cn(\r\n \"flex items-center justify-between gap-6\",\r\n !isActive && \"border border-transparent\",\r\n )}\r\n >\r\n {label}\r\n {shortcut}\r\n \r\n );\r\n })}\r\n
\r\n
\r\n );\r\n}\r\n\r\nfunction DatePresetsSelect({\r\n selected,\r\n onSelect,\r\n presets,\r\n}: {\r\n selected: DateRange | undefined;\r\n onSelect: (date: DateRange | undefined) => void;\r\n presets: DatePreset[];\r\n}) {\r\n function findPreset(from?: Date, to?: Date) {\r\n return presets.find((p) => p.from === from && p.to === to)?.shortcut;\r\n }\r\n const [value, setValue] = React.useState(\r\n findPreset(selected?.from, selected?.to),\r\n );\r\n\r\n React.useEffect(() => {\r\n const preset = findPreset(selected?.from, selected?.to);\r\n if (preset === value) return;\r\n setValue(preset);\r\n }, [selected, presets]);\r\n\r\n return (\r\n {\r\n const preset = presets.find((p) => p.shortcut === v);\r\n if (preset) {\r\n onSelect({ from: preset.from, to: preset.to });\r\n }\r\n }}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n Date Presets\r\n {presets.map(({ label, shortcut }) => {\r\n return (\r\n span:last-child]:flex [&>span:last-child]:w-full [&>span:last-child]:justify-between\"\r\n >\r\n {label}\r\n \r\n {shortcut}\r\n \r\n \r\n );\r\n })}\r\n \r\n \r\n \r\n );\r\n}\r\n\r\n// REMINDER: We can add min max date range validation https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#setting_maximum_and_minimum_dates_and_times\r\nfunction CustomDateRange({\r\n selected,\r\n onSelect,\r\n}: {\r\n selected: DateRange | undefined;\r\n onSelect: (date: DateRange | undefined) => void;\r\n}) {\r\n const [dateFrom, setDateFrom] = React.useState(\r\n selected?.from,\r\n );\r\n const [dateTo, setDateTo] = React.useState(selected?.to);\r\n const debounceDateFrom = useDebounce(dateFrom, 1000);\r\n const debounceDateTo = useDebounce(dateTo, 1000);\r\n\r\n const formatDateForInput = (date: Date | undefined): string => {\r\n if (!date) return \"\";\r\n const utcDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);\r\n return utcDate.toISOString().slice(0, 16);\r\n };\r\n\r\n React.useEffect(() => {\r\n onSelect({ from: debounceDateFrom, to: debounceDateTo });\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [debounceDateFrom, debounceDateTo]);\r\n\r\n return (\r\n
\r\n

Custom Range

\r\n
\r\n
\r\n \r\n {\r\n const newDate = new Date(e.target.value);\r\n if (!Number.isNaN(newDate.getTime())) {\r\n setDateFrom(newDate);\r\n }\r\n }}\r\n disabled={!selected?.from}\r\n />\r\n
\r\n
\r\n \r\n {\r\n const newDate = new Date(e.target.value);\r\n if (!Number.isNaN(newDate.getTime())) {\r\n setDateTo(newDate);\r\n }\r\n }}\r\n disabled={!selected?.to}\r\n />\r\n
\r\n
\r\n
\r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/custom/date-picker-with-range.tsx" }, { "path": "src/hooks/use-copy-to-clipboard.ts", "content": "import { useCallback, useState } from \"react\";\r\nimport { toast } from \"sonner\";\r\n\r\nexport function useCopyToClipboard() {\r\n const [text, setText] = useState(null);\r\n\r\n const copy = useCallback(\r\n async (\r\n text: string,\r\n { timeout, withToast }: { timeout?: number; withToast?: boolean } = {\r\n timeout: 3000,\r\n withToast: false,\r\n },\r\n ) => {\r\n if (!navigator?.clipboard) {\r\n console.warn(\"Clipboard not supported\");\r\n return false;\r\n }\r\n\r\n try {\r\n await navigator.clipboard.writeText(text);\r\n setText(text);\r\n\r\n if (timeout) {\r\n setTimeout(() => {\r\n setText(null);\r\n }, timeout);\r\n }\r\n\r\n if (withToast) {\r\n toast.success(\"Copied to clipboard\");\r\n }\r\n\r\n return true;\r\n } catch (error) {\r\n console.warn(\"Copy failed\", error);\r\n setText(null);\r\n return false;\r\n }\r\n },\r\n [],\r\n );\r\n\r\n return { text, copy, isCopied: text !== null };\r\n}\r\n", "type": "registry:hook", "target": "hooks/use-copy-to-clipboard.ts" }, { "path": "src/components/custom/text-with-tooltip.tsx", "content": "import {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport {cn} from \"@/lib/utils\";\r\nimport {useEffect,useRef,useState} from \"react\";\r\n\r\ninterface TextWithTooltipProps {\r\n text: string|number;\r\n className?: string;\r\n}\r\n\r\nexport function TextWithTooltip({text,className}: TextWithTooltipProps) {\r\n const [isTruncated,setIsTruncated]=useState(false);\r\n const textRef=useRef(null);\r\n\r\n useEffect(() => {\r\n const checkTruncation=() => {\r\n if (textRef.current) {\r\n const {scrollWidth,clientWidth}=textRef.current;\r\n setIsTruncated(scrollWidth>clientWidth);\r\n }\r\n };\r\n\r\n const resizeObserver=new ResizeObserver(() => {\r\n checkTruncation();\r\n });\r\n\r\n if (textRef.current) {\r\n resizeObserver.observe(textRef.current);\r\n }\r\n\r\n checkTruncation();\r\n\r\n return () => {\r\n resizeObserver.disconnect();\r\n };\r\n },[]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {text}\r\n \r\n \r\n {text}\r\n \r\n \r\n );\r\n}\r\n", "type": "registry:component", "target": "components/data-grid/custom/text-with-tooltip.tsx" }, { "path": "src/lib/table/filterfns.ts", "content": "import type {FilterFn} from \"@tanstack/react-table\";\r\nimport {isAfter,isBefore,isSameDay} from \"date-fns\";\r\nimport {isArrayOfDates} from \"../is-array\";\r\n\r\nexport const inDateRange: FilterFn=(row,columnId,value) => {\r\n const date=new Date(row.getValue(columnId));\r\n const [start,end]=value as Date[];\r\n\r\n if (isNaN(date.getTime())) return false;\r\n\r\n // if no end date, check if it's the same day\r\n if (!end) return isSameDay(date,start);\r\n\r\n return isAfter(date,start)&&isBefore(date,end);\r\n};\r\n\r\ninDateRange.autoRemove=(val: unknown) =>\r\n !Array.isArray(val)||!val.length||!isArrayOfDates(val);\r\n\r\nexport const arrSome: FilterFn=(row,columnId,filterValue) => {\r\n if (!Array.isArray(filterValue)) return false;\r\n return filterValue.some((val) => row.getValue(columnId)===val);\r\n};\r\n\r\narrSome.autoRemove=(val: unknown) => !Array.isArray(val)||!val?.length;", "type": "registry:lib", "target": "lib/data-grid/table/filterfns.ts" }, { "path": "src/lib/table-schema/index.ts", "content": "import { col as _col } from \"./col\";\r\nimport { presets } from \"./presets\";\r\nimport { deserializeSchema, serializeSchema } from \"./serialize\";\r\nimport { validateSchema } from \"./validate\";\r\n\r\n/**\r\n * Column builder factories and presets for defining table schemas.\r\n *\r\n * **Primitive factories** — choose based on the data type of the column:\r\n * - `col.string()` — text data (`string`)\r\n * - `col.number()` — numeric data (`number`)\r\n * - `col.boolean()` — boolean data (`boolean`)\r\n * - `col.timestamp()` — date/time data (`Date`)\r\n * - `col.enum(values)` — string union (`T[number]`)\r\n * - `col.array(item)` — array of values (`U[]`)\r\n * - `col.record()` — key-value map (`Record`)\r\n *\r\n * **Presets** — pre-configured builders for common log table patterns:\r\n * - `col.presets.logLevel(values)` — severity levels\r\n * - `col.presets.httpMethod(values)` — HTTP verbs\r\n * - `col.presets.httpStatus(codes?)` — HTTP status codes\r\n * - `col.presets.duration(unit?, slider?)` — timing / latency\r\n * - `col.presets.timestamp()` — sortable timestamp with timerange filter\r\n * - `col.presets.traceId()` — trace / request ID (code display, not filterable)\r\n * - `col.presets.pathname()` — URL path with text search\r\n *\r\n * @example\r\n * ```ts\r\n * import { col, createTableSchema } from \"@/lib/table-schema\";\r\n *\r\n * export const tableSchema = createTableSchema({\r\n * level: col.presets.logLevel(LEVELS).description(\"Log severity\"),\r\n * date: col.presets.timestamp().label(\"Date\").size(200).sheet(),\r\n * latency: col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet(),\r\n * status: col.presets.httpStatus().label(\"Status\").size(60),\r\n * method: col.presets.httpMethod(METHODS).size(69),\r\n * host: col.string().label(\"Host\").size(125).sheet(),\r\n * headers: col.record().label(\"Headers\").hidden().sheet(),\r\n * });\r\n * ```\r\n */\r\nexport const col = { ..._col, presets };\r\nexport type {\r\n ColBuilder,\r\n ColConfig,\r\n ColKind,\r\n ColumnDescriptor,\r\n DisplayConfig,\r\n FilterConfig,\r\n FilterDescriptor,\r\n FilterType,\r\n InferTableType,\r\n SchemaJSON,\r\n SheetConfig,\r\n SheetDescriptor,\r\n TableSchemaDefinition,\r\n} from \"./types\";\r\nexport { generateColumns } from \"./generators/columns\";\r\nexport { generateFilterFields } from \"./generators/filter-fields\";\r\nexport { generateFilterSchema } from \"./generators/filter-schema\";\r\nexport { generateSheetFields } from \"./generators/sheet-fields\";\r\nexport { serializeSchema, deserializeSchema } from \"./serialize\";\r\n\r\n/**\r\n * Derive defaultColumnVisibility from the schema.\r\n * Returns { [key]: false } for every column marked with .hidden().\r\n */\r\nexport function getDefaultColumnVisibility(\r\n schema: import(\"./types\").TableSchemaDefinition,\r\n): Record {\r\n const visibility: Record = {};\r\n for (const [key, builder] of Object.entries(schema)) {\r\n if (builder._config.hidden) {\r\n visibility[key] = false;\r\n }\r\n }\r\n return visibility;\r\n}\r\n\r\n/**\r\n * Create a table schema from a map of col.* builders.\r\n *\r\n * The returned object holds the definition and exposes:\r\n * - `toJSON()` — serializes the schema to a function-free JSON descriptor,\r\n * suitable for AI agents, MCP tools, and `JSON.stringify`.\r\n *\r\n * Use `createTableSchema.fromJSON(json)` to reconstruct a schema from a\r\n * JSON descriptor (e.g. one generated by an AI agent). Custom renderers\r\n * (display.cell, filter.component, sheet.component) are not serialized and\r\n * must be applied manually on top of the reconstructed builders.\r\n *\r\n * @example\r\n * ```ts\r\n * export const tableSchema = createTableSchema({\r\n * level: col.enum(LEVELS).label(\"Level\").defaultOpen().sheet(),\r\n * date: col.timestamp().label(\"Date\").sortable().size(200).sheet(),\r\n * });\r\n *\r\n * export type ColumnSchema = InferTableType;\r\n *\r\n * // Serialize for an AI agent or MCP tool\r\n * const json = tableSchema.toJSON();\r\n * // JSON.stringify(tableSchema) also works — toJSON() is called automatically\r\n *\r\n * // Reconstruct from AI-generated JSON\r\n * const schema = createTableSchema.fromJSON(json);\r\n * ```\r\n */\r\nexport function createTableSchema<\r\n T extends import(\"./types\").TableSchemaDefinition,\r\n>(definition: T): { definition: T; toJSON(): import(\"./types\").SchemaJSON } {\r\n validateSchema(definition);\r\n return {\r\n definition,\r\n toJSON() {\r\n return serializeSchema(definition);\r\n },\r\n };\r\n}\r\n\r\ncreateTableSchema.fromJSON = (\r\n json: import(\"./types\").SchemaJSON,\r\n): {\r\n definition: import(\"./types\").TableSchemaDefinition;\r\n toJSON(): import(\"./types\").SchemaJSON;\r\n} => {\r\n return createTableSchema(deserializeSchema(json));\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/index.ts" }, { "path": "src/lib/table-schema/col.ts", "content": "import type {\r\n ColBuilder,\r\n ColConfig,\r\n DisplayConfig,\r\n FilterConfig,\r\n FilterType,\r\n SheetConfig,\r\n} from \"./types\";\r\n\r\nfunction createColBuilder(\r\n config: ColConfig,\r\n): ColBuilder {\r\n // The implementation uses loose parameter types to satisfy all overload\r\n // signatures at once. TypeScript enforces the constraints at call sites\r\n // via the ColBuilder interface overloads, not here.\r\n const builder = {\r\n get _config() {\r\n return config;\r\n },\r\n\r\n label(text: string): ColBuilder {\r\n return createColBuilder({ ...config, label: text });\r\n },\r\n\r\n description(text: string): ColBuilder {\r\n return createColBuilder({ ...config, description: text });\r\n },\r\n\r\n display(type: string, options?: Record): ColBuilder {\r\n const displayConfig = options\r\n ? ({ type, ...options } as DisplayConfig)\r\n : ({ type } as DisplayConfig);\r\n return createColBuilder({ ...config, display: displayConfig });\r\n },\r\n\r\n filterable(\r\n type?: string,\r\n options?: Record,\r\n ): ColBuilder {\r\n const filterType = (type ||\r\n config.filter?.type ||\r\n \"input\") as FilterConfig[\"type\"];\r\n const existing = config.filter;\r\n const newFilter: FilterConfig = {\r\n type: filterType,\r\n defaultOpen: existing?.defaultOpen ?? false,\r\n commandDisabled: existing?.commandDisabled ?? false,\r\n ...(options ?? {}),\r\n };\r\n return createColBuilder({ ...config, filter: newFilter });\r\n },\r\n\r\n notFilterable(): ColBuilder {\r\n return createColBuilder({ ...config, filter: null });\r\n },\r\n\r\n defaultOpen(): ColBuilder {\r\n if (!config.filter) return createColBuilder(config);\r\n return createColBuilder({\r\n ...config,\r\n filter: { ...config.filter, defaultOpen: true },\r\n });\r\n },\r\n\r\n commandDisabled(): ColBuilder {\r\n if (!config.filter) return createColBuilder(config);\r\n return createColBuilder({\r\n ...config,\r\n filter: { ...config.filter, commandDisabled: true },\r\n });\r\n },\r\n\r\n hidden(): ColBuilder {\r\n return createColBuilder({ ...config, hidden: true });\r\n },\r\n\r\n size(px: number): ColBuilder {\r\n return createColBuilder({ ...config, size: px });\r\n },\r\n\r\n sortable(): ColBuilder {\r\n return createColBuilder({ ...config, sortable: true });\r\n },\r\n\r\n optional(): ColBuilder {\r\n return createColBuilder({ ...config, optional: true });\r\n },\r\n\r\n sheet(sheetConfig?: SheetConfig): ColBuilder {\r\n return createColBuilder({ ...config, sheet: sheetConfig ?? {} });\r\n },\r\n } as ColBuilder;\r\n\r\n return builder;\r\n}\r\n\r\n/**\r\n * A string column.\r\n *\r\n * - Data type: `string`\r\n * - Default display: `\"text\"` (plain text with overflow tooltip)\r\n * - Default filter: `\"input\"` (text search)\r\n * - Allowed filters: `\"input\"`\r\n *\r\n * @example\r\n * col.string().label(\"Host\").size(125).sheet()\r\n * col.string().label(\"Message\").notFilterable().optional().hidden()\r\n */\r\nfunction string(): ColBuilder {\r\n return createColBuilder({\r\n kind: \"string\",\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"text\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: { type: \"input\", defaultOpen: false, commandDisabled: false },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * A numeric column.\r\n *\r\n * - Data type: `number`\r\n * - Default display: `\"number\"` (formatted, with optional unit)\r\n * - Default filter: `\"input\"` (exact match)\r\n * - Allowed filters: `\"input\"` | `\"slider\"` | `\"checkbox\"`\r\n * - Use `\"slider\"` for continuous values (latency, file size)\r\n * - Use `\"checkbox\"` for discrete values (HTTP status codes, port numbers)\r\n *\r\n * @example\r\n * col.number().label(\"Latency\").display(\"number\", { unit: \"ms\" }).filterable(\"slider\", { min: 0, max: 5000 }).sortable()\r\n * col.number().label(\"Status\").filterable(\"checkbox\", { options: [{ label: \"200\", value: 200 }] })\r\n */\r\nfunction number(): ColBuilder {\r\n return createColBuilder({\r\n kind: \"number\",\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"number\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: { type: \"input\", defaultOpen: false, commandDisabled: false },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * A boolean column.\r\n *\r\n * - Data type: `boolean`\r\n * - Default display: `\"boolean\"` (checkmark / dash icon)\r\n * - Default filter: `\"checkbox\"` with `Yes` / `No` options pre-wired\r\n * - Allowed filters: `\"checkbox\"`\r\n *\r\n * @example\r\n * col.boolean().label(\"Cache Hit\").defaultOpen()\r\n */\r\nfunction boolean(): ColBuilder {\r\n return createColBuilder({\r\n kind: \"boolean\",\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"boolean\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: {\r\n type: \"checkbox\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n options: [\r\n { label: \"Yes\", value: true },\r\n { label: \"No\", value: false },\r\n ],\r\n },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * A timestamp column.\r\n *\r\n * - Data type: `Date`\r\n * - Default display: `\"timestamp\"` (relative time, absolute datetime on hover)\r\n * - Default filter: `\"timerange\"` (date range picker)\r\n * - Allowed filters: `\"timerange\"`\r\n *\r\n * @example\r\n * col.timestamp().label(\"Date\").sortable().commandDisabled().size(200).sheet()\r\n */\r\nfunction timestamp(): ColBuilder {\r\n return createColBuilder({\r\n kind: \"timestamp\",\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"timestamp\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: { type: \"timerange\", defaultOpen: false, commandDisabled: false },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * An enum column from a `readonly string[]` union.\r\n *\r\n * - Data type: `T[number]` (union of the provided string literals)\r\n * - Default display: `\"badge\"` (colored chip)\r\n * - Default filter: `\"checkbox\"`\r\n * - Allowed filters: `\"checkbox\"`\r\n *\r\n * Checkbox options are NOT auto-derived from `values` — provide them via\r\n * `.filterable(\"checkbox\", { options: [...] })` or use `col.presets.logLevel()`\r\n * which handles option mapping automatically.\r\n *\r\n * @param values - `as const` array of allowed string values\r\n *\r\n * @example\r\n * col.enum(LEVELS).label(\"Level\").filterable(\"checkbox\", {\r\n * options: LEVELS.map(v => ({ label: v, value: v })),\r\n * }).defaultOpen()\r\n */\r\nfunction colEnum(\r\n values: T,\r\n): ColBuilder {\r\n return createColBuilder({\r\n kind: \"enum\",\r\n enumValues: values,\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"badge\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: { type: \"checkbox\", defaultOpen: false, commandDisabled: false },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * An array column, typically used for multi-value enum fields.\r\n *\r\n * - Data type: `U[]` where `U` is the item builder's type\r\n * - Default display: `\"badge\"` (colored chip per value)\r\n * - Default filter: `\"checkbox\"`\r\n * - Allowed filters: `\"checkbox\"`\r\n *\r\n * Most commonly used as `col.array(col.enum(values))` for tags / regions / labels.\r\n *\r\n * @param itemBuilder - A `ColBuilder` describing the array item type\r\n *\r\n * @example\r\n * col.array(col.enum(REGIONS)).label(\"Regions\").filterable(\"checkbox\", {\r\n * options: REGIONS.map(r => ({ label: r, value: r })),\r\n * })\r\n */\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nfunction array(\r\n itemBuilder: ColBuilder,\r\n): ColBuilder {\r\n return createColBuilder({\r\n kind: \"array\",\r\n arrayItem: itemBuilder._config,\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"badge\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: { type: \"checkbox\", defaultOpen: false, commandDisabled: false },\r\n sheet: null,\r\n });\r\n}\r\n\r\n/**\r\n * A key-value record column.\r\n *\r\n * - Data type: `Record`\r\n * - Default display: `\"text\"`\r\n * - Not filterable (`F = never`)\r\n *\r\n * Use for metadata maps, HTTP headers, environment variables, etc.\r\n * Typically rendered with a custom sheet component (key-value table / tabs).\r\n *\r\n * @example\r\n * col.record().label(\"Headers\").hidden().sheet({\r\n * component: (row) => ,\r\n * className: \"flex-col items-start w-full gap-1\",\r\n * })\r\n */\r\nfunction record(): ColBuilder, never> {\r\n return createColBuilder, never>({\r\n kind: \"record\",\r\n optional: false,\r\n label: \"\",\r\n display: { type: \"text\" },\r\n hidden: false,\r\n sortable: false,\r\n filter: null,\r\n sheet: null,\r\n });\r\n}\r\n\r\nexport const col = {\r\n string,\r\n number,\r\n boolean,\r\n timestamp,\r\n enum: colEnum,\r\n array,\r\n record,\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/col.ts" }, { "path": "src/lib/table-schema/infer.ts", "content": "import type { ColumnDescriptor, FilterDescriptor, SchemaJSON } from \"./types\";\r\n\r\n// Unix ms timestamps are 13-digit numbers (> Sep 2001, < Nov 2286)\r\nconst UNIX_MS_MIN = 1_000_000_000_000;\r\nconst UNIX_MS_MAX = 9_999_999_999_999;\r\n\r\nfunction isIso8601(value: string): boolean {\r\n return (\r\n /^\\d{4}-\\d{2}-\\d{2}(T[\\d:.Z+\\-]+)?$/.test(value) &&\r\n !isNaN(Date.parse(value))\r\n );\r\n}\r\n\r\nfunction isUnixMs(value: number): boolean {\r\n return (\r\n Number.isInteger(value) && value >= UNIX_MS_MIN && value <= UNIX_MS_MAX\r\n );\r\n}\r\n\r\n/** Convert camelCase or snake_case key to a Title Case label. */\r\nfunction keyToLabel(key: string): string {\r\n let label = key.replace(/_/g, \" \");\r\n label = label.replace(/([a-z])([A-Z])/g, \"$1 $2\");\r\n return label.charAt(0).toUpperCase() + label.slice(1);\r\n}\r\n\r\nfunction makeDescriptor(\r\n key: string,\r\n label: string,\r\n dataType: ColumnDescriptor[\"dataType\"],\r\n filter: FilterDescriptor | null,\r\n): ColumnDescriptor {\r\n const displayMap: Record = {\r\n string: \"text\",\r\n number: \"number\",\r\n boolean: \"boolean\",\r\n timestamp: \"timestamp\",\r\n enum: \"badge\",\r\n array: \"badge\",\r\n record: \"text\",\r\n };\r\n return {\r\n key,\r\n label,\r\n dataType,\r\n optional: false,\r\n hidden: false,\r\n sortable: false,\r\n display: { type: displayMap[dataType] ?? \"text\" },\r\n filter,\r\n sheet: null,\r\n };\r\n}\r\n\r\nfunction inferColDescriptor(key: string, values: unknown[]): ColumnDescriptor {\r\n const label = keyToLabel(key);\r\n const nonNull = values.filter((v) => v !== null && v !== undefined);\r\n\r\n if (nonNull.length === 0) {\r\n return makeDescriptor(key, label, \"string\", {\r\n type: \"input\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n }\r\n\r\n // Timestamp: ISO 8601 strings\r\n if (nonNull.every((v) => typeof v === \"string\" && isIso8601(v as string))) {\r\n return makeDescriptor(key, label, \"timestamp\", {\r\n type: \"timerange\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n }\r\n\r\n // Timestamp: Unix ms numbers\r\n if (nonNull.every((v) => typeof v === \"number\" && isUnixMs(v as number))) {\r\n return makeDescriptor(key, label, \"timestamp\", {\r\n type: \"timerange\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n }\r\n\r\n // Boolean\r\n if (nonNull.every((v) => v === true || v === false)) {\r\n return makeDescriptor(key, label, \"boolean\", {\r\n type: \"checkbox\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n }\r\n\r\n // Number\r\n if (nonNull.every((v) => typeof v === \"number\")) {\r\n const nums = nonNull as number[];\r\n const min = Math.min(...nums);\r\n const max = Math.max(...nums);\r\n const filter: FilterDescriptor =\r\n min !== max\r\n ? {\r\n type: \"slider\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n min,\r\n max,\r\n }\r\n : { type: \"input\", defaultOpen: false, commandDisabled: false };\r\n return {\r\n ...makeDescriptor(key, label, \"number\", filter),\r\n display: { type: \"number\" },\r\n };\r\n }\r\n\r\n // Array\r\n if (nonNull.every((v) => Array.isArray(v))) {\r\n const allItems = (nonNull as unknown[][])\r\n .flat()\r\n .filter((v) => v !== null && v !== undefined);\r\n const allStrings =\r\n allItems.length > 0 && allItems.every((v) => typeof v === \"string\");\r\n if (allStrings) {\r\n const distinct = new Set(allItems as string[]);\r\n if (distinct.size <= 10) {\r\n const enumValues = Array.from(distinct);\r\n return {\r\n key,\r\n label,\r\n dataType: \"array\",\r\n arrayItemType: { dataType: \"enum\", enumValues },\r\n optional: false,\r\n hidden: false,\r\n sortable: false,\r\n display: { type: \"badge\" },\r\n filter: {\r\n type: \"checkbox\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n options: enumValues.map((v) => ({ label: v, value: v })),\r\n },\r\n sheet: null,\r\n };\r\n }\r\n }\r\n // Non-enum array: not filterable\r\n return makeDescriptor(key, label, \"array\", null);\r\n }\r\n\r\n // Record (plain object, non-array)\r\n if (nonNull.every((v) => typeof v === \"object\" && !Array.isArray(v))) {\r\n return makeDescriptor(key, label, \"record\", null);\r\n }\r\n\r\n // String: check if enum (≤ 10 distinct values)\r\n if (nonNull.every((v) => typeof v === \"string\")) {\r\n const distinct = new Set(nonNull as string[]);\r\n if (distinct.size <= 10) {\r\n const enumValues = Array.from(distinct);\r\n return {\r\n key,\r\n label,\r\n dataType: \"enum\",\r\n enumValues,\r\n optional: false,\r\n hidden: false,\r\n sortable: false,\r\n display: { type: \"badge\" },\r\n filter: {\r\n type: \"checkbox\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n options: enumValues.map((v) => ({ label: v, value: v })),\r\n },\r\n sheet: null,\r\n };\r\n }\r\n return makeDescriptor(key, label, \"string\", {\r\n type: \"input\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n }\r\n\r\n // Fallback\r\n return makeDescriptor(key, label, \"string\", {\r\n type: \"input\",\r\n defaultOpen: false,\r\n commandDisabled: false,\r\n });\r\n}\r\n\r\n/**\r\n * Infer a SchemaJSON from an array of plain data objects.\r\n *\r\n * Walks all rows, collects per-key values, and infers the best ColKind and\r\n * FilterType for each column using these heuristics:\r\n * - `timestamp`: ISO 8601 strings or Unix-ms numbers\r\n * - `boolean`: all values strictly true/false\r\n * - `number`: all non-null values are typeof \"number\"\r\n * - `enum`: strings with ≤ 10 distinct values across the sample\r\n * - `array`: values are arrays (item type inferred recursively)\r\n * - `record`: values are plain objects (non-array)\r\n * - `string`: fallback\r\n *\r\n * Number columns with min ≠ max get a \"slider\" filter; otherwise \"input\".\r\n */\r\nexport function inferSchemaFromJSON(data: unknown[]): SchemaJSON {\r\n if (!Array.isArray(data) || data.length === 0) {\r\n return { columns: [] };\r\n }\r\n\r\n // Collect all keys and their values across rows (preserving insertion order)\r\n const keyValues = new Map();\r\n\r\n for (const row of data) {\r\n if (typeof row !== \"object\" || row === null || Array.isArray(row)) continue;\r\n for (const [key, value] of Object.entries(row as Record)) {\r\n if (!keyValues.has(key)) keyValues.set(key, []);\r\n keyValues.get(key)!.push(value);\r\n }\r\n }\r\n\r\n const columns = Array.from(keyValues.entries()).map(([key, values]) =>\r\n inferColDescriptor(key, values),\r\n );\r\n\r\n return { columns };\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/infer.ts" }, { "path": "src/lib/table-schema/presets.ts", "content": "import { col } from \"./col\";\r\nimport type { ColBuilder } from \"./types\";\r\n\r\nconst DEFAULT_HTTP_STATUS_CODES = [\r\n 200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504,\r\n];\r\n\r\n/**\r\n * Pre-configured column builders for patterns common in log and observability tables.\r\n *\r\n * Every preset returns a `ColBuilder` with sensible defaults already applied.\r\n * All builders remain fully customizable — chain additional methods to override\r\n * any default (label, size, display, sheet, etc.).\r\n *\r\n * @example\r\n * ```ts\r\n * const tableSchema = createTableSchema({\r\n * level: col.presets.logLevel(LEVELS).description(\"Log severity\"),\r\n * date: col.presets.timestamp().label(\"Date\").size(200).sheet(),\r\n * latency: col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet(),\r\n * status: col.presets.httpStatus().label(\"Status\").size(60),\r\n * method: col.presets.httpMethod(METHODS).size(69),\r\n * path: col.presets.pathname().label(\"Path\").size(130).sheet(),\r\n * traceId: col.presets.traceId().label(\"Request ID\").hidden().sheet(),\r\n * });\r\n * ```\r\n */\r\nexport const presets = {\r\n /**\r\n * A log severity level column.\r\n *\r\n * Defaults: enum + badge display + checkbox filter + `defaultOpen`.\r\n * Checkbox options are auto-derived from `values` — no need to map them manually.\r\n *\r\n * @param values - The allowed severity levels, e.g. `[\"error\", \"warn\", \"info\", \"debug\"] as const`\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.logLevel(LEVELS)\r\n * .label(\"Level\")\r\n * .description(\"Log severity: error > warn > info > debug\")\r\n * .size(27)\r\n * ```\r\n */\r\n logLevel(\r\n values: T,\r\n ): ColBuilder {\r\n return col\r\n .enum(values)\r\n .label(\"Level\")\r\n .filterable(\"checkbox\", {\r\n options: values.map((v) => ({ label: v, value: v })),\r\n })\r\n .defaultOpen();\r\n },\r\n\r\n /**\r\n * An HTTP method column.\r\n *\r\n * Defaults: enum + plain text display + checkbox filter.\r\n * Options are auto-derived from `values`.\r\n *\r\n * @param values - The allowed HTTP methods, e.g. `[\"GET\", \"POST\", \"PUT\", \"DELETE\"] as const`\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.httpMethod(METHODS).size(69)\r\n * ```\r\n */\r\n httpMethod(\r\n values: T,\r\n ): ColBuilder {\r\n return col\r\n .enum(values)\r\n .label(\"Method\")\r\n .display(\"text\")\r\n .filterable(\"checkbox\", {\r\n options: values.map((v) => ({ label: v, value: v })),\r\n });\r\n },\r\n\r\n /**\r\n * An HTTP status code column.\r\n *\r\n * Defaults: number + checkbox filter with a standard set of common status codes.\r\n * Pass a custom `codes` array to override the defaults.\r\n *\r\n * Default codes: 200, 201, 204, 301, 302, 400, 401, 403, 404, 422, 429, 500, 502, 503, 504\r\n *\r\n * @param codes - Override the default status code options\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.httpStatus().label(\"Status\").size(60)\r\n * col.presets.httpStatus([200, 400, 500]).label(\"Status\") // custom codes\r\n * ```\r\n */\r\n httpStatus(\r\n codes?: number[],\r\n ): ColBuilder {\r\n return col\r\n .number()\r\n .label(\"Status\")\r\n .filterable(\"checkbox\", {\r\n options: (codes ?? DEFAULT_HTTP_STATUS_CODES).map((code) => ({\r\n label: String(code),\r\n value: code,\r\n })),\r\n });\r\n },\r\n\r\n /**\r\n * A duration / latency / timing column.\r\n *\r\n * Defaults: number + formatted number display with unit + slider filter\r\n * with bounds `{ min: 0, max: 5000 }`.\r\n *\r\n * @param unit - Unit label shown after the value, e.g. `\"ms\"`, `\"s\"`, `\"µs\"`\r\n * @param slider - Override the slider bounds (default: `{ min: 0, max: 5000 }`)\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.duration(\"ms\").label(\"Latency\").sortable().size(110).sheet()\r\n * col.presets.duration(\"s\", { min: 0, max: 60 }).label(\"Response time\")\r\n * ```\r\n */\r\n duration(\r\n unit?: string,\r\n slider?: { min: number; max: number },\r\n ): ColBuilder {\r\n return col\r\n .number()\r\n .label(\"Duration\")\r\n .display(\"number\", { unit })\r\n .filterable(\"slider\", slider ?? { min: 0, max: 5000 });\r\n },\r\n\r\n /**\r\n * A timestamp column.\r\n *\r\n * Defaults: Date + relative timestamp display (absolute on hover) +\r\n * timerange filter + sortable.\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.timestamp().label(\"Date\").commandDisabled().size(200).sheet()\r\n * ```\r\n */\r\n timestamp(): ColBuilder {\r\n return col.timestamp().label(\"Timestamp\").display(\"timestamp\").sortable();\r\n },\r\n\r\n /**\r\n * A trace / span / request ID column.\r\n *\r\n * Defaults: string + monospace code display + not filterable.\r\n * Typically hidden in the table and shown only in the row detail drawer.\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.traceId().label(\"Request ID\").hidden().sheet({ skeletonClassName: \"w-64\" })\r\n * ```\r\n */\r\n traceId(): ColBuilder {\r\n return col.string().label(\"Trace ID\").display(\"code\").notFilterable();\r\n },\r\n\r\n /**\r\n * A URL pathname column.\r\n *\r\n * Defaults: string + plain text display + input (text search) filter.\r\n *\r\n * @example\r\n * ```ts\r\n * col.presets.pathname().size(130).sheet()\r\n * ```\r\n */\r\n pathname(): ColBuilder {\r\n return col.string().label(\"Pathname\").filterable(\"input\");\r\n },\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/presets.ts" }, { "path": "src/lib/table-schema/serialize.ts", "content": "import { col } from \"./col\";\r\nimport type {\r\n ColBuilder,\r\n ColConfig,\r\n ColKind,\r\n ColumnDescriptor,\r\n FilterDescriptor,\r\n SchemaJSON,\r\n SheetDescriptor,\r\n TableSchemaDefinition,\r\n} from \"./types\";\r\n\r\n// ── Serialization (schema → JSON) ────────────────────────────────────────────\r\n\r\nfunction serializeFilter(filter: ColConfig[\"filter\"]): FilterDescriptor | null {\r\n if (!filter) return null;\r\n const descriptor: FilterDescriptor = {\r\n type: filter.type,\r\n defaultOpen: filter.defaultOpen,\r\n commandDisabled: filter.commandDisabled,\r\n };\r\n if (filter.options) {\r\n descriptor.options = filter.options.map((o) => ({\r\n label: o.label,\r\n value: o.value as string | number | boolean,\r\n }));\r\n }\r\n if (filter.min !== undefined) descriptor.min = filter.min;\r\n if (filter.max !== undefined) descriptor.max = filter.max;\r\n // filter.component and filter.presets are functions/complex objects — stripped\r\n return descriptor;\r\n}\r\n\r\nfunction serializeSheet(sheet: ColConfig[\"sheet\"]): SheetDescriptor | null {\r\n if (!sheet) return null;\r\n const descriptor: SheetDescriptor = {};\r\n if (sheet.label) descriptor.label = sheet.label;\r\n if (sheet.className) descriptor.className = sheet.className;\r\n if (sheet.skeletonClassName)\r\n descriptor.skeletonClassName = sheet.skeletonClassName;\r\n // sheet.component and sheet.condition are functions — stripped\r\n return descriptor;\r\n}\r\n\r\nexport function serializeSchema(definition: TableSchemaDefinition): SchemaJSON {\r\n const columns: ColumnDescriptor[] = Object.entries(definition).map(\r\n ([key, builder]) => {\r\n const c = builder._config;\r\n const descriptor: ColumnDescriptor = {\r\n key,\r\n label: c.label,\r\n dataType: c.kind,\r\n optional: c.optional,\r\n hidden: c.hidden,\r\n sortable: c.sortable,\r\n display:\r\n c.display.type === \"number\" && \"unit\" in c.display && c.display.unit\r\n ? { type: \"number\", unit: c.display.unit }\r\n : { type: c.display.type },\r\n filter: serializeFilter(c.filter),\r\n sheet: serializeSheet(c.sheet),\r\n };\r\n if (c.description) descriptor.description = c.description;\r\n if (c.enumValues) descriptor.enumValues = c.enumValues;\r\n if (c.arrayItem) {\r\n descriptor.arrayItemType = {\r\n dataType: c.arrayItem.kind,\r\n ...(c.arrayItem.enumValues\r\n ? { enumValues: c.arrayItem.enumValues }\r\n : {}),\r\n };\r\n }\r\n if (c.size !== undefined) descriptor.size = c.size;\r\n return descriptor;\r\n },\r\n );\r\n return { columns };\r\n}\r\n\r\n// ── Deserialization (JSON → schema) ─────────────────────────────────────────\r\n//\r\n// Reconstructs col.* builders from a SchemaJSON descriptor.\r\n// Limitation: custom renderers (display.cell, filter.component, sheet.component,\r\n// sheet.condition) are not serialized and therefore cannot be reconstructed.\r\n// Columns with display.type === \"custom\" fall back to the col kind's default\r\n// display. Developers can override renderers on the returned builders.\r\n\r\nfunction defaultDisplayType(kind: ColKind): string {\r\n switch (kind) {\r\n case \"enum\":\r\n case \"array\":\r\n return \"badge\";\r\n case \"boolean\":\r\n return \"boolean\";\r\n case \"timestamp\":\r\n return \"timestamp\";\r\n case \"number\":\r\n return \"number\";\r\n case \"string\":\r\n case \"record\":\r\n default:\r\n return \"text\";\r\n }\r\n}\r\n\r\nexport function deserializeSchema(json: SchemaJSON): TableSchemaDefinition {\r\n const definition: TableSchemaDefinition = {};\r\n\r\n for (const col_ of json.columns) {\r\n // 1. Pick the right col.* factory.\r\n // F is typed as `any` on the variable so we can call filterable() dynamically\r\n // without knowing the col kind at compile time — this is intentional since\r\n // deserializeSchema is a runtime operation reading from JSON.\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n let builder: ColBuilder =\r\n col_.dataType === \"enum\" && col_.enumValues\r\n ? col.enum(col_.enumValues as readonly string[])\r\n : col_.dataType === \"array\" &&\r\n col_.arrayItemType?.dataType === \"enum\" &&\r\n col_.arrayItemType.enumValues\r\n ? col.array(\r\n col.enum(col_.arrayItemType.enumValues as readonly string[]),\r\n )\r\n : col_.dataType === \"boolean\"\r\n ? col.boolean()\r\n : col_.dataType === \"timestamp\"\r\n ? col.timestamp()\r\n : col_.dataType === \"number\"\r\n ? col.number()\r\n : col_.dataType === \"record\"\r\n ? col.record()\r\n : col.string();\r\n\r\n // 2. Label + description\r\n builder = builder.label(col_.label);\r\n if (col_.description) builder = builder.description(col_.description);\r\n\r\n // 3. Display — fall back to kind default when \"custom\" (function not serialized)\r\n const displayType =\r\n col_.display.type === \"custom\"\r\n ? defaultDisplayType(col_.dataType)\r\n : col_.display.type;\r\n if (displayType === \"number\" && col_.display.unit) {\r\n builder = builder.display(\"number\", { unit: col_.display.unit });\r\n } else if (\r\n displayType === \"text\" ||\r\n displayType === \"code\" ||\r\n displayType === \"boolean\" ||\r\n displayType === \"badge\" ||\r\n displayType === \"timestamp\"\r\n ) {\r\n builder = builder.display(displayType);\r\n }\r\n\r\n // 4. Filter\r\n if (col_.filter === null) {\r\n builder = builder.notFilterable();\r\n } else {\r\n const f = col_.filter;\r\n if (f.type === \"slider\" && f.min !== undefined && f.max !== undefined) {\r\n builder = builder.filterable(\"slider\", { min: f.min, max: f.max });\r\n } else if (f.type === \"checkbox\") {\r\n builder = builder.filterable(\"checkbox\", {\r\n ...(f.options ? { options: f.options } : {}),\r\n });\r\n } else if (f.type === \"timerange\") {\r\n builder = builder.filterable(\"timerange\");\r\n } else {\r\n builder = builder.filterable(\"input\");\r\n }\r\n if (f.defaultOpen) builder = builder.defaultOpen();\r\n if (f.commandDisabled) builder = builder.commandDisabled();\r\n }\r\n\r\n // 5. Structural modifiers\r\n if (col_.hidden) builder = builder.hidden();\r\n if (col_.sortable) builder = builder.sortable();\r\n if (col_.optional) builder = builder.optional();\r\n if (col_.size !== undefined) builder = builder.size(col_.size);\r\n\r\n // 6. Sheet\r\n if (col_.sheet !== null) {\r\n builder = builder.sheet({\r\n ...(col_.sheet.label ? { label: col_.sheet.label } : {}),\r\n ...(col_.sheet.className ? { className: col_.sheet.className } : {}),\r\n ...(col_.sheet.skeletonClassName\r\n ? { skeletonClassName: col_.sheet.skeletonClassName }\r\n : {}),\r\n });\r\n }\r\n\r\n definition[col_.key] = builder;\r\n }\r\n\r\n return definition;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/serialize.ts" }, { "path": "src/lib/table-schema/types.ts", "content": "import type { DatePreset, Option } from \"@/components/data-table/types\";\r\nimport type { JSX } from \"react\";\r\n\r\nexport type ColKind =\r\n | \"string\"\r\n | \"number\"\r\n | \"boolean\"\r\n | \"timestamp\"\r\n | \"enum\"\r\n | \"array\"\r\n | \"record\";\r\n\r\n/** The set of filter UI types. Used as the `F` generic on `ColBuilder`. */\r\nexport type FilterType = \"input\" | \"checkbox\" | \"slider\" | \"timerange\";\r\n\r\nexport type DisplayConfig =\r\n | { type: \"text\" }\r\n | { type: \"code\" }\r\n | { type: \"boolean\" }\r\n | { type: \"badge\" }\r\n | { type: \"timestamp\" }\r\n | { type: \"number\"; unit?: string }\r\n | {\r\n type: \"custom\";\r\n cell: (value: unknown, row: unknown) => JSX.Element | null;\r\n };\r\n\r\nexport type FilterConfig = {\r\n type: FilterType;\r\n defaultOpen: boolean;\r\n commandDisabled: boolean;\r\n options?: Option[];\r\n component?: (props: Option) => JSX.Element | null;\r\n min?: number;\r\n max?: number;\r\n presets?: DatePreset[];\r\n};\r\n\r\nexport type SheetConfig = {\r\n label?: string;\r\n component?: (row: unknown) => JSX.Element | null | string;\r\n condition?: (row: unknown) => boolean;\r\n className?: string;\r\n skeletonClassName?: string;\r\n};\r\n\r\nexport type ColConfig = {\r\n kind: ColKind;\r\n enumValues?: readonly string[];\r\n arrayItem?: ColConfig;\r\n optional: boolean;\r\n label: string;\r\n description?: string;\r\n display: DisplayConfig;\r\n size?: number;\r\n hidden: boolean;\r\n sortable: boolean;\r\n filter: FilterConfig | null;\r\n sheet: SheetConfig | null;\r\n};\r\n\r\n/**\r\n * A fluent builder for a single table column.\r\n *\r\n * `T` — TypeScript type of the column's data value (inferred by `InferTableType`).\r\n * `F` — union of filter UI types valid for this col kind (compile-time constraint):\r\n *\r\n * | Factory | Allowed filter types |\r\n * |--------------------|-----------------------------------------|\r\n * | `col.string()` | `\"input\"` |\r\n * | `col.number()` | `\"input\" \\| \"slider\" \\| \"checkbox\"` |\r\n * | `col.boolean()` | `\"checkbox\"` |\r\n * | `col.timestamp()` | `\"timerange\"` |\r\n * | `col.enum()` | `\"checkbox\"` |\r\n * | `col.array()` | `\"checkbox\"` |\r\n * | `col.record()` | `never` — `filterable()` is an error |\r\n *\r\n * Calling `filterable(type)` with a type not in `F` is a **compile-time error**.\r\n */\r\nexport interface ColBuilder {\r\n /** @internal Raw column configuration. Used by generators — do not access directly. */\r\n readonly _config: ColConfig;\r\n\r\n /**\r\n * Sets the column header label shown in the table and filter sidebar.\r\n *\r\n * @example\r\n * col.string().label(\"Host\")\r\n * col.enum(LEVELS).label(\"Severity\")\r\n */\r\n label(text: string): ColBuilder;\r\n\r\n /**\r\n * Attaches a human-readable description of the column's domain meaning.\r\n *\r\n * Not shown in the UI. Used by AI agents and MCP tools (via `toJSON()`) to\r\n * understand what the column represents.\r\n *\r\n * @example\r\n * col.number().label(\"Latency\").description(\"Round-trip time from request to response, in ms\")\r\n * col.enum(LEVELS).label(\"Level\").description(\"Log severity: error > warn > info > debug\")\r\n */\r\n description(text: string): ColBuilder;\r\n\r\n /**\r\n * Sets how the column value is rendered in table cells.\r\n *\r\n * Built-in display types:\r\n * - `\"text\"` — plain text, truncated with tooltip on overflow\r\n * - `\"code\"` — monospace font (IDs, hashes, paths, hostnames)\r\n * - `\"boolean\"` — checkmark / dash icon\r\n * - `\"badge\"` — colored chip (enums, categories, tags)\r\n * - `\"timestamp\"` — relative time (\"3m ago\"), absolute datetime on hover\r\n * - `\"number\"` — formatted number with optional `unit` suffix\r\n * - `\"custom\"` — developer-supplied JSX renderer (not serializable)\r\n *\r\n * @example\r\n * col.string().display(\"code\")\r\n * col.number().display(\"number\", { unit: \"ms\" })\r\n * col.enum(LEVELS).display(\"custom\", { cell: (value) => })\r\n */\r\n display(\r\n type: \"text\" | \"code\" | \"boolean\" | \"badge\" | \"timestamp\",\r\n ): ColBuilder;\r\n display(type: \"number\", options?: { unit?: string }): ColBuilder;\r\n display(\r\n type: \"custom\",\r\n options: { cell: (value: unknown, row: unknown) => JSX.Element | null },\r\n ): ColBuilder;\r\n\r\n /**\r\n * Enables filtering for this column using its default filter type.\r\n *\r\n * For `col.record()` (`F = never`) this is a **compile-time error**.\r\n * Use `.notFilterable()` on record columns instead (it is already the default).\r\n */\r\n filterable(...args: [F] extends [never] ? [never] : []): ColBuilder;\r\n\r\n /**\r\n * Enables filtering with an explicit filter type and optional configuration.\r\n *\r\n * Only types in `F` are accepted — passing an invalid type is a **compile-time error**:\r\n * - `\"input\"` — free-text or number search field\r\n * - `\"timerange\"` — date range picker (with optional `presets`)\r\n * - `\"checkbox\"` — multi-select from a list of `options`\r\n * - `\"slider\"` — numeric range with required `min` / `max` bounds\r\n *\r\n * @example\r\n * col.string().filterable(\"input\")\r\n * col.timestamp().filterable(\"timerange\")\r\n * col.enum(LEVELS).filterable(\"checkbox\", { options: LEVELS.map(v => ({ label: v, value: v })) })\r\n * col.number().filterable(\"slider\", { min: 0, max: 5000 })\r\n */\r\n filterable(type: F & (\"input\" | \"timerange\")): ColBuilder;\r\n filterable(\r\n type: F & \"checkbox\",\r\n options?: {\r\n options?: Option[];\r\n component?: (props: Option) => JSX.Element | null;\r\n },\r\n ): ColBuilder;\r\n filterable(\r\n type: F & \"slider\",\r\n options: { min: number; max: number },\r\n ): ColBuilder;\r\n\r\n /**\r\n * Removes filtering from this column.\r\n *\r\n * After calling `.notFilterable()`, subsequent `.filterable()` calls are\r\n * **compile-time errors** (`F` becomes `never`).\r\n *\r\n * @example\r\n * col.string().label(\"Request ID\").notFilterable().hidden()\r\n */\r\n notFilterable(): ColBuilder;\r\n\r\n /**\r\n * Opens the filter accordion for this column by default in the filter sidebar.\r\n *\r\n * Only applies to filterable columns. Use for high-priority filters that\r\n * users should see immediately without expanding the sidebar manually.\r\n *\r\n * @example\r\n * col.enum(LEVELS).label(\"Level\").filterable(\"checkbox\").defaultOpen()\r\n */\r\n defaultOpen(): ColBuilder;\r\n\r\n /**\r\n * Excludes this column from the command palette filter search.\r\n *\r\n * Useful for columns whose filter state is managed elsewhere (e.g. a date\r\n * picker in the toolbar) or for UI-only state fields like `cursor`.\r\n *\r\n * @example\r\n * col.timestamp().label(\"Date\").filterable(\"timerange\").commandDisabled()\r\n */\r\n commandDisabled(): ColBuilder;\r\n\r\n /**\r\n * Hides the column by default (not shown on first render).\r\n *\r\n * Hidden columns appear in the column visibility menu and can be toggled on.\r\n * Use `getDefaultColumnVisibility(schema)` to derive the initial visibility map.\r\n *\r\n * @example\r\n * col.number().label(\"DNS\").filterable(\"slider\", { min: 0, max: 5000 }).hidden()\r\n */\r\n hidden(): ColBuilder;\r\n\r\n /**\r\n * Sets a fixed column width in pixels.\r\n *\r\n * Both `size` and `minSize` are set to this value, preventing resizing below it.\r\n *\r\n * @example\r\n * col.enum(LEVELS).label(\"Level\").size(27)\r\n * col.timestamp().label(\"Date\").size(200)\r\n */\r\n size(px: number): ColBuilder;\r\n\r\n /**\r\n * Enables click-to-sort on the column header.\r\n *\r\n * Renders a `DataTableColumnHeader` with ascending/descending/none sort controls.\r\n *\r\n * @example\r\n * col.timestamp().label(\"Date\").sortable()\r\n * col.number().label(\"Latency\").filterable(\"slider\", { min: 0, max: 5000 }).sortable()\r\n */\r\n sortable(): ColBuilder;\r\n\r\n /**\r\n * Marks the data field as potentially `undefined` in the row type.\r\n *\r\n * Changes `T` to `T | undefined` in the inferred schema type (`InferTableType`).\r\n * Use when the column field may be absent from some rows.\r\n *\r\n * @example\r\n * col.number().optional().label(\"Percentile\").notFilterable().hidden()\r\n * col.string().optional().label(\"Message\").notFilterable()\r\n */\r\n optional(): ColBuilder;\r\n\r\n /**\r\n * Includes this column in the row detail drawer (`DataTableSheet`).\r\n *\r\n * Pass a `SheetConfig` to customize label, renderer, visibility condition,\r\n * and layout class. Calling `.sheet()` with no arguments uses defaults\r\n * (label from `.label()`, no custom renderer).\r\n *\r\n * @example\r\n * col.timestamp().label(\"Date\").sheet()\r\n * col.string().label(\"Host\").sheet({ skeletonClassName: \"w-24\" })\r\n * col.number().label(\"Latency\").sheet({\r\n * component: (row) => <>{row.latency}ms,\r\n * skeletonClassName: \"w-16\",\r\n * })\r\n */\r\n sheet(config?: SheetConfig): ColBuilder;\r\n}\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nexport type TableSchemaDefinition = Record>;\r\n\r\n// Infer the data row type from a table schema definition\r\nexport type InferTableType = {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n [K in keyof T]: T[K] extends ColBuilder ? U : never;\r\n};\r\n\r\n// ── Serializable descriptors (function-free) ────────────────────────────────\r\n\r\nexport type FilterDescriptor = {\r\n type: FilterType;\r\n defaultOpen: boolean;\r\n commandDisabled: boolean;\r\n options?: Array<{ label: string; value: string | number | boolean }>;\r\n min?: number;\r\n max?: number;\r\n};\r\n\r\nexport type SheetDescriptor = {\r\n label?: string;\r\n className?: string;\r\n skeletonClassName?: string;\r\n};\r\n\r\nexport type ColumnDescriptor = {\r\n key: string;\r\n label: string;\r\n description?: string;\r\n dataType: ColKind;\r\n enumValues?: readonly string[];\r\n arrayItemType?: { dataType: ColKind; enumValues?: readonly string[] };\r\n optional: boolean;\r\n hidden: boolean;\r\n sortable: boolean;\r\n size?: number;\r\n /** `\"custom\"` means a developer-supplied renderer exists; not reconstructable from JSON. */\r\n display: { type: string; unit?: string };\r\n filter: FilterDescriptor | null;\r\n sheet: SheetDescriptor | null;\r\n};\r\n\r\nexport type SchemaJSON = {\r\n columns: ColumnDescriptor[];\r\n};\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/types.ts" }, { "path": "src/lib/table-schema/to-typescript.ts", "content": "import type { ColumnDescriptor, SchemaJSON } from \"./types\";\r\n\r\n/**\r\n * Build the `col.*` factory call and method chain for one column descriptor.\r\n */\r\nfunction buildChain(c: ColumnDescriptor): string {\r\n const parts: string[] = [];\r\n\r\n // 1. Factory\r\n if (c.dataType === \"enum\" && c.enumValues) {\r\n const vals = c.enumValues.map((v) => JSON.stringify(v)).join(\", \");\r\n parts.push(`col.enum([${vals}])`);\r\n } else if (\r\n c.dataType === \"array\" &&\r\n c.arrayItemType?.dataType === \"enum\" &&\r\n c.arrayItemType.enumValues\r\n ) {\r\n const vals = c.arrayItemType.enumValues\r\n .map((v) => JSON.stringify(v))\r\n .join(\", \");\r\n parts.push(`col.array(col.enum([${vals}]))`);\r\n } else {\r\n parts.push(`col.${c.dataType}()`);\r\n }\r\n\r\n // 2. .label()\r\n parts.push(`.label(${JSON.stringify(c.label)})`);\r\n\r\n // 3. .description()\r\n if (c.description) {\r\n parts.push(`.description(${JSON.stringify(c.description)})`);\r\n }\r\n\r\n // 4. .display()\r\n const dt = c.display.type;\r\n if (dt === \"number\" && c.display.unit) {\r\n parts.push(\r\n `.display(\"number\", { unit: ${JSON.stringify(c.display.unit)} })`,\r\n );\r\n } else if (\r\n dt !== \"text\" // \"text\" is the default for string/record, skip it\r\n ) {\r\n parts.push(`.display(${JSON.stringify(dt)})`);\r\n }\r\n\r\n // 5. .filterable() / .notFilterable()\r\n if (c.filter === null) {\r\n parts.push(`.notFilterable()`);\r\n } else {\r\n const f = c.filter;\r\n if (f.type === \"slider\" && f.min !== undefined && f.max !== undefined) {\r\n parts.push(`.filterable(\"slider\", { min: ${f.min}, max: ${f.max} })`);\r\n } else if (f.type === \"checkbox\") {\r\n if (f.options && f.options.length > 0) {\r\n const opts = f.options\r\n .map(\r\n (o) =>\r\n `{ label: ${JSON.stringify(o.label)}, value: ${JSON.stringify(o.value)} }`,\r\n )\r\n .join(\", \");\r\n parts.push(`.filterable(\"checkbox\", { options: [${opts}] })`);\r\n } else {\r\n parts.push(`.filterable(\"checkbox\")`);\r\n }\r\n } else if (f.type === \"timerange\") {\r\n parts.push(`.filterable(\"timerange\")`);\r\n } else {\r\n parts.push(`.filterable(\"input\")`);\r\n }\r\n\r\n if (f.defaultOpen) parts.push(`.defaultOpen()`);\r\n if (f.commandDisabled) parts.push(`.commandDisabled()`);\r\n }\r\n\r\n // 6. Structural modifiers\r\n if (c.sortable) parts.push(`.sortable()`);\r\n if (c.hidden) parts.push(`.hidden()`);\r\n if (c.optional) parts.push(`.optional()`);\r\n if (c.size !== undefined) parts.push(`.size(${c.size})`);\r\n\r\n // 7. .sheet()\r\n if (c.sheet !== null) {\r\n const sheetArgs: string[] = [];\r\n if (c.sheet.label)\r\n sheetArgs.push(`label: ${JSON.stringify(c.sheet.label)}`);\r\n if (c.sheet.className)\r\n sheetArgs.push(`className: ${JSON.stringify(c.sheet.className)}`);\r\n if (c.sheet.skeletonClassName)\r\n sheetArgs.push(\r\n `skeletonClassName: ${JSON.stringify(c.sheet.skeletonClassName)}`,\r\n );\r\n parts.push(\r\n sheetArgs.length > 0 ? `.sheet({ ${sheetArgs.join(\", \")} })` : `.sheet()`,\r\n );\r\n }\r\n\r\n return parts.join(\"\\n \");\r\n}\r\n\r\n/**\r\n * Convert a `SchemaJSON` descriptor to a `createTableSchema(...)` TypeScript\r\n * source code string.\r\n *\r\n * The output is ready to copy-paste into a project that imports from\r\n * `@/lib/table-schema`.\r\n *\r\n * @example\r\n * ```ts\r\n * const ts = schemaToTypeScript(tableSchema.toJSON());\r\n * // → 'import { createTableSchema, col } from \"@/lib/table-schema\"; ...'\r\n * ```\r\n */\r\nexport function schemaToTypeScript(json: SchemaJSON): string {\r\n const lines: string[] = [\r\n 'import { createTableSchema, col } from \"@/lib/table-schema\";',\r\n \"\",\r\n \"export const schema = createTableSchema({\",\r\n ];\r\n\r\n for (const descriptor of json.columns) {\r\n const chain = buildChain(descriptor);\r\n lines.push(` ${descriptor.key}: ${chain},`);\r\n }\r\n\r\n lines.push(\"});\");\r\n return lines.join(\"\\n\");\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/to-typescript.ts" }, { "path": "src/lib/table-schema/validate.ts", "content": "import type { TableSchemaDefinition } from \"./types\";\r\n\r\n/**\r\n * Validates a table schema definition and throws a descriptive error on the\r\n * first violation found.\r\n *\r\n * Called automatically by `createTableSchema()` — no need to call manually.\r\n *\r\n * Catches errors that the TypeScript type system cannot prevent:\r\n * - Missing label (`.label()` was never called)\r\n * - Slider `min` greater than `max`\r\n *\r\n * These checks run for both the TypeScript-authored path (`createTableSchema({...})`)\r\n * and the AI-generated path (`createTableSchema.fromJSON(json)`).\r\n */\r\nexport function validateSchema(definition: TableSchemaDefinition): void {\r\n for (const [key, builder] of Object.entries(definition)) {\r\n const c = builder._config;\r\n\r\n // 1. Label is required — col.* factories default to label: \"\"\r\n if (!c.label) {\r\n throw new Error(\r\n `[createTableSchema] Column \"${key}\" is missing a label.\\n` +\r\n ` Fix: .label(\"${key[0]!.toUpperCase()}${key.slice(1)}\")`,\r\n );\r\n }\r\n\r\n // 2. Slider bounds must be valid — type system requires { min, max } to be\r\n // passed but cannot enforce min < max\r\n if (c.filter?.type === \"slider\") {\r\n const { min, max } = c.filter;\r\n if (min === undefined || max === undefined) {\r\n throw new Error(\r\n `[createTableSchema] Column \"${key}\": slider filter is missing min/max bounds.\\n` +\r\n ` Fix: .filterable(\"slider\", { min: 0, max: 100 })`,\r\n );\r\n }\r\n if (min > max) {\r\n throw new Error(\r\n `[createTableSchema] Column \"${key}\": slider min (${min}) must be less than max (${max}).\\n` +\r\n ` Fix: swap the values — .filterable(\"slider\", { min: ${max}, max: ${min} })`,\r\n );\r\n }\r\n }\r\n }\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/validate.ts" }, { "path": "src/lib/table-schema/generators/columns.tsx", "content": "\"use client\";\r\n\r\nimport {DataTableCellBadge} from \"@/components/data-table/data-table-cell/data-table-cell-badge\";\r\nimport {DataTableCellBoolean} from \"@/components/data-table/data-table-cell/data-table-cell-boolean\";\r\nimport {DataTableCellCode} from \"@/components/data-table/data-table-cell/data-table-cell-code\";\r\nimport {DataTableCellNumber} from \"@/components/data-table/data-table-cell/data-table-cell-number\";\r\nimport {DataTableCellText} from \"@/components/data-table/data-table-cell/data-table-cell-text\";\r\nimport {DataTableCellTimestamp} from \"@/components/data-table/data-table-cell/data-table-cell-timestamp\";\r\n\r\nimport {DataTableColumnHeader} from \"@/components/data-table/data-table-column-header\";\r\nimport type {ColumnDef} from \"@tanstack/react-table\";\r\nimport type {JSX} from \"react\";\r\nimport type {ColConfig,DisplayConfig,TableSchemaDefinition} from \"../types\";\r\n\r\n/**\r\n * Derive the TanStack Table filterFn name from a column config.\r\n *\r\n * Custom filterFns (arrSome, inDateRange) must be registered on the table:\r\n * filterFns: { inDateRange, arrSome } // from src/lib/table/filterfns.ts\r\n */\r\nfunction getFilterFn(config: ColConfig): string|undefined {\r\n if (!config.filter) return undefined;\r\n\r\n const {kind,filter}=config;\r\n\r\n switch (filter.type) {\r\n case \"timerange\":\r\n return \"inDateRange\"; // custom — must be registered\r\n case \"slider\":\r\n return \"inNumberRange\"; // TanStack built-in\r\n case \"input\":\r\n if (kind===\"string\") return \"includesString\"; // TanStack built-in\r\n if (kind===\"number\") return \"equals\"; // TanStack built-in\r\n return undefined;\r\n case \"checkbox\":\r\n // Array columns use arrIncludesSome (checks row's array for filter values)\r\n if (kind===\"array\") return \"arrIncludesSome\"; // TanStack built-in\r\n // Single-value columns use arrSome (checks if row value is in filter array)\r\n return \"arrSome\"; // custom — must be registered\r\n }\r\n}\r\n\r\n/**\r\n * Render the cell based on the display config.\r\n */\r\nfunction renderCell(\r\n display: DisplayConfig,\r\n value: unknown,\r\n row: unknown,\r\n): JSX.Element|null {\r\n switch (display.type) {\r\n case \"text\":\r\n return ;\r\n case \"code\":\r\n return ;\r\n case \"number\":\r\n return (\r\n \r\n );\r\n case \"timestamp\":\r\n return ;\r\n case \"badge\":\r\n return ;\r\n case \"boolean\":\r\n return ;\r\n case \"custom\":\r\n return display.cell(value,row);\r\n }\r\n}\r\n\r\n/**\r\n * Generate ColumnDef[] from a table schema definition.\r\n *\r\n * Rules:\r\n * - Dotted keys (e.g. \"timing.dns\") → id + accessorFn\r\n * - Non-dotted keys → accessorKey\r\n * - Sortable columns get DataTableColumnHeader; others get a plain string header\r\n * - filterFn is derived from col kind + filter type\r\n * - Cell renders via built-in display components or the \"custom\" cell function\r\n * - meta.label is always set; meta.hidden reflects .hidden() calls\r\n *\r\n * The consuming component must register custom filterFns:\r\n * filterFns: { inDateRange, arrSome }\r\n *\r\n * Composite/virtual columns that span multiple fields must be appended manually:\r\n * @example\r\n * ```ts\r\n * const columns = [\r\n * ...generateColumns(tableSchema),\r\n * { id: \"timing\", header: ..., cell: ..., size: 130 },\r\n * ];\r\n * ```\r\n */\r\nexport function generateColumns(\r\n schema: TableSchemaDefinition,\r\n): ColumnDef[] {\r\n return Object.entries(schema).map(([key,builder]) => {\r\n const config=builder._config;\r\n const isDotted=key.includes(\".\");\r\n const filterFn=getFilterFn(config);\r\n\r\n const header=config.sortable\r\n ? ({\r\n column,\r\n }: {\r\n column: Parameters[0][\"column\"];\r\n }) => \r\n :config.label;\r\n\r\n const cell=({\r\n getValue,\r\n row,\r\n }: {\r\n getValue: () => unknown;\r\n row: {original: TData};\r\n }) => renderCell(config.display,getValue(),row.original);\r\n\r\n const meta={\r\n label: config.label,\r\n hidden: config.hidden,\r\n };\r\n\r\n const base={\r\n header,\r\n cell,\r\n ...(filterFn? {filterFn}:{}),\r\n ...(config.size!==undefined\r\n ? {size: config.size,minSize: config.size}\r\n :{}),\r\n meta,\r\n };\r\n\r\n if (isDotted) {\r\n return {\r\n ...base,\r\n id: key,\r\n accessorFn: (row: TData) => (row as Record)[key],\r\n } as ColumnDef;\r\n }\r\n\r\n return {\r\n ...base,\r\n accessorKey: key,\r\n } as ColumnDef;\r\n });\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/generators/columns.tsx" }, { "path": "src/lib/table-schema/generators/filter-fields.ts", "content": "import type { DataTableFilterField } from \"@/components/data-table/types\";\r\nimport type { TableSchemaDefinition } from \"../types\";\r\n\r\n/**\r\n * Generate DataTableFilterField[] from a table schema definition.\r\n *\r\n * Only includes fields where filter !== null.\r\n * Order follows schema definition order (JS object key insertion order).\r\n *\r\n * Options for checkbox fields are auto-derived from col.enum(values) or\r\n * col.boolean() if not explicitly provided via filterable(\"checkbox\", { options }).\r\n */\r\nexport function generateFilterFields(\r\n schema: TableSchemaDefinition,\r\n): DataTableFilterField[] {\r\n const result: DataTableFilterField[] = [];\r\n\r\n for (const [key, builder] of Object.entries(schema)) {\r\n const config = builder._config;\r\n if (!config.filter) continue;\r\n\r\n const { filter, label, kind, enumValues, arrayItem } = config;\r\n\r\n const base = {\r\n label,\r\n value: key as keyof TData,\r\n defaultOpen: filter.defaultOpen || undefined,\r\n commandDisabled: filter.commandDisabled || undefined,\r\n };\r\n\r\n switch (filter.type) {\r\n case \"input\": {\r\n result.push({ ...base, type: \"input\" });\r\n break;\r\n }\r\n case \"timerange\": {\r\n result.push({\r\n ...base,\r\n type: \"timerange\",\r\n presets: filter.presets,\r\n });\r\n break;\r\n }\r\n case \"checkbox\": {\r\n // Derive options if not explicitly provided\r\n let options = filter.options;\r\n if (!options) {\r\n if (kind === \"enum\" && enumValues) {\r\n options = enumValues.map((v) => ({ label: v, value: v }));\r\n } else if (kind === \"boolean\") {\r\n options = [\r\n { label: \"Yes\", value: true },\r\n { label: \"No\", value: false },\r\n ];\r\n } else if (\r\n kind === \"array\" &&\r\n arrayItem?.kind === \"enum\" &&\r\n arrayItem.enumValues\r\n ) {\r\n options = arrayItem.enumValues.map((v) => ({ label: v, value: v }));\r\n }\r\n }\r\n result.push({\r\n ...base,\r\n type: \"checkbox\",\r\n options,\r\n component: filter.component,\r\n });\r\n break;\r\n }\r\n case \"slider\": {\r\n result.push({\r\n ...base,\r\n type: \"slider\",\r\n min: filter.min ?? 0,\r\n max: filter.max ?? 100,\r\n });\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/generators/filter-fields.ts" }, { "path": "src/lib/table-schema/generators/filter-schema.ts", "content": "import {\r\n ARRAY_DELIMITER,\r\n RANGE_DELIMITER,\r\n SLIDER_DELIMITER,\r\n} from \"@/lib/delimiters\";\r\nimport {field} from \"@/lib/store/schema/field\";\r\nimport {createSchema} from \"@/lib/store/schema/schema\";\r\nimport type {SchemaDefinition} from \"@/lib/store/schema/schemaTypes\";\r\nimport type {TableSchemaDefinition} from \"../types\";\r\n\r\n/**\r\n * Generate a BYOS filter schema from a table schema definition.\r\n *\r\n * Each filterable column maps to the appropriate field.* builder:\r\n * - col.string() + input → field.string()\r\n * - col.number() + input → field.number()\r\n * - col.number() + slider → field.array(field.number()).delimiter(SLIDER_DELIMITER)\r\n * - col.number() + checkbox → field.array(field.number()).delimiter(ARRAY_DELIMITER)\r\n * - col.boolean() + checkbox → field.array(field.boolean()).delimiter(ARRAY_DELIMITER)\r\n * - col.timestamp()+ timerange→ field.array(field.timestamp()).delimiter(RANGE_DELIMITER)\r\n * - col.enum(v) + checkbox → field.array(field.stringLiteral(v))\r\n * - col.array(col.enum(v)) + checkbox → field.array(field.stringLiteral(v))\r\n *\r\n * Non-filterable fields are excluded. Pagination/UI-state fields must be\r\n * composed separately:\r\n *\r\n * @example\r\n * ```ts\r\n * export const filterSchema = createSchema({\r\n * ...generateFilterSchema(tableSchema).definition,\r\n * sort: field.sort(),\r\n * live: field.boolean().default(false),\r\n * });\r\n * ```\r\n */\r\nexport function generateFilterSchema(\r\n schema: TableSchemaDefinition,\r\n): ReturnType> {\r\n const definition: SchemaDefinition={};\r\n\r\n for (const [key,builder] of Object.entries(schema)) {\r\n const config=builder._config;\r\n if (!config.filter) continue;\r\n\r\n const {kind,filter,enumValues,arrayItem}=config;\r\n\r\n switch (filter.type) {\r\n case \"input\": {\r\n if (kind===\"string\") {\r\n definition[key]=field.string();\r\n } else if (kind===\"number\") {\r\n definition[key]=field.number();\r\n }\r\n break;\r\n }\r\n case \"checkbox\": {\r\n if (kind===\"enum\"&&enumValues) {\r\n definition[key]=field.array(\r\n field.stringLiteral(enumValues as readonly string[]),\r\n );\r\n } else if (kind===\"number\") {\r\n definition[key]=field\r\n .array(field.number())\r\n .delimiter(ARRAY_DELIMITER);\r\n } else if (kind===\"boolean\") {\r\n definition[key]=field\r\n .array(field.boolean())\r\n .delimiter(ARRAY_DELIMITER);\r\n } else if (\r\n kind===\"array\"&&\r\n arrayItem?.kind===\"enum\"&&\r\n arrayItem.enumValues\r\n ) {\r\n definition[key]=field.array(\r\n field.stringLiteral(arrayItem.enumValues as readonly string[]),\r\n );\r\n }\r\n break;\r\n }\r\n case \"slider\": {\r\n definition[key]=field\r\n .array(field.number())\r\n .delimiter(SLIDER_DELIMITER);\r\n break;\r\n }\r\n case \"timerange\": {\r\n definition[key]=field\r\n .array(field.timestamp())\r\n .delimiter(RANGE_DELIMITER);\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return createSchema(definition);\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/generators/filter-schema.ts" }, { "path": "src/lib/table-schema/generators/sheet-fields.ts", "content": "import type { SheetField } from \"@/components/data-table/types\";\r\nimport type { TableSchemaDefinition } from \"../types\";\r\n\r\n/**\r\n * Generate SheetField[] from a table schema definition.\r\n *\r\n * Only includes fields where sheet !== null (.sheet() was called).\r\n * Sheet type is derived from the filter type, or \"readonly\" if not filterable.\r\n * Sheet label falls back to the column label if not overridden in .sheet({ label }).\r\n */\r\nexport function generateSheetFields(\r\n schema: TableSchemaDefinition,\r\n): SheetField[] {\r\n const result: SheetField[] = [];\r\n\r\n for (const [key, builder] of Object.entries(schema)) {\r\n const config = builder._config;\r\n if (config.sheet === null) continue;\r\n\r\n const sheetConfig = config.sheet;\r\n const filterConfig = config.filter;\r\n\r\n // Derive sheet type from filter type, or \"readonly\" if not filterable\r\n const sheetType: SheetField[\"type\"] =\r\n filterConfig?.type ?? \"readonly\";\r\n\r\n result.push({\r\n id: key as keyof TData,\r\n label: sheetConfig.label ?? config.label,\r\n type: sheetType,\r\n component: sheetConfig.component as SheetField[\"component\"],\r\n condition: sheetConfig.condition as SheetField[\"condition\"],\r\n className: sheetConfig.className,\r\n skeletonClassName: sheetConfig.skeletonClassName,\r\n });\r\n }\r\n\r\n return result;\r\n}\r\n", "type": "registry:lib", "target": "lib/data-grid/table-schema/generators/sheet-fields.ts" } ], "type": "registry:block" }