@superdangerous/app-framework 4.15.0 → 4.15.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superdangerous/app-framework",
3
- "version": "4.15.0",
3
+ "version": "4.15.1",
4
4
  "description": "Opinionated TypeScript framework for structured vibecoding - building internal web and desktop apps with batteries included",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,492 @@
1
+ import { useMemo, useCallback, useState, useEffect } from 'react';
2
+ import {
3
+ Table,
4
+ TableBody,
5
+ TableCell,
6
+ TableHead,
7
+ TableHeader,
8
+ TableRow,
9
+ } from '../../shadcn/table';
10
+ import { TooltipProvider } from '../../shadcn/tooltip';
11
+ import { ChevronUp, ChevronDown, ArrowUpDown, Loader2, Check, Eye, EyeOff } from 'lucide-react';
12
+ import { cn } from '../../utils';
13
+ import { useResizableColumns } from '../hooks/useResizableColumns';
14
+ import { useColumnVisibility } from '../hooks/useColumnVisibility';
15
+ import { useColumnOrder, useColumnDragDrop } from '../hooks/useColumnOrder';
16
+ import { usePagination } from '../hooks/usePagination';
17
+ import { Pagination } from './Pagination';
18
+ import type {
19
+ DataTableProps,
20
+ ColumnDef,
21
+ ColumnConfigCompat,
22
+ ColumnSizeConfig,
23
+ } from './types';
24
+
25
+ /**
26
+ * DataTable - Generic data table with full feature set
27
+ *
28
+ * Features:
29
+ * - Column resizing (drag handles)
30
+ * - Column reordering (drag-drop)
31
+ * - Column visibility toggle
32
+ * - Row selection with checkboxes
33
+ * - Sorting
34
+ * - Pagination
35
+ * - Sticky actions column
36
+ * - Context menu support
37
+ * - Header context menu for column visibility
38
+ */
39
+ export function DataTable<T>({
40
+ data,
41
+ columns,
42
+ storageKey,
43
+ getRowId,
44
+ selectable = false,
45
+ selectedIds,
46
+ onSelectionChange,
47
+ onRowClick,
48
+ onRowContextMenu,
49
+ sortField,
50
+ sortOrder,
51
+ onSort,
52
+ actionsColumn,
53
+ actionsColumnWidth = 80,
54
+ pageSize = 25,
55
+ pagination: externalPagination,
56
+ hidePagination = false,
57
+ className,
58
+ rowClassName,
59
+ enableHeaderContextMenu = true,
60
+ lockedColumns = [],
61
+ defaultColumnOrder,
62
+ loading = false,
63
+ emptyState,
64
+ }: DataTableProps<T>) {
65
+ // Build column configs for hooks
66
+ const columnSizeConfig = useMemo<ColumnSizeConfig[]>(() => {
67
+ const configs: ColumnSizeConfig[] = [];
68
+
69
+ if (selectable) {
70
+ configs.push({ key: 'select', defaultWidth: 40, minWidth: 40 });
71
+ }
72
+
73
+ columns.forEach((col) => {
74
+ configs.push({
75
+ key: col.id,
76
+ defaultWidth: col.width?.default ?? 150,
77
+ minWidth: col.width?.min ?? 80,
78
+ maxWidth: col.width?.max,
79
+ });
80
+ });
81
+
82
+ if (actionsColumn) {
83
+ configs.push({ key: 'actions', defaultWidth: actionsColumnWidth, minWidth: 60 });
84
+ }
85
+
86
+ return configs;
87
+ }, [columns, selectable, actionsColumn, actionsColumnWidth]);
88
+
89
+ const columnVisibilityConfig = useMemo<ColumnConfigCompat[]>(() => {
90
+ return columns.map((col) => ({
91
+ id: col.id,
92
+ label: typeof col.header === 'string' ? col.header : col.id,
93
+ defaultVisible: col.visibility?.default ?? true,
94
+ locked: col.visibility?.locked,
95
+ }));
96
+ }, [columns]);
97
+
98
+ const defaultOrder = useMemo(() => {
99
+ if (defaultColumnOrder) return defaultColumnOrder;
100
+ const order: string[] = [];
101
+ if (selectable) order.push('select');
102
+ columns.forEach((col) => order.push(col.id));
103
+ if (actionsColumn) order.push('actions');
104
+ return order;
105
+ }, [defaultColumnOrder, columns, selectable, actionsColumn]);
106
+
107
+ // Initialize hooks
108
+ const {
109
+ getResizeHandleProps,
110
+ getColumnStyle,
111
+ getTableStyle,
112
+ } = useResizableColumns({
113
+ tableId: storageKey,
114
+ columns: columnSizeConfig,
115
+ });
116
+
117
+ const columnVisibility = useColumnVisibility({
118
+ columns: columnVisibilityConfig,
119
+ storageKey,
120
+ });
121
+
122
+ const { columnOrder, moveColumn } = useColumnOrder({
123
+ storageKey: `${storageKey}-order`,
124
+ defaultOrder,
125
+ });
126
+
127
+ const { dragState, getDragHandleProps, showDropIndicator } = useColumnDragDrop(
128
+ columnOrder,
129
+ moveColumn,
130
+ [...lockedColumns, 'select', 'actions']
131
+ );
132
+
133
+ // Use external pagination if provided, otherwise use internal
134
+ const internalPagination = usePagination({
135
+ data,
136
+ pageSize,
137
+ storageKey,
138
+ });
139
+
140
+ const pagination = externalPagination || internalPagination;
141
+
142
+ // Header context menu state
143
+ const [headerContextMenu, setHeaderContextMenu] = useState<{ x: number; y: number } | null>(null);
144
+
145
+ const handleHeaderContextMenu = useCallback((e: React.MouseEvent) => {
146
+ if (!enableHeaderContextMenu) return;
147
+ e.preventDefault();
148
+ setHeaderContextMenu({ x: e.clientX, y: e.clientY });
149
+ }, [enableHeaderContextMenu]);
150
+
151
+ // Close header context menu on click outside
152
+ useEffect(() => {
153
+ if (!headerContextMenu) return;
154
+ const close = () => setHeaderContextMenu(null);
155
+ window.addEventListener('click', close);
156
+ return () => {
157
+ window.removeEventListener('click', close);
158
+ };
159
+ }, [headerContextMenu]);
160
+
161
+ // Selection helpers
162
+ const isAllSelected = useMemo(() => {
163
+ if (!selectable || !selectedIds || pagination.paginatedData.length === 0) return false;
164
+ return pagination.paginatedData.every((item) => selectedIds.has(getRowId(item)));
165
+ }, [selectable, selectedIds, pagination.paginatedData, getRowId]);
166
+
167
+ const isSomeSelected = useMemo(() => {
168
+ if (!selectable || !selectedIds) return false;
169
+ const selected = pagination.paginatedData.filter((item) => selectedIds.has(getRowId(item)));
170
+ return selected.length > 0 && selected.length < pagination.paginatedData.length;
171
+ }, [selectable, selectedIds, pagination.paginatedData, getRowId]);
172
+
173
+ const toggleSelection = useCallback((itemId: string) => {
174
+ if (!onSelectionChange || !selectedIds) return;
175
+ const next = new Set(selectedIds);
176
+ if (next.has(itemId)) {
177
+ next.delete(itemId);
178
+ } else {
179
+ next.add(itemId);
180
+ }
181
+ onSelectionChange(next);
182
+ }, [selectedIds, onSelectionChange]);
183
+
184
+ const selectAll = useCallback(() => {
185
+ if (!onSelectionChange) return;
186
+ const ids = new Set(pagination.paginatedData.map(getRowId));
187
+ onSelectionChange(ids);
188
+ }, [pagination.paginatedData, getRowId, onSelectionChange]);
189
+
190
+ const clearSelection = useCallback(() => {
191
+ if (!onSelectionChange) return;
192
+ onSelectionChange(new Set());
193
+ }, [onSelectionChange]);
194
+
195
+ // Get sort icon
196
+ const getSortIcon = useCallback((field: string) => {
197
+ if (sortField !== field) {
198
+ return <ArrowUpDown className="h-3.5 w-3.5 text-muted-foreground" />;
199
+ }
200
+ return sortOrder === 'asc'
201
+ ? <ChevronUp className="h-3.5 w-3.5" />
202
+ : <ChevronDown className="h-3.5 w-3.5" />;
203
+ }, [sortField, sortOrder]);
204
+
205
+ // Find column def by id
206
+ const getColumnDef = useCallback((id: string): ColumnDef<T> | undefined => {
207
+ return columns.find((col) => col.id === id);
208
+ }, [columns]);
209
+
210
+ // Render header cell content
211
+ const renderHeaderContent = useCallback((col: ColumnDef<T>) => {
212
+ const headerProps = {
213
+ columnId: col.id,
214
+ isSorted: sortField === col.sortKey,
215
+ sortDirection: sortField === col.sortKey ? sortOrder : undefined,
216
+ };
217
+
218
+ if (typeof col.header === 'function') {
219
+ return col.header(headerProps);
220
+ }
221
+
222
+ if (col.sortKey && onSort) {
223
+ return (
224
+ <button
225
+ onClick={() => onSort(col.sortKey!)}
226
+ className={cn(
227
+ 'flex items-center gap-1 hover:text-foreground transition-colors',
228
+ sortField === col.sortKey && 'text-primary font-medium'
229
+ )}
230
+ >
231
+ {col.header}
232
+ {getSortIcon(col.sortKey)}
233
+ </button>
234
+ );
235
+ }
236
+
237
+ return col.header;
238
+ }, [sortField, sortOrder, onSort, getSortIcon]);
239
+
240
+ return (
241
+ <TooltipProvider>
242
+ <>
243
+ {/* Table - scrolling handled by parent container */}
244
+ <Table
245
+ style={getTableStyle()}
246
+ className={cn('resizable-table sticky-actions-table', className)}
247
+ >
248
+ <TableHeader>
249
+ <TableRow onContextMenu={handleHeaderContextMenu}>
250
+ {columnOrder.map((colKey) => {
251
+ // Select column (sticky on left)
252
+ if (colKey === 'select' && selectable) {
253
+ return (
254
+ <TableHead
255
+ key="select"
256
+ className="sticky-select-header w-10 sticky left-0 z-20 bg-muted relative after:absolute after:right-0 after:top-0 after:bottom-0 after:w-px after:bg-border"
257
+ >
258
+ <input
259
+ type="checkbox"
260
+ checked={isAllSelected}
261
+ ref={(el) => {
262
+ if (el) el.indeterminate = isSomeSelected;
263
+ }}
264
+ onChange={(e) => e.target.checked ? selectAll() : clearSelection()}
265
+ className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
266
+ title={isAllSelected ? 'Deselect all' : 'Select all visible'}
267
+ />
268
+ </TableHead>
269
+ );
270
+ }
271
+
272
+ // Data columns
273
+ const col = getColumnDef(colKey);
274
+ if (!col) return null;
275
+ if (!columnVisibility.isColumnVisible(colKey)) return null;
276
+
277
+ return (
278
+ <TableHead
279
+ key={colKey}
280
+ style={getColumnStyle(colKey)}
281
+ {...getDragHandleProps(colKey)}
282
+ className={cn(
283
+ 'cursor-grab relative',
284
+ dragState.draggedId === colKey && 'column-dragging opacity-50',
285
+ showDropIndicator(colKey) && 'drop-indicator'
286
+ )}
287
+ >
288
+ {renderHeaderContent(col)}
289
+ <div {...getResizeHandleProps(colKey)} />
290
+ </TableHead>
291
+ );
292
+ })}
293
+
294
+ {/* Actions column header */}
295
+ {actionsColumn && (
296
+ <TableHead
297
+ className="sticky right-0 z-20 bg-muted text-center relative before:absolute before:left-0 before:top-0 before:bottom-0 before:w-px before:bg-border"
298
+ style={{ width: actionsColumnWidth, minWidth: actionsColumnWidth, maxWidth: actionsColumnWidth }}
299
+ >
300
+ Actions
301
+ </TableHead>
302
+ )}
303
+ </TableRow>
304
+ </TableHeader>
305
+
306
+ <TableBody>
307
+ {loading ? (
308
+ <TableRow>
309
+ <TableCell
310
+ colSpan={columnOrder.length + (actionsColumn ? 1 : 0)}
311
+ className="!p-0 h-32"
312
+ >
313
+ <div className="sticky left-0 w-screen max-w-full h-full bg-white flex justify-center items-center">
314
+ <div className="flex items-center gap-2 text-muted-foreground">
315
+ <Loader2 className="h-5 w-5 animate-spin" />
316
+ Loading...
317
+ </div>
318
+ </div>
319
+ </TableCell>
320
+ </TableRow>
321
+ ) : pagination.paginatedData.length === 0 ? null : (
322
+ pagination.paginatedData.map((item) => {
323
+ const rowId = getRowId(item);
324
+ const isSelected = selectedIds?.has(rowId) ?? false;
325
+
326
+ return (
327
+ <TableRow
328
+ key={rowId}
329
+ className={cn(
330
+ 'cursor-pointer bg-white hover:bg-muted/50',
331
+ isSelected && 'bg-primary/5',
332
+ rowClassName?.(item)
333
+ )}
334
+ onClick={() => onRowClick?.(item)}
335
+ onContextMenu={(e) => {
336
+ if (onRowContextMenu) {
337
+ e.preventDefault();
338
+ onRowContextMenu(item, { x: e.clientX, y: e.clientY });
339
+ }
340
+ }}
341
+ >
342
+ {columnOrder.map((colKey) => {
343
+ // Select cell (sticky on left)
344
+ if (colKey === 'select' && selectable) {
345
+ return (
346
+ <TableCell
347
+ key="select"
348
+ className={cn(
349
+ 'sticky-select-cell w-10 sticky left-0 z-10 relative after:absolute after:right-0 after:top-0 after:bottom-0 after:w-px after:bg-border',
350
+ isSelected ? 'bg-primary/5' : 'bg-white'
351
+ )}
352
+ onClick={(e) => e.stopPropagation()}
353
+ >
354
+ <input
355
+ type="checkbox"
356
+ checked={isSelected}
357
+ onChange={() => toggleSelection(rowId)}
358
+ className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer"
359
+ />
360
+ </TableCell>
361
+ );
362
+ }
363
+
364
+ // Data cells
365
+ const col = getColumnDef(colKey);
366
+ if (!col) return null;
367
+ if (!columnVisibility.isColumnVisible(colKey)) return null;
368
+
369
+ const cellProps = {
370
+ columnId: colKey,
371
+ isDragging: dragState.draggedId === colKey,
372
+ };
373
+
374
+ return (
375
+ <TableCell
376
+ key={colKey}
377
+ style={getColumnStyle(colKey)}
378
+ className={cn(
379
+ col.className,
380
+ dragState.draggedId === colKey && 'column-dragging'
381
+ )}
382
+ >
383
+ {col.cell(item, cellProps)}
384
+ </TableCell>
385
+ );
386
+ })}
387
+
388
+ {/* Actions cell */}
389
+ {actionsColumn && (
390
+ <TableCell
391
+ className={cn(
392
+ 'sticky right-0 z-10 text-center relative before:absolute before:left-0 before:top-0 before:bottom-0 before:w-px before:bg-border',
393
+ isSelected ? 'bg-primary/5' : 'bg-white'
394
+ )}
395
+ style={{ width: actionsColumnWidth, minWidth: actionsColumnWidth, maxWidth: actionsColumnWidth }}
396
+ onClick={(e) => e.stopPropagation()}
397
+ >
398
+ {actionsColumn(item)}
399
+ </TableCell>
400
+ )}
401
+ </TableRow>
402
+ );
403
+ })
404
+ )}
405
+ </TableBody>
406
+ </Table>
407
+
408
+ {/* Empty state - rendered outside table for proper positioning */}
409
+ {!loading && pagination.paginatedData.length === 0 && (
410
+ <div className="empty-state-container flex-1 flex items-center justify-center bg-white">
411
+ {emptyState || <span className="block text-center text-muted-foreground py-8">No data</span>}
412
+ </div>
413
+ )}
414
+
415
+ {/* Pagination (hidden when using external pagination controls) */}
416
+ {!hidePagination && !loading && pagination.totalPages > 1 && (
417
+ <Pagination
418
+ page={pagination.page}
419
+ pageSize={pagination.pageSize}
420
+ totalItems={pagination.totalItems}
421
+ totalPages={pagination.totalPages}
422
+ startIndex={pagination.startIndex}
423
+ endIndex={pagination.endIndex}
424
+ canGoPrev={pagination.canGoPrev}
425
+ canGoNext={pagination.canGoNext}
426
+ onPageChange={pagination.setPage}
427
+ onPageSizeChange={pagination.setPageSize}
428
+ onNextPage={pagination.nextPage}
429
+ onPrevPage={pagination.prevPage}
430
+ onFirstPage={'firstPage' in pagination ? (pagination as { firstPage: () => void }).firstPage : () => pagination.setPage(1)}
431
+ onLastPage={'lastPage' in pagination ? (pagination as { lastPage: () => void }).lastPage : () => pagination.setPage(pagination.totalPages)}
432
+ pageSizeOptions={pagination.pageSizeOptions}
433
+ />
434
+ )}
435
+
436
+ {/* Header Context Menu for Column Visibility */}
437
+ {headerContextMenu && (
438
+ <div
439
+ className="fixed z-50 min-w-[200px] overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
440
+ style={{
441
+ top: headerContextMenu.y,
442
+ left: headerContextMenu.x,
443
+ maxHeight: `calc(100vh - ${headerContextMenu.y}px - 20px)`,
444
+ }}
445
+ onClick={(e) => e.stopPropagation()}
446
+ >
447
+ <div className="px-2 py-1.5 text-xs font-medium text-muted-foreground">
448
+ Toggle columns
449
+ </div>
450
+ <div className="h-px bg-border my-1" />
451
+ {columnVisibility.columns.map(column => {
452
+ const visible = columnVisibility.isColumnVisible(column.id);
453
+ const isLocked = column.locked === true;
454
+ return (
455
+ <button
456
+ key={column.id}
457
+ onClick={() => !isLocked && columnVisibility.toggleColumn(column.id)}
458
+ disabled={isLocked}
459
+ className={cn(
460
+ 'flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded-sm hover:bg-accent',
461
+ isLocked && 'opacity-50 cursor-not-allowed'
462
+ )}
463
+ >
464
+ <div className="w-4 h-4 flex items-center justify-center">
465
+ {visible && <Check className="h-3.5 w-3.5 text-primary" />}
466
+ </div>
467
+ <span className="flex-1 text-left">{column.label}</span>
468
+ {isLocked && <span className="text-xs text-muted-foreground">Required</span>}
469
+ </button>
470
+ );
471
+ })}
472
+ <div className="h-px bg-border my-1" />
473
+ <button
474
+ onClick={() => columnVisibility.showAllColumns()}
475
+ className="flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded-sm hover:bg-accent"
476
+ >
477
+ <Eye className="h-4 w-4" />
478
+ Show All
479
+ </button>
480
+ <button
481
+ onClick={() => columnVisibility.hideAllColumns()}
482
+ className="flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded-sm hover:bg-accent"
483
+ >
484
+ <EyeOff className="h-4 w-4" />
485
+ Hide Optional
486
+ </button>
487
+ </div>
488
+ )}
489
+ </>
490
+ </TooltipProvider>
491
+ );
492
+ }
@@ -2,6 +2,7 @@
2
2
  * Data Table Components
3
3
  *
4
4
  * Reusable components for building data tables:
5
+ * - DataTable: Full-featured generic data table
5
6
  * - DataTablePage: Full-page layout with header controls
6
7
  * - PaginationControls: Compact inline pagination
7
8
  * - Pagination: Full pagination with page numbers
@@ -10,6 +11,19 @@
10
11
  * - TableFilters: Search and filter controls
11
12
  */
12
13
 
14
+ export { DataTable } from './DataTable';
15
+ export type {
16
+ DataTableProps,
17
+ ColumnDef,
18
+ ColumnWidth,
19
+ ColumnVisibility as ColumnVisibilityConfig,
20
+ HeaderCellProps,
21
+ CellProps,
22
+ ExternalPaginationState,
23
+ ColumnConfigCompat,
24
+ ColumnSizeConfig,
25
+ } from './types';
26
+
13
27
  export { DataTablePage } from './DataTablePage';
14
28
  export type { DataTablePageProps, FilterOption } from './DataTablePage';
15
29
 
@@ -0,0 +1,181 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ /**
4
+ * Column width configuration
5
+ */
6
+ export interface ColumnWidth {
7
+ default: number;
8
+ min: number;
9
+ max?: number;
10
+ }
11
+
12
+ /**
13
+ * Column visibility configuration
14
+ */
15
+ export interface ColumnVisibility {
16
+ default: boolean;
17
+ locked?: boolean; // If true, column cannot be hidden
18
+ }
19
+
20
+ /**
21
+ * Props passed to header cell render function
22
+ */
23
+ export interface HeaderCellProps {
24
+ columnId: string;
25
+ isSorted: boolean;
26
+ sortDirection?: 'asc' | 'desc';
27
+ }
28
+
29
+ /**
30
+ * Props passed to cell render function
31
+ */
32
+ export interface CellProps {
33
+ columnId: string;
34
+ isDragging: boolean;
35
+ }
36
+
37
+ /**
38
+ * Column definition for DataTable
39
+ */
40
+ export interface ColumnDef<T> {
41
+ /** Unique column identifier */
42
+ id: string;
43
+
44
+ /** Header content - string or render function */
45
+ header: string | ((props: HeaderCellProps) => ReactNode);
46
+
47
+ /** Cell content render function */
48
+ cell: (item: T, props: CellProps) => ReactNode;
49
+
50
+ /** Key to use for sorting (if sortable) */
51
+ sortKey?: string;
52
+
53
+ /** Width configuration */
54
+ width?: ColumnWidth;
55
+
56
+ /** Visibility configuration */
57
+ visibility?: ColumnVisibility;
58
+
59
+ /** Additional CSS class for cells */
60
+ className?: string;
61
+
62
+ /** Whether this column should use column style from resize hook */
63
+ resizable?: boolean;
64
+ }
65
+
66
+ // DragState is exported from ../hooks/useColumnOrder
67
+
68
+ /**
69
+ * External pagination state (from usePagination hook)
70
+ */
71
+ export interface ExternalPaginationState<T> {
72
+ paginatedData: T[];
73
+ page: number;
74
+ pageSize: number;
75
+ totalPages: number;
76
+ totalItems: number;
77
+ startIndex: number;
78
+ endIndex: number;
79
+ canGoNext: boolean;
80
+ canGoPrev: boolean;
81
+ pageSizeOptions: number[];
82
+ setPage: (page: number) => void;
83
+ setPageSize: (size: number) => void;
84
+ nextPage: () => void;
85
+ prevPage: () => void;
86
+ }
87
+
88
+ /**
89
+ * DataTable props
90
+ */
91
+ export interface DataTableProps<T> {
92
+ /** Data array to display */
93
+ data: T[];
94
+
95
+ /** Column definitions */
96
+ columns: ColumnDef<T>[];
97
+
98
+ /** Storage key for persisting table state (column widths, order, visibility) */
99
+ storageKey: string;
100
+
101
+ /** Function to get unique ID from item */
102
+ getRowId: (item: T) => string;
103
+
104
+ // Selection
105
+ /** Enable row selection with checkboxes */
106
+ selectable?: boolean;
107
+ /** Set of selected row IDs */
108
+ selectedIds?: Set<string>;
109
+ /** Callback when selection changes */
110
+ onSelectionChange?: (ids: Set<string>) => void;
111
+
112
+ // Row interactions
113
+ /** Callback when row is clicked */
114
+ onRowClick?: (item: T) => void;
115
+ /** Callback when row is right-clicked (for context menu) */
116
+ onRowContextMenu?: (item: T, position: { x: number; y: number }) => void;
117
+
118
+ // Sorting
119
+ /** Current sort field */
120
+ sortField?: string;
121
+ /** Current sort order */
122
+ sortOrder?: 'asc' | 'desc';
123
+ /** Callback when sort changes */
124
+ onSort?: (field: string) => void;
125
+
126
+ // Actions column
127
+ /** Render function for actions column (always last, sticky) */
128
+ actionsColumn?: (item: T) => ReactNode;
129
+ /** Width for actions column */
130
+ actionsColumnWidth?: number;
131
+
132
+ // Pagination
133
+ /** Page size for pagination (used when no external pagination provided) */
134
+ pageSize?: number;
135
+ /** External pagination state from usePagination hook (overrides internal pagination) */
136
+ pagination?: ExternalPaginationState<T>;
137
+ /** Hide the built-in pagination controls (use when pagination is shown elsewhere) */
138
+ hidePagination?: boolean;
139
+
140
+ // Styling
141
+ /** Additional CSS class for table container */
142
+ className?: string;
143
+ /** Function to compute row CSS class */
144
+ rowClassName?: (item: T) => string;
145
+
146
+ // Features
147
+ /** Enable header right-click for column visibility menu */
148
+ enableHeaderContextMenu?: boolean;
149
+ /** Columns that cannot be reordered */
150
+ lockedColumns?: string[];
151
+ /** Default column order (if not persisted) */
152
+ defaultColumnOrder?: string[];
153
+
154
+ // Loading state
155
+ /** Show loading indicator */
156
+ loading?: boolean;
157
+
158
+ // Empty state
159
+ /** Content to show when no data */
160
+ emptyState?: ReactNode;
161
+ }
162
+
163
+ /**
164
+ * Column config for visibility hook (for compatibility)
165
+ */
166
+ export interface ColumnConfigCompat {
167
+ id: string;
168
+ label: string;
169
+ defaultVisible?: boolean;
170
+ locked?: boolean;
171
+ }
172
+
173
+ /**
174
+ * Column config for resize hook (for compatibility)
175
+ */
176
+ export interface ColumnSizeConfig {
177
+ key: string;
178
+ defaultWidth: number;
179
+ minWidth: number;
180
+ maxWidth?: number;
181
+ }
@@ -56,6 +56,7 @@ export type {
56
56
 
57
57
  // Components
58
58
  export {
59
+ DataTable,
59
60
  DataTablePage,
60
61
  PaginationControls,
61
62
  Pagination,
@@ -65,6 +66,15 @@ export {
65
66
  } from './components';
66
67
 
67
68
  export type {
69
+ DataTableProps,
70
+ ColumnDef,
71
+ ColumnWidth,
72
+ ColumnVisibilityConfig,
73
+ HeaderCellProps,
74
+ CellProps,
75
+ ExternalPaginationState,
76
+ ColumnConfigCompat,
77
+ ColumnSizeConfig,
68
78
  DataTablePageProps,
69
79
  FilterOption,
70
80
  PaginationControlsProps,