@blenx-dev/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/package.json +49 -0
  4. package/src/DataTable/data-table-column-toggle.tsx +73 -0
  5. package/src/DataTable/data-table-empty.tsx +27 -0
  6. package/src/DataTable/data-table-error.tsx +25 -0
  7. package/src/DataTable/data-table-infinite-loader.tsx +73 -0
  8. package/src/DataTable/data-table-loading.tsx +67 -0
  9. package/src/DataTable/data-table-pagination.tsx +80 -0
  10. package/src/DataTable/data-table-toolbar.tsx +62 -0
  11. package/src/DataTable/data-table.css.ts +420 -0
  12. package/src/DataTable/data-table.tsx +507 -0
  13. package/src/DataTable/index.ts +24 -0
  14. package/src/DataTable/types.ts +169 -0
  15. package/src/DataTable/use-infinite-scroll.ts +67 -0
  16. package/src/components/Accordion/accordion.css.ts +84 -0
  17. package/src/components/Accordion/accordion.tsx +87 -0
  18. package/src/components/Accordion/index.ts +8 -0
  19. package/src/components/Alert/alert.css.ts +29 -0
  20. package/src/components/Alert/alert.tsx +40 -0
  21. package/src/components/Alert/index.ts +1 -0
  22. package/src/components/AlertDialog/alert-dialog.css.ts +62 -0
  23. package/src/components/AlertDialog/alert-dialog.tsx +199 -0
  24. package/src/components/AlertDialog/index.ts +1 -0
  25. package/src/components/AspectRatio/aspect-ratio.css.ts +7 -0
  26. package/src/components/AspectRatio/aspect-ratio.tsx +20 -0
  27. package/src/components/AspectRatio/index.ts +1 -0
  28. package/src/components/Autocomplete/autocomplete.css.ts +167 -0
  29. package/src/components/Autocomplete/autocomplete.tsx +226 -0
  30. package/src/components/Autocomplete/index.ts +1 -0
  31. package/src/components/Avatar/avatar.css.ts +65 -0
  32. package/src/components/Avatar/avatar.tsx +44 -0
  33. package/src/components/Avatar/index.ts +1 -0
  34. package/src/components/Badge/badge.css.ts +180 -0
  35. package/src/components/Badge/badge.tsx +47 -0
  36. package/src/components/Badge/index.ts +1 -0
  37. package/src/components/Box/box.css.ts +5 -0
  38. package/src/components/Box/box.tsx +21 -0
  39. package/src/components/Box/index.ts +1 -0
  40. package/src/components/Breadcrumbs/breadcrumbs.css.ts +72 -0
  41. package/src/components/Breadcrumbs/breadcrumbs.tsx +79 -0
  42. package/src/components/Breadcrumbs/index.ts +9 -0
  43. package/src/components/Button/button.css.ts +200 -0
  44. package/src/components/Button/button.tsx +55 -0
  45. package/src/components/Button/index.ts +1 -0
  46. package/src/components/Calendar/calendar.css.ts +187 -0
  47. package/src/components/Calendar/calendar.tsx +143 -0
  48. package/src/components/Calendar/index.ts +1 -0
  49. package/src/components/Card/card.tsx +32 -0
  50. package/src/components/Card/index.ts +1 -0
  51. package/src/components/Checkbox/checkbox.css.ts +76 -0
  52. package/src/components/Checkbox/checkbox.tsx +94 -0
  53. package/src/components/Checkbox/index.ts +1 -0
  54. package/src/components/CloseButton/close-button.css.ts +11 -0
  55. package/src/components/CloseButton/close-button.tsx +15 -0
  56. package/src/components/CloseButton/index.ts +2 -0
  57. package/src/components/ColorPicker/color-picker.tsx +123 -0
  58. package/src/components/ColorPicker/index.ts +1 -0
  59. package/src/components/ColorSwatch/color-swatch.tsx +21 -0
  60. package/src/components/ColorSwatch/index.ts +1 -0
  61. package/src/components/Combobox/combobox.css.ts +333 -0
  62. package/src/components/Combobox/combobox.tsx +350 -0
  63. package/src/components/Combobox/index.ts +1 -0
  64. package/src/components/Command/command.css.ts +130 -0
  65. package/src/components/Command/command.tsx +413 -0
  66. package/src/components/Command/index.ts +7 -0
  67. package/src/components/Container/container.css.ts +41 -0
  68. package/src/components/Container/container.tsx +25 -0
  69. package/src/components/Container/index.ts +1 -0
  70. package/src/components/CopyButton/copy-button.css.ts +11 -0
  71. package/src/components/CopyButton/copy-button.tsx +45 -0
  72. package/src/components/CopyButton/index.ts +2 -0
  73. package/src/components/DatePicker/date-picker.tsx +75 -0
  74. package/src/components/DatePicker/index.ts +1 -0
  75. package/src/components/Dialog/dialog.css.ts +57 -0
  76. package/src/components/Dialog/dialog.tsx +181 -0
  77. package/src/components/Dialog/index.ts +1 -0
  78. package/src/components/Drawer/drawer.css.ts +404 -0
  79. package/src/components/Drawer/drawer.tsx +573 -0
  80. package/src/components/Drawer/index.ts +1 -0
  81. package/src/components/Field/field.css.ts +35 -0
  82. package/src/components/Field/field.tsx +101 -0
  83. package/src/components/Field/index.ts +1 -0
  84. package/src/components/Grid/grid.css.ts +12 -0
  85. package/src/components/Grid/grid.tsx +32 -0
  86. package/src/components/Grid/index.ts +1 -0
  87. package/src/components/Icon/icon.css.ts +10 -0
  88. package/src/components/Icon/icon.tsx +15 -0
  89. package/src/components/Icon/index.ts +1 -0
  90. package/src/components/IconButton/icon-button.css.ts +6 -0
  91. package/src/components/IconButton/icon-button.tsx +11 -0
  92. package/src/components/IconButton/index.ts +2 -0
  93. package/src/components/Input/index.ts +1 -0
  94. package/src/components/Input/input.css.ts +72 -0
  95. package/src/components/Input/input.tsx +50 -0
  96. package/src/components/InputGroup/index.ts +1 -0
  97. package/src/components/InputGroup/input-group.css.ts +156 -0
  98. package/src/components/InputGroup/input-group.tsx +133 -0
  99. package/src/components/Menu/index.ts +1 -0
  100. package/src/components/Menu/menu.css.ts +121 -0
  101. package/src/components/Menu/menu.tsx +115 -0
  102. package/src/components/OTPField/index.ts +1 -0
  103. package/src/components/OTPField/otp-field.css.ts +54 -0
  104. package/src/components/OTPField/otp-field.tsx +46 -0
  105. package/src/components/Popover/index.ts +1 -0
  106. package/src/components/Popover/popover.css.ts +81 -0
  107. package/src/components/Popover/popover.tsx +113 -0
  108. package/src/components/Progress/index.ts +7 -0
  109. package/src/components/Progress/progress.css.ts +37 -0
  110. package/src/components/Progress/progress.tsx +62 -0
  111. package/src/components/Radio/index.ts +1 -0
  112. package/src/components/Radio/radio.css.ts +72 -0
  113. package/src/components/Radio/radio.tsx +49 -0
  114. package/src/components/ScrollArea/index.ts +1 -0
  115. package/src/components/ScrollArea/scroll-area.css.ts +79 -0
  116. package/src/components/ScrollArea/scroll-area.tsx +96 -0
  117. package/src/components/SegmentedControl/index.ts +1 -0
  118. package/src/components/SegmentedControl/segmented-control.tsx +42 -0
  119. package/src/components/Select/index.ts +1 -0
  120. package/src/components/Select/select.css.ts +182 -0
  121. package/src/components/Select/select.tsx +165 -0
  122. package/src/components/Separator/index.ts +1 -0
  123. package/src/components/Separator/separator.css.ts +59 -0
  124. package/src/components/Separator/separator.tsx +34 -0
  125. package/src/components/Sheet/index.ts +1 -0
  126. package/src/components/Sheet/sheet.css.ts +184 -0
  127. package/src/components/Sheet/sheet.tsx +215 -0
  128. package/src/components/Slider/index.ts +1 -0
  129. package/src/components/Slider/slider.css.ts +81 -0
  130. package/src/components/Slider/slider.tsx +100 -0
  131. package/src/components/Spinner/index.ts +1 -0
  132. package/src/components/Spinner/spinner.css.ts +17 -0
  133. package/src/components/Spinner/spinner.tsx +15 -0
  134. package/src/components/Splitter/index.ts +1 -0
  135. package/src/components/Splitter/splitter.css.ts +69 -0
  136. package/src/components/Splitter/splitter.tsx +521 -0
  137. package/src/components/Stack/index.ts +1 -0
  138. package/src/components/Stack/stack.css.ts +42 -0
  139. package/src/components/Stack/stack.tsx +32 -0
  140. package/src/components/Surface/index.ts +1 -0
  141. package/src/components/Surface/surface.css.ts +40 -0
  142. package/src/components/Surface/surface.tsx +19 -0
  143. package/src/components/Switch/index.ts +1 -0
  144. package/src/components/Switch/switch.css.ts +46 -0
  145. package/src/components/Switch/switch.tsx +25 -0
  146. package/src/components/Table/index.ts +2 -0
  147. package/src/components/Table/table.css.ts +71 -0
  148. package/src/components/Table/table.tsx +117 -0
  149. package/src/components/Tabs/index.ts +1 -0
  150. package/src/components/Tabs/tabs.css.ts +250 -0
  151. package/src/components/Tabs/tabs.tsx +119 -0
  152. package/src/components/Text/index.ts +2 -0
  153. package/src/components/Text/text.css.ts +118 -0
  154. package/src/components/Text/text.tsx +66 -0
  155. package/src/components/Textarea/index.ts +1 -0
  156. package/src/components/Textarea/textarea.css.ts +66 -0
  157. package/src/components/Textarea/textarea.tsx +48 -0
  158. package/src/components/Toggle/index.ts +1 -0
  159. package/src/components/Toggle/toggle.css.ts +91 -0
  160. package/src/components/Toggle/toggle.tsx +44 -0
  161. package/src/components/ToggleGroup/index.ts +1 -0
  162. package/src/components/ToggleGroup/toggle-group.css.ts +77 -0
  163. package/src/components/ToggleGroup/toggle-group.tsx +131 -0
  164. package/src/components/index.ts +54 -0
  165. package/src/index.ts +3 -0
  166. package/src/utils/drawer-styles.css.ts +85 -0
  167. package/src/utils/heights.ts +16 -0
  168. package/src/utils/sprinkles.css.ts +197 -0
  169. package/src/utils/sprinkles.tokens.ts +74 -0
  170. package/src/utils/types.ts +10 -0
  171. package/src/utils/ve-style.utils.ts +51 -0
  172. package/tsconfig.json +10 -0
@@ -0,0 +1,507 @@
1
+ import { CaretDownIcon, CaretUpIcon } from "@phosphor-icons/react";
2
+ import clsx from "clsx";
3
+ import {
4
+ type ColumnDef,
5
+ type ColumnFiltersState,
6
+ flexRender,
7
+ getCoreRowModel,
8
+ getExpandedRowModel,
9
+ getFacetedRowModel,
10
+ getFacetedUniqueValues,
11
+ getFilteredRowModel,
12
+ getPaginationRowModel,
13
+ getSortedRowModel,
14
+ type PaginationState,
15
+ type Row,
16
+ type RowSelectionState,
17
+ type SortingState,
18
+ useReactTable,
19
+ type VisibilityState,
20
+ } from "@tanstack/react-table";
21
+ import { type UIEvent, useCallback, useMemo, useRef, useState } from "react";
22
+ import { DataTableEmpty } from "./data-table-empty";
23
+ import { DataTableError } from "./data-table-error";
24
+ import { DataTableInfiniteLoader } from "./data-table-infinite-loader";
25
+ import { DataTableLoading } from "./data-table-loading";
26
+ import { DataTablePagination } from "./data-table-pagination";
27
+ import { DataTableToolbar } from "./data-table-toolbar";
28
+ import type { DataTableProps, RowAction, TableFeatures } from "./types";
29
+ import { Button, Spinner } from "../components";
30
+ import * as styles from "./data-table.css";
31
+
32
+ const DEFAULT_FEATURES: TableFeatures = {
33
+ sorting: true,
34
+ globalSearch: true,
35
+ pagination: true,
36
+ columnVisibility: true,
37
+ rowSelection: false,
38
+ columnResizing: false,
39
+ columnPinning: false,
40
+ stickyHeader: false,
41
+ rowActions: false,
42
+ bulkActions: false,
43
+ };
44
+
45
+ function IndeterminateCheckbox({
46
+ checked,
47
+ indeterminate,
48
+ onChange,
49
+ label,
50
+ }: {
51
+ checked: boolean;
52
+ indeterminate: boolean;
53
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
54
+ label: string;
55
+ }) {
56
+ const ref = useRef<HTMLInputElement>(null);
57
+ useMemo(() => {
58
+ if (ref.current) {
59
+ ref.current.indeterminate = indeterminate;
60
+ }
61
+ }, [indeterminate]);
62
+
63
+ return (
64
+ <input
65
+ ref={ref}
66
+ type="checkbox"
67
+ checked={checked}
68
+ onChange={onChange}
69
+ aria-label={label}
70
+ className={styles.checkbox}
71
+ />
72
+ );
73
+ }
74
+
75
+ function useDataTableStates<TData extends Record<string, unknown>>(
76
+ props: Partial<DataTableProps<TData>>,
77
+ ) {
78
+ const [sorting, setSorting] = useState<SortingState>(props.initialSorting ?? []);
79
+ const [pagination, setPagination] = useState<PaginationState>(
80
+ props.initialPagination ?? { pageIndex: 0, pageSize: 10 },
81
+ );
82
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
83
+ props.initialColumnFilters ?? [],
84
+ );
85
+ const [globalFilter, setGlobalFilter] = useState(props.initialGlobalFilter ?? "");
86
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>(
87
+ props.initialRowSelection ?? {},
88
+ );
89
+
90
+ const handlePaginationChange = useCallback(
91
+ (v: PaginationState | ((old: PaginationState) => PaginationState)) => {
92
+ const next = typeof v === "function" ? v(pagination) : v;
93
+ setPagination(next);
94
+ props.callbacks?.onPaginationChange?.(next);
95
+ },
96
+ [pagination, props.callbacks],
97
+ );
98
+ const handleSortingChange = useCallback(
99
+ (v: SortingState | ((old: SortingState) => SortingState)) => {
100
+ const next = typeof v === "function" ? v(sorting) : v;
101
+ setSorting(next);
102
+ props.callbacks?.onSortingChange?.(next);
103
+ if (props.mode === "server") setPagination((p) => ({ ...p, pageIndex: 0 }));
104
+ },
105
+ [sorting, props.callbacks, props.mode],
106
+ );
107
+
108
+ const handleGlobalFilterChange = useCallback(
109
+ (value: string) => {
110
+ setGlobalFilter(value);
111
+ props.callbacks?.onGlobalFilterChange?.(value);
112
+ setPagination((p) => ({ ...p, pageIndex: 0 }));
113
+ },
114
+ [props.callbacks],
115
+ );
116
+
117
+ const handleColumnFiltersChange = useCallback(
118
+ (v: ColumnFiltersState | ((old: ColumnFiltersState) => ColumnFiltersState)) => {
119
+ const next = typeof v === "function" ? v(columnFilters) : v;
120
+ setColumnFilters(next);
121
+ props.callbacks?.onColumnFiltersChange?.(next);
122
+ },
123
+ [columnFilters, props.callbacks],
124
+ );
125
+
126
+ const handleRowSelectionChange = useCallback(
127
+ (v: RowSelectionState | ((old: RowSelectionState) => RowSelectionState)) => {
128
+ const next = typeof v === "function" ? v(rowSelection) : v;
129
+ setRowSelection(next);
130
+ props.callbacks?.onRowSelectionChange?.(next);
131
+ },
132
+ [rowSelection, props.callbacks],
133
+ );
134
+ return {
135
+ sorting,
136
+ handleSortingChange,
137
+ columnFilters,
138
+ handleColumnFiltersChange,
139
+ pagination,
140
+ handlePaginationChange,
141
+ globalFilter,
142
+ handleGlobalFilterChange,
143
+ rowSelection,
144
+ handleRowSelectionChange,
145
+ };
146
+ }
147
+ export function DataTable<TData extends Record<string, unknown>>({
148
+ columns,
149
+ data,
150
+ pageCount,
151
+ mode = "client",
152
+ isLoading,
153
+ isFetching,
154
+ isError,
155
+ errorMessage,
156
+ fetchNextPage,
157
+ hasNextPage,
158
+ isFetchingNextPage,
159
+ features: featuresProp,
160
+ size = "md",
161
+ infiniteScroll,
162
+ columnPinning,
163
+ slots,
164
+ rowActions,
165
+ initialSorting,
166
+ initialPagination,
167
+ initialColumnFilters,
168
+ initialGlobalFilter,
169
+ initialColumnVisibility,
170
+ initialRowSelection,
171
+ callbacks,
172
+ tableOptions,
173
+ className,
174
+ style,
175
+ }: DataTableProps<TData>) {
176
+ const features = useMemo(() => ({ ...DEFAULT_FEATURES, ...featuresProp }), [featuresProp]);
177
+
178
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
179
+ initialColumnVisibility ?? {},
180
+ );
181
+ const {
182
+ sorting,
183
+ handleSortingChange,
184
+ pagination,
185
+ handlePaginationChange,
186
+ columnFilters,
187
+ handleColumnFiltersChange,
188
+ globalFilter,
189
+ handleGlobalFilterChange,
190
+ rowSelection,
191
+ handleRowSelectionChange,
192
+ } = useDataTableStates({
193
+ initialPagination,
194
+ initialSorting,
195
+ initialColumnFilters,
196
+ initialGlobalFilter,
197
+ initialColumnVisibility,
198
+ initialRowSelection,
199
+ callbacks,
200
+ mode,
201
+ });
202
+
203
+ const stableColumns = useMemo(() => {
204
+ let result: ColumnDef<TData, unknown>[] = columns;
205
+
206
+ if (features.rowSelection) {
207
+ result = [
208
+ {
209
+ id: "select",
210
+ header: ({ table }) => (
211
+ <IndeterminateCheckbox
212
+ checked={table.getIsAllPageRowsSelected()}
213
+ indeterminate={table.getIsSomePageRowsSelected()}
214
+ onChange={table.getToggleAllPageRowsSelectedHandler()}
215
+ label="Select all rows"
216
+ />
217
+ ),
218
+ cell: ({ row }) => (
219
+ <IndeterminateCheckbox
220
+ checked={row.getIsSelected()}
221
+ indeterminate={row.getIsSomeSelected()}
222
+ onChange={row.getToggleSelectedHandler()}
223
+ label={`Select row ${row.id}`}
224
+ />
225
+ ),
226
+ enableSorting: false,
227
+ enableHiding: false,
228
+ size: 40,
229
+ } satisfies ColumnDef<TData, unknown>,
230
+ ...result,
231
+ ];
232
+ }
233
+
234
+ if (features.rowActions && rowActions && rowActions.length > 0) {
235
+ result = [
236
+ ...result,
237
+ {
238
+ id: "actions",
239
+ header: "Actions",
240
+ cell: ({ row }) => <RowActionsCell row={row} actions={rowActions} />,
241
+ enableSorting: false,
242
+ enableHiding: false,
243
+ size: 80,
244
+ } satisfies ColumnDef<TData, unknown>,
245
+ ];
246
+ }
247
+
248
+ return result;
249
+ }, [columns, features.rowSelection, features.rowActions, rowActions]);
250
+
251
+ const displayedData = useMemo(() => data ?? [], [data]);
252
+ const enablePagination = features.pagination && mode !== "infinite";
253
+
254
+ const table = useReactTable<TData>({
255
+ data: displayedData,
256
+ columns: stableColumns,
257
+ state: {
258
+ sorting,
259
+ pagination: enablePagination ? pagination : undefined,
260
+ columnFilters,
261
+ globalFilter,
262
+ rowSelection,
263
+ columnVisibility,
264
+ columnPinning: {
265
+ left: columnPinning?.left ?? [],
266
+ right: columnPinning?.right ?? [],
267
+ },
268
+ },
269
+ onSortingChange: handleSortingChange,
270
+ onPaginationChange: handlePaginationChange,
271
+ onColumnFiltersChange: handleColumnFiltersChange,
272
+ onGlobalFilterChange: handleGlobalFilterChange,
273
+ onRowSelectionChange: handleRowSelectionChange,
274
+ onColumnVisibilityChange: setColumnVisibility,
275
+ enableRowSelection: features.rowSelection,
276
+ enableSorting: features.sorting,
277
+ enableColumnResizing: features.columnResizing,
278
+ enableColumnPinning: features.columnPinning,
279
+ getCoreRowModel: getCoreRowModel(),
280
+ getSortedRowModel: features.sorting ? getSortedRowModel() : undefined,
281
+ getFilteredRowModel: getFilteredRowModel(),
282
+ getPaginationRowModel:
283
+ enablePagination && mode === "client" ? getPaginationRowModel() : undefined,
284
+ getExpandedRowModel: getExpandedRowModel(),
285
+ getFacetedRowModel: getFacetedRowModel(),
286
+ getFacetedUniqueValues: getFacetedUniqueValues(),
287
+ manualPagination: mode === "server",
288
+ manualSorting: mode === "server",
289
+ manualFiltering: mode === "server",
290
+ pageCount: mode === "server" ? (pageCount ?? 0) : undefined,
291
+ autoResetPageIndex: false,
292
+ ...tableOptions,
293
+ } as Parameters<typeof useReactTable<TData>>[0]);
294
+
295
+ const selectedRowModel = table.getSelectedRowModel();
296
+ const selectedRows = useMemo(
297
+ () => selectedRowModel.rows.map((r) => r.original),
298
+ [selectedRowModel.rows],
299
+ );
300
+ const { rows } = table.getRowModel();
301
+ const headerGroups = table.getHeaderGroups();
302
+
303
+ const showPagination = enablePagination && mode === "client" && rows.length > 0;
304
+ const showInfiniteLoader = mode === "infinite" && fetchNextPage;
305
+ const showToolbar =
306
+ features.globalSearch ||
307
+ features.columnVisibility ||
308
+ slots?.toolbar ||
309
+ (features.bulkActions && selectedRows.length > 0);
310
+
311
+ const tableContainerRef = useRef<HTMLDivElement>(null);
312
+ const [isScrolled, setIsScrolled] = useState(false);
313
+ const handleScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
314
+ setIsScrolled((e.target as HTMLDivElement).scrollTop > 0);
315
+ }, []);
316
+
317
+ const emptyState = useMemo(() => slots?.empty ?? <DataTableEmpty />, [slots?.empty]);
318
+ const loadingState = useMemo(
319
+ () => slots?.loading ?? <DataTableLoading columnCount={stableColumns.length} size={size} />,
320
+ [slots?.loading, stableColumns.length, size],
321
+ );
322
+ const errorState = useMemo(
323
+ () => slots?.error ?? <DataTableError message={errorMessage} />,
324
+ [slots?.error, errorMessage],
325
+ );
326
+
327
+ if (isError) return <div className={className}>{errorState}</div>;
328
+ if (isLoading) return <div className={className}>{loadingState}</div>;
329
+
330
+ return (
331
+ <div className={className} style={style}>
332
+ {showToolbar && (
333
+ <DataTableToolbar
334
+ table={table}
335
+ features={features}
336
+ globalSearch={globalFilter}
337
+ onGlobalSearchChange={handleGlobalFilterChange}
338
+ customToolbar={slots?.toolbar}
339
+ bulkActions={
340
+ features.bulkActions
341
+ ? ((
342
+ tableOptions as {
343
+ bulkActions?: {
344
+ label: string;
345
+ onClick: (rows: TData[]) => void;
346
+ }[];
347
+ }
348
+ )?.bulkActions ?? undefined)
349
+ : undefined
350
+ }
351
+ selectedRows={selectedRows}
352
+ />
353
+ )}
354
+
355
+ <div ref={tableContainerRef} onScroll={handleScroll} className={styles.tableContainer}>
356
+ <table className={styles.table} aria-label="Data table">
357
+ <thead
358
+ className={
359
+ features.stickyHeader
360
+ ? isScrolled
361
+ ? styles.theadStickyScrolled
362
+ : styles.theadSticky
363
+ : styles.theadStatic
364
+ }
365
+ >
366
+ {headerGroups.map((hg) => (
367
+ <tr key={hg.id} className={styles.headRow}>
368
+ {hg.headers.map((header) => {
369
+ const isSorted = header.column.getIsSorted();
370
+ const isSortable = header.column.getCanSort();
371
+ return (
372
+ <th
373
+ key={header.id}
374
+ colSpan={header.colSpan}
375
+ className={clsx(
376
+ styles.th,
377
+ size === "sm" && styles.thSm,
378
+ size === "lg" && styles.thLg,
379
+ isSortable && styles.thSortable,
380
+ )}
381
+ onClick={isSortable ? header.column.getToggleSortingHandler() : undefined}
382
+ aria-sort={
383
+ isSorted === "asc"
384
+ ? "ascending"
385
+ : isSorted === "desc"
386
+ ? "descending"
387
+ : undefined
388
+ }
389
+ aria-label={
390
+ isSortable
391
+ ? `Sort by ${header.column.columnDef.header?.toString() ?? header.id}`
392
+ : undefined
393
+ }
394
+ >
395
+ <div className={styles.thContent}>
396
+ {header.isPlaceholder
397
+ ? null
398
+ : flexRender(header.column.columnDef.header, header.getContext())}
399
+ {isSorted === "asc" && <CaretUpIcon size={10} aria-hidden="true" />}
400
+ {isSorted === "desc" && <CaretDownIcon size={10} aria-hidden="true" />}
401
+ </div>
402
+ </th>
403
+ );
404
+ })}
405
+ </tr>
406
+ ))}
407
+ </thead>
408
+ <tbody>
409
+ {rows.length === 0 ? (
410
+ <tr>
411
+ <td colSpan={stableColumns.length} className={styles.emptyTd}>
412
+ {emptyState}
413
+ </td>
414
+ </tr>
415
+ ) : (
416
+ rows.map((row) => {
417
+ const selected = row.getIsSelected();
418
+ return (
419
+ <tr
420
+ key={row.id}
421
+ onClick={() => callbacks?.onRowClick?.(row.original)}
422
+ onKeyDown={(e) => {
423
+ if (e.key === "Enter" || e.key === " ") callbacks?.onRowClick?.(row.original);
424
+ }}
425
+ tabIndex={callbacks?.onRowClick ? 0 : undefined}
426
+ role={callbacks?.onRowClick ? "button" : undefined}
427
+ className={clsx(
428
+ styles.tr,
429
+ size === "sm" && styles.trSm,
430
+ size === "lg" && styles.trLg,
431
+ selected && styles.trSelected,
432
+ )}
433
+ aria-selected={selected}
434
+ >
435
+ {row.getVisibleCells().map((cell) => (
436
+ <td
437
+ key={cell.id}
438
+ className={clsx(
439
+ styles.td,
440
+ size === "sm" && styles.tdSm,
441
+ size === "lg" && styles.tdLg,
442
+ )}
443
+ >
444
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
445
+ </td>
446
+ ))}
447
+ </tr>
448
+ );
449
+ })
450
+ )}
451
+ </tbody>
452
+ </table>
453
+ </div>
454
+
455
+ {isFetching && !isFetchingNextPage && mode !== "client" && (
456
+ <div className={styles.fetchingBar}>
457
+ <Spinner />
458
+ <span className={styles.fetchingText}>Updating...</span>
459
+ </div>
460
+ )}
461
+
462
+ {showPagination && <DataTablePagination table={table} />}
463
+
464
+ {showInfiniteLoader && (
465
+ <DataTableInfiniteLoader
466
+ fetchNextPage={fetchNextPage}
467
+ hasNextPage={Boolean(hasNextPage)}
468
+ isFetchingNextPage={Boolean(isFetchingNextPage)}
469
+ isFetching={isFetching}
470
+ config={infiniteScroll}
471
+ />
472
+ )}
473
+ </div>
474
+ );
475
+ }
476
+
477
+ interface RowActionsCellProps<TData> {
478
+ row: Row<TData>;
479
+ actions: RowAction<TData>[];
480
+ }
481
+
482
+ function RowActionsCell<TData>({ row, actions }: RowActionsCellProps<TData>) {
483
+ return (
484
+ <div className={styles.actionsCell}>
485
+ {actions.map((action, i) => {
486
+ const isDisabled =
487
+ typeof action.disabled === "function" ? action.disabled(row.original) : action.disabled;
488
+ return (
489
+ <Button
490
+ key={`row-action-${i.toString()}`}
491
+ variant="ghost"
492
+ size="sm"
493
+ disabled={isDisabled}
494
+ onClick={(e) => {
495
+ e.stopPropagation();
496
+ action.onClick(row.original);
497
+ }}
498
+ aria-label={action.label}
499
+ >
500
+ {action.icon && <span>{action.icon}</span>}
501
+ {action.label}
502
+ </Button>
503
+ );
504
+ })}
505
+ </div>
506
+ );
507
+ }
@@ -0,0 +1,24 @@
1
+ export { DataTable } from "./data-table";
2
+ export { DataTableColumnToggle } from "./data-table-column-toggle";
3
+ export { DataTableEmpty } from "./data-table-empty";
4
+ export { DataTableError } from "./data-table-error";
5
+ export { DataTableInfiniteLoader } from "./data-table-infinite-loader";
6
+ export { DataTableLoading } from "./data-table-loading";
7
+ export { DataTablePagination } from "./data-table-pagination";
8
+ export { DataTableToolbar } from "./data-table-toolbar";
9
+ export type {
10
+ BulkAction,
11
+ ColumnPinningOptions,
12
+ DataTableProps,
13
+ InfiniteScrollConfig,
14
+ InfiniteScrollMode,
15
+ RowAction,
16
+ ServerTableResponse,
17
+ ServerTableState,
18
+ TableCallbacks,
19
+ TableFeatures,
20
+ TableMode,
21
+ TableSize,
22
+ TableSlots,
23
+ } from "./types";
24
+ export { useInfiniteScroll } from "./use-infinite-scroll";
@@ -0,0 +1,169 @@
1
+ import type {
2
+ ColumnDef,
3
+ ColumnFiltersState,
4
+ OnChangeFn,
5
+ PaginationState,
6
+ RowSelectionState,
7
+ SortingState,
8
+ TableOptions,
9
+ VisibilityState,
10
+ } from "@tanstack/react-table";
11
+ import type { ReactNode } from "react";
12
+
13
+ // ─── Data loading modes ──────────────────────────────────────────────────────
14
+
15
+ export type TableMode = "client" | "server" | "infinite";
16
+
17
+ // ─── Server-state metadata ───────────────────────────────────────────────────
18
+
19
+ export interface ServerTableState {
20
+ pagination: PaginationState;
21
+ sorting: SortingState;
22
+ columnFilters: ColumnFiltersState;
23
+ globalFilter: string;
24
+ }
25
+
26
+ export interface ServerTableResponse<TData> {
27
+ rows: TData[];
28
+ totalCount: number;
29
+ pageCount: number;
30
+ }
31
+
32
+ // ─── Feature configuration ───────────────────────────────────────────────────
33
+
34
+ export interface TableFeatures {
35
+ sorting?: boolean;
36
+ globalSearch?: boolean;
37
+ pagination?: boolean;
38
+ columnVisibility?: boolean;
39
+ rowSelection?: boolean;
40
+ columnResizing?: boolean;
41
+ columnPinning?: boolean;
42
+ stickyHeader?: boolean;
43
+ rowActions?: boolean;
44
+ bulkActions?: boolean;
45
+ }
46
+
47
+ // ─── Infinite scroll configuration ───────────────────────────────────────────
48
+
49
+ export type InfiniteScrollMode = "auto" | "manual";
50
+
51
+ export interface InfiniteScrollConfig {
52
+ mode: InfiniteScrollMode;
53
+ /** Root margin for IntersectionObserver. Default: "200px" */
54
+ rootMargin?: string;
55
+ /** IntersectionObserver threshold. Default: 0 */
56
+ threshold?: number;
57
+ /** Load-more button text for manual mode. Default: "Load more" */
58
+ loadMoreText?: string;
59
+ /** Loading text. Default: "Loading..." */
60
+ loadingText?: string;
61
+ /** No more data text. Default: "No more results" */
62
+ noMoreText?: string;
63
+ }
64
+
65
+ // ─── Column pinning configuration ────────────────────────────────────────────
66
+
67
+ export interface ColumnPinningOptions {
68
+ left?: string[];
69
+ right?: string[];
70
+ }
71
+
72
+ // ─── Row action ──────────────────────────────────────────────────────────────
73
+
74
+ export interface RowAction<TData> {
75
+ label: string;
76
+ icon?: ReactNode;
77
+ onClick: (row: TData) => void;
78
+ disabled?: boolean | ((row: TData) => boolean);
79
+ variant?: "default" | "destructive";
80
+ }
81
+
82
+ // ─── Bulk action ─────────────────────────────────────────────────────────────
83
+
84
+ export interface BulkAction<TData> {
85
+ label: string;
86
+ icon?: ReactNode;
87
+ onClick: (selectedRows: TData[]) => void;
88
+ disabled?: boolean;
89
+ variant?: "default" | "destructive";
90
+ }
91
+
92
+ // ─── Component slots ─────────────────────────────────────────────────────────
93
+
94
+ export interface TableSlots<TData> {
95
+ toolbar?: ReactNode;
96
+ pagination?: ReactNode;
97
+ empty?: ReactNode;
98
+ loading?: ReactNode;
99
+ error?: ReactNode;
100
+ rowActions?: (row: TData) => ReactNode;
101
+ bulkActions?: (selectedRows: TData[]) => ReactNode;
102
+ }
103
+
104
+ // ─── State callbacks ─────────────────────────────────────────────────────────
105
+
106
+ export interface TableCallbacks<TData> {
107
+ onSortingChange?: OnChangeFn<SortingState>;
108
+ onPaginationChange?: OnChangeFn<PaginationState>;
109
+ onGlobalFilterChange?: OnChangeFn<string>;
110
+ onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;
111
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>;
112
+ onRowClick?: (row: TData) => void;
113
+ }
114
+
115
+ // ─── Table size ──────────────────────────────────────────────────────────────
116
+
117
+ export type TableSize = "sm" | "md" | "lg";
118
+
119
+ // ─── DataTable unified props ─────────────────────────────────────────────────
120
+
121
+ export interface DataTableProps<TData extends Record<string, unknown>> {
122
+ // ── Data ──
123
+ columns: ColumnDef<TData, unknown>[];
124
+ data: TData[] | undefined;
125
+ totalCount?: number;
126
+ pageCount?: number;
127
+
128
+ // ── Mode ──
129
+ mode?: TableMode;
130
+
131
+ // ── States ──
132
+ isLoading?: boolean;
133
+ isFetching?: boolean;
134
+ isError?: boolean;
135
+ errorMessage?: string;
136
+
137
+ // ── TanStack Query integration (server + infinite) ──
138
+ fetchNextPage?: () => void;
139
+ hasNextPage?: boolean;
140
+ isFetchingNextPage?: boolean;
141
+
142
+ // ── Configuration ──
143
+ features?: TableFeatures;
144
+ size?: TableSize;
145
+ infiniteScroll?: InfiniteScrollConfig;
146
+ columnPinning?: ColumnPinningOptions;
147
+
148
+ // ── Slots ──
149
+ slots?: TableSlots<TData>;
150
+ rowActions?: RowAction<TData>[];
151
+
152
+ // ── State ──
153
+ initialSorting?: SortingState;
154
+ initialPagination?: PaginationState;
155
+ initialColumnFilters?: ColumnFiltersState;
156
+ initialGlobalFilter?: string;
157
+ initialColumnVisibility?: VisibilityState;
158
+ initialRowSelection?: RowSelectionState;
159
+
160
+ // ── Callbacks ──
161
+ callbacks?: TableCallbacks<TData>;
162
+
163
+ // ── Table options override ──
164
+ tableOptions?: Partial<TableOptions<TData>>;
165
+
166
+ // ── Styles ──
167
+ className?: string;
168
+ style?: React.CSSProperties;
169
+ }