@startsimpli/ui 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 (86) hide show
  1. package/README.md +537 -0
  2. package/package.json +80 -0
  3. package/src/components/index.ts +50 -0
  4. package/src/components/navigation/sidebar.tsx +178 -0
  5. package/src/components/ui/accordion.tsx +58 -0
  6. package/src/components/ui/alert.tsx +59 -0
  7. package/src/components/ui/badge.tsx +36 -0
  8. package/src/components/ui/button.tsx +57 -0
  9. package/src/components/ui/calendar.tsx +70 -0
  10. package/src/components/ui/card.tsx +68 -0
  11. package/src/components/ui/checkbox.tsx +30 -0
  12. package/src/components/ui/collapsible.tsx +12 -0
  13. package/src/components/ui/dialog.tsx +122 -0
  14. package/src/components/ui/dropdown-menu.tsx +200 -0
  15. package/src/components/ui/index.ts +24 -0
  16. package/src/components/ui/input.tsx +25 -0
  17. package/src/components/ui/label.tsx +26 -0
  18. package/src/components/ui/popover.tsx +31 -0
  19. package/src/components/ui/progress.tsx +28 -0
  20. package/src/components/ui/scroll-area.tsx +48 -0
  21. package/src/components/ui/select.tsx +160 -0
  22. package/src/components/ui/separator.tsx +31 -0
  23. package/src/components/ui/skeleton.tsx +15 -0
  24. package/src/components/ui/table.tsx +117 -0
  25. package/src/components/ui/tabs.tsx +55 -0
  26. package/src/components/ui/textarea.tsx +24 -0
  27. package/src/components/ui/tooltip.tsx +30 -0
  28. package/src/components/unified-table/UnifiedTable.tsx +553 -0
  29. package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
  30. package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
  31. package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
  32. package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
  33. package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
  34. package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
  35. package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
  36. package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
  37. package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
  38. package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
  39. package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
  40. package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
  41. package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
  42. package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
  43. package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
  44. package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
  45. package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
  46. package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
  47. package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
  48. package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
  49. package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
  50. package/src/components/unified-table/components/MobileView/README.md +411 -0
  51. package/src/components/unified-table/components/MobileView/index.tsx +77 -0
  52. package/src/components/unified-table/components/MobileView/types.ts +77 -0
  53. package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
  54. package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
  55. package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
  56. package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
  57. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
  58. package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
  59. package/src/components/unified-table/hooks/index.ts +21 -0
  60. package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
  61. package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
  62. package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
  63. package/src/components/unified-table/hooks/useFilters.ts +53 -0
  64. package/src/components/unified-table/hooks/usePagination.ts +120 -0
  65. package/src/components/unified-table/hooks/useResponsive.ts +50 -0
  66. package/src/components/unified-table/hooks/useSelection.ts +152 -0
  67. package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
  68. package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
  69. package/src/components/unified-table/hooks/useTableState.ts +103 -0
  70. package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
  71. package/src/components/unified-table/hooks/useTableURL.ts +301 -0
  72. package/src/components/unified-table/index.ts +16 -0
  73. package/src/components/unified-table/types.ts +393 -0
  74. package/src/components/unified-table/utils/export.ts +236 -0
  75. package/src/components/unified-table/utils/index.ts +4 -0
  76. package/src/components/unified-table/utils/renderers.ts +105 -0
  77. package/src/components/unified-table/utils/themes.ts +87 -0
  78. package/src/components/unified-table/utils/validation.ts +122 -0
  79. package/src/index.ts +6 -0
  80. package/src/lib/utils.ts +1 -0
  81. package/src/theme/contract.ts +46 -0
  82. package/src/theme/index.ts +9 -0
  83. package/src/theme/tailwind.config.js +70 -0
  84. package/src/theme/tailwind.preset.ts +93 -0
  85. package/src/utils/cn.ts +6 -0
  86. package/src/utils/index.ts +91 -0
@@ -0,0 +1,553 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
4
+ import { UnifiedTableProps, ColumnVisibilityState, SortState, SavedView } from './types'
5
+ import { useSelection, usePagination, useFilters, useTableURL, useColumnReorder, useColumnResize } from './hooks'
6
+ import { useTableKeyboard } from './hooks/useTableKeyboard'
7
+ import { TableFilters } from './components/TableFilters'
8
+ import { BulkActionBar } from './components/BulkActionBar'
9
+ import { DataTableCore } from './components/DataTableCore'
10
+ import { TablePagination } from './components/TablePagination'
11
+ import { StandardTableToolbar } from './components/Toolbar'
12
+ import { Loader2 } from 'lucide-react'
13
+ import { cn } from '../../utils'
14
+
15
+ export function UnifiedTable<TData>({
16
+ data,
17
+ columns,
18
+ tableId,
19
+ getRowId,
20
+ pagination: paginationConfig,
21
+ selection: selectionConfig,
22
+ filters: filtersConfig,
23
+ bulkActions = [],
24
+ rowActions = [],
25
+ search: searchConfig,
26
+ sorting: sortingConfig,
27
+ columnVisibility: columnVisibilityConfig,
28
+ columnReorder: columnReorderConfig,
29
+ columnResize: columnResizeConfig,
30
+ inlineEdit: inlineEditConfig,
31
+ savedViews: savedViewsConfig,
32
+ urlPersistence: urlPersistenceConfig,
33
+ export: exportConfig,
34
+ onRowClick,
35
+ mobileConfig,
36
+ loading = false,
37
+ loadingRows = new Set(),
38
+ className,
39
+ emptyState,
40
+ errorState,
41
+ }: UnifiedTableProps<TData>) {
42
+ const searchInputRef = useRef<HTMLInputElement | null>(null)
43
+ const shouldRestoreFocusRef = useRef(false)
44
+ const tableRef = useRef<HTMLDivElement>(null)
45
+
46
+ // URL state persistence hook
47
+ const urlState = useTableURL({
48
+ tableId,
49
+ persistToUrl: urlPersistenceConfig?.enabled ?? false,
50
+ debounceMs: urlPersistenceConfig?.debounceMs ?? 300,
51
+ })
52
+
53
+ // Initialize state from URL if persistence is enabled
54
+ const initialURLState = useMemo(() => {
55
+ if (!urlPersistenceConfig?.enabled) return null
56
+ return urlState.getURLState()
57
+ }, []) // Only run once on mount
58
+
59
+ // Column visibility state
60
+ const [columnVisibility, setColumnVisibility] = useState<ColumnVisibilityState>(() => {
61
+ // Try to load from localStorage if persistKey is set
62
+ if (columnVisibilityConfig?.persistKey && typeof window !== 'undefined') {
63
+ try {
64
+ const stored = localStorage.getItem(`table-columns-${columnVisibilityConfig.persistKey}`)
65
+ if (stored) {
66
+ return JSON.parse(stored)
67
+ }
68
+ } catch {
69
+ // Ignore localStorage errors
70
+ }
71
+ }
72
+
73
+ // Initialize default visibility
74
+ const initial: ColumnVisibilityState = {}
75
+ columns.forEach(col => {
76
+ if (columnVisibilityConfig?.defaultVisible) {
77
+ initial[col.id] = columnVisibilityConfig.defaultVisible.includes(col.id)
78
+ } else {
79
+ initial[col.id] = true // Default all visible
80
+ }
81
+ })
82
+ return initial
83
+ })
84
+
85
+ // Client-side sorting state (used when no external sorting config is provided)
86
+ const [localSort, setLocalSort] = useState<SortState>(() => {
87
+ // Initialize from URL if available
88
+ if (initialURLState) {
89
+ return {
90
+ sortBy: initialURLState.sortBy,
91
+ sortDirection: initialURLState.sortDirection,
92
+ }
93
+ }
94
+ return { sortBy: null, sortDirection: 'asc' }
95
+ })
96
+
97
+ // Persist column visibility
98
+ useEffect(() => {
99
+ if (columnVisibilityConfig?.persistKey && typeof window !== 'undefined') {
100
+ try {
101
+ localStorage.setItem(
102
+ `table-columns-${columnVisibilityConfig.persistKey}`,
103
+ JSON.stringify(columnVisibility)
104
+ )
105
+ } catch {
106
+ // Ignore localStorage errors
107
+ }
108
+ }
109
+ }, [columnVisibility, columnVisibilityConfig?.persistKey])
110
+
111
+ // Toggle column visibility
112
+ const toggleColumnVisibility = useCallback((columnId: string) => {
113
+ // Don't allow toggling always-visible columns
114
+ if (columnVisibilityConfig?.alwaysVisible?.includes(columnId)) {
115
+ return
116
+ }
117
+ setColumnVisibility(prev => ({
118
+ ...prev,
119
+ [columnId]: !prev[columnId]
120
+ }))
121
+ }, [columnVisibilityConfig?.alwaysVisible])
122
+
123
+ // Filter visible columns
124
+ const visibleColumns = useMemo(() => {
125
+ if (!columnVisibilityConfig?.enabled) {
126
+ return columns
127
+ }
128
+ return columns.filter(col => columnVisibility[col.id] !== false)
129
+ }, [columns, columnVisibility, columnVisibilityConfig?.enabled])
130
+
131
+ // Get hideable columns for the dropdown
132
+ const hideableColumns = useMemo(() => {
133
+ return columns.filter(col => {
134
+ // Can't hide if hideable is explicitly false
135
+ if (col.hideable === false) return false
136
+ // Can't hide if in alwaysVisible list
137
+ if (columnVisibilityConfig?.alwaysVisible?.includes(col.id)) return false
138
+ return true
139
+ })
140
+ }, [columns, columnVisibilityConfig?.alwaysVisible])
141
+
142
+ // Column reordering hook
143
+ const columnReorder = useColumnReorder({
144
+ columns: visibleColumns,
145
+ initialOrder: columnReorderConfig?.initialOrder,
146
+ enabled: columnReorderConfig?.enabled ?? false,
147
+ onOrderChange: columnReorderConfig?.onOrderChange,
148
+ })
149
+
150
+ // Final columns to display (ordered if reorder enabled)
151
+ const displayColumns = columnReorderConfig?.enabled
152
+ ? columnReorder.orderedColumns
153
+ : visibleColumns
154
+
155
+ // Column resize hook
156
+ const columnResize = useColumnResize({
157
+ enabled: columnResizeConfig?.enabled ?? false,
158
+ initialWidths: columnResizeConfig?.initialWidths,
159
+ minWidth: columnResizeConfig?.minWidth ?? 50,
160
+ onWidthChange: columnResizeConfig?.onWidthChange,
161
+ })
162
+
163
+ // Determine sorting state and handler
164
+ const sortingEnabled = sortingConfig?.enabled ?? true // Default to enabled
165
+ const currentSortBy = sortingConfig?.value?.sortBy ?? localSort.sortBy
166
+ const currentSortDirection = sortingConfig?.value?.sortDirection ?? localSort.sortDirection
167
+
168
+ const handleSort = useCallback((columnId: string) => {
169
+ const newDirection = currentSortBy === columnId && currentSortDirection === 'asc' ? 'desc' : 'asc'
170
+ const newSort = { sortBy: columnId, sortDirection: newDirection as 'asc' | 'desc' }
171
+
172
+ if (sortingConfig?.onChange) {
173
+ sortingConfig.onChange(newSort)
174
+ } else {
175
+ setLocalSort(newSort)
176
+ }
177
+
178
+ // Sync to URL if enabled
179
+ urlState.setSortToURL(newSort)
180
+ }, [currentSortBy, currentSortDirection, sortingConfig, urlState])
181
+
182
+ // Ensure data is always an array
183
+ const safeData = useMemo(() => Array.isArray(data) ? data : [], [data])
184
+
185
+ // Apply client-side sorting if no server-side sorting
186
+ const sortedData = useMemo(() => {
187
+ if (sortingConfig?.serverSide || !currentSortBy) {
188
+ return safeData
189
+ }
190
+
191
+ const column = columns.find(c => c.id === currentSortBy)
192
+ if (!column) return safeData
193
+
194
+ return [...safeData].sort((a, b) => {
195
+ let valueA: any
196
+ let valueB: any
197
+
198
+ if (column.sortingFn) {
199
+ return currentSortDirection === 'asc'
200
+ ? column.sortingFn(a, b)
201
+ : column.sortingFn(b, a)
202
+ }
203
+
204
+ if (column.accessorFn) {
205
+ valueA = column.accessorFn(a)
206
+ valueB = column.accessorFn(b)
207
+ } else if (column.accessorKey) {
208
+ // Handle nested keys like 'firm.name'
209
+ const keys = column.accessorKey.split('.')
210
+ valueA = keys.reduce((obj, key) => obj?.[key], a as any)
211
+ valueB = keys.reduce((obj, key) => obj?.[key], b as any)
212
+ } else {
213
+ return 0
214
+ }
215
+
216
+ // Handle null/undefined
217
+ if (valueA == null && valueB == null) return 0
218
+ if (valueA == null) return currentSortDirection === 'asc' ? 1 : -1
219
+ if (valueB == null) return currentSortDirection === 'asc' ? -1 : 1
220
+
221
+ // Compare
222
+ if (typeof valueA === 'string' && typeof valueB === 'string') {
223
+ const comparison = valueA.localeCompare(valueB, undefined, { sensitivity: 'base' })
224
+ return currentSortDirection === 'asc' ? comparison : -comparison
225
+ }
226
+
227
+ if (typeof valueA === 'number' && typeof valueB === 'number') {
228
+ return currentSortDirection === 'asc' ? valueA - valueB : valueB - valueA
229
+ }
230
+
231
+ const comparison = String(valueA).localeCompare(String(valueB))
232
+ return currentSortDirection === 'asc' ? comparison : -comparison
233
+ })
234
+ }, [safeData, columns, currentSortBy, currentSortDirection, sortingConfig?.serverSide])
235
+
236
+ // Pagination hook
237
+ const pagination = usePagination({
238
+ totalCount: paginationConfig?.totalCount || sortedData.length,
239
+ initialPageSize: paginationConfig?.pageSize || 25,
240
+ initialPage: paginationConfig?.currentPage || (initialURLState?.page ?? 1),
241
+ serverSide: paginationConfig?.serverSide || false,
242
+ onPageChange: paginationConfig?.onPageChange,
243
+ })
244
+
245
+ // Selection hook - supports both controlled and uncontrolled modes
246
+ const selection = useSelection({
247
+ currentPageData: sortedData,
248
+ totalCount: paginationConfig?.totalCount || sortedData.length,
249
+ getRowId,
250
+ onSelectionChange: selectionConfig?.onSelectionChange,
251
+ onSelectAllPages: selectionConfig?.onSelectAllPages,
252
+ // External controlled state
253
+ externalSelectedIds: selectionConfig?.selectedIds,
254
+ externalSelectAllPages: selectionConfig?.selectAllPages,
255
+ })
256
+
257
+ // Filters hook
258
+ const filters = useFilters({
259
+ initialFilters: filtersConfig?.value || (initialURLState?.filters ?? {}),
260
+ onChange: filtersConfig?.onChange,
261
+ })
262
+
263
+ // Get current view state for saving
264
+ const getCurrentViewState = useCallback((): Omit<SavedView, 'id' | 'name' | 'createdAt'> => {
265
+ return {
266
+ columnVisibility: columnVisibilityConfig?.enabled ? columnVisibility : undefined,
267
+ columnOrder: columnReorderConfig?.enabled ? columnReorder.columnOrder : undefined,
268
+ sortBy: currentSortBy,
269
+ sortDirection: currentSortDirection,
270
+ filters: filtersConfig?.enabled ? filters.filters : undefined,
271
+ pageSize: paginationConfig?.pageSize,
272
+ }
273
+ }, [
274
+ columnVisibility,
275
+ columnVisibilityConfig?.enabled,
276
+ columnReorder.columnOrder,
277
+ columnReorderConfig?.enabled,
278
+ currentSortBy,
279
+ currentSortDirection,
280
+ filters.filters,
281
+ filtersConfig?.enabled,
282
+ paginationConfig?.pageSize,
283
+ ])
284
+
285
+ // Keyboard navigation hook
286
+ const keyboard = useTableKeyboard({
287
+ data: sortedData,
288
+ getRowId,
289
+ selectedIds: selection.selectedIds,
290
+ onToggleRow: selection.toggleRow,
291
+ onToggleAll: selection.toggleAll,
292
+ onRowClick,
293
+ onDelete: bulkActions.find(action => action.id === 'delete')?.onClick
294
+ ? (ids) => {
295
+ const deleteAction = bulkActions.find(action => action.id === 'delete')
296
+ if (deleteAction) {
297
+ deleteAction.onClick(ids)
298
+ }
299
+ }
300
+ : undefined,
301
+ tableRef,
302
+ enabled: true,
303
+ })
304
+
305
+ // Restore focus to search input after state-driven rerenders (e.g., URL sync)
306
+ useEffect(() => {
307
+ if (!searchConfig?.enabled || !searchConfig.preserveFocus) return
308
+ if (!shouldRestoreFocusRef.current) return
309
+ if (searchInputRef.current) {
310
+ searchInputRef.current.focus()
311
+ const len = searchConfig.value?.length ?? 0
312
+ try {
313
+ searchInputRef.current.setSelectionRange(len, len)
314
+ } catch {
315
+ // selection range may fail on some input types; ignore
316
+ }
317
+ }
318
+ shouldRestoreFocusRef.current = false
319
+ }, [searchConfig?.enabled, searchConfig?.preserveFocus, searchConfig?.value])
320
+
321
+ // External selection state is now synced in useSelection hook
322
+
323
+ // Sync pagination changes to URL
324
+ useEffect(() => {
325
+ if (!urlPersistenceConfig?.enabled) return
326
+ if (!paginationConfig?.enabled) return
327
+
328
+ urlState.setPageToURL(pagination.currentPage)
329
+ }, [pagination.currentPage, urlPersistenceConfig?.enabled, paginationConfig?.enabled, urlState])
330
+
331
+ // Sync filter changes to URL
332
+ useEffect(() => {
333
+ if (!urlPersistenceConfig?.enabled) return
334
+ if (!filtersConfig?.enabled) return
335
+
336
+ urlState.setFiltersToURL(filters.filters)
337
+ }, [filters.filters, urlPersistenceConfig?.enabled, filtersConfig?.enabled, urlState])
338
+
339
+ // Initialize search from URL on mount
340
+ useEffect(() => {
341
+ if (!urlPersistenceConfig?.enabled) return
342
+ if (!searchConfig?.enabled) return
343
+ if (!initialURLState?.search) return
344
+
345
+ // Only set if search value is empty (first mount)
346
+ if (!searchConfig.value && initialURLState.search) {
347
+ searchConfig.onChange(initialURLState.search)
348
+ }
349
+ }, []) // Only run on mount
350
+
351
+ // Check if all current page rows are selected
352
+ const allRowsSelected = useMemo(() => {
353
+ if (sortedData.length === 0) return false
354
+ return sortedData.every(row => selection.selectedIds.has(getRowId(row)))
355
+ }, [sortedData, selection.selectedIds, getRowId])
356
+
357
+ // Prepare selected data for export
358
+ const selectedDataForExport = useMemo(() => {
359
+ if (!selectionConfig?.enabled || selection.selectedIds.size === 0) {
360
+ return undefined
361
+ }
362
+ return sortedData.filter(row => selection.selectedIds.has(getRowId(row)))
363
+ }, [sortedData, selection.selectedIds, getRowId, selectionConfig?.enabled])
364
+
365
+ // Handle bulk action execution
366
+ const handleExecuteBulkAction = async (action: typeof bulkActions[0]) => {
367
+ const selectedIds = selection.selectAllPages
368
+ ? new Set(sortedData.map(getRowId)) // In real implementation, this would fetch all IDs
369
+ : selection.selectedIds
370
+
371
+ // Show confirmation if needed
372
+ if (action.confirmMessage) {
373
+ const message = action.confirmMessage.replace('{count}', selectedIds.size.toString())
374
+ if (!confirm(message)) {
375
+ return
376
+ }
377
+ }
378
+
379
+ // Check max selection
380
+ if (action.maxSelection && selectedIds.size > action.maxSelection) {
381
+ alert(`Maximum ${action.maxSelection} items can be selected for this action`)
382
+ return
383
+ }
384
+
385
+ try {
386
+ await action.onClick(selectedIds)
387
+ // Clear selection after successful action
388
+ selection.clearSelection()
389
+ } catch (error) {
390
+ console.error('Bulk action failed:', error)
391
+ }
392
+ }
393
+
394
+ // Loading state
395
+ if (loading && sortedData.length === 0) {
396
+ return (
397
+ <div className={cn('flex items-center justify-center p-12', className)}>
398
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
399
+ </div>
400
+ )
401
+ }
402
+
403
+ // Empty state
404
+ if (!loading && sortedData.length === 0 && emptyState) {
405
+ return <div className={className}>{emptyState}</div>
406
+ }
407
+
408
+ return (
409
+ <div ref={tableRef} className={cn('space-y-4', className)} tabIndex={0}>
410
+ {/* Standard Table Toolbar */}
411
+ <StandardTableToolbar
412
+ searchEnabled={searchConfig?.enabled}
413
+ searchPlaceholder={searchConfig?.placeholder}
414
+ searchValue={searchConfig?.value}
415
+ onSearchChange={(value) => {
416
+ if (searchConfig) {
417
+ shouldRestoreFocusRef.current = true
418
+ searchConfig.onChange(value)
419
+ urlState.setSearchToURL(value)
420
+ }
421
+ }}
422
+ searchInputRef={searchInputRef}
423
+ searchAutoFocus={searchConfig?.autoFocus}
424
+ columnVisibilityEnabled={columnVisibilityConfig?.enabled}
425
+ hideableColumns={hideableColumns}
426
+ columnVisibility={columnVisibility}
427
+ onToggleColumnVisibility={toggleColumnVisibility}
428
+ exportEnabled={exportConfig?.enabled}
429
+ exportData={sortedData}
430
+ exportColumns={displayColumns}
431
+ exportProps={exportConfig?.enabled ? {
432
+ filteredData: sortedData.length < safeData.length ? sortedData : undefined,
433
+ selectedData: selectedDataForExport,
434
+ baseFilename: exportConfig.baseFilename,
435
+ showProgress: exportConfig.showProgress,
436
+ onExportStart: exportConfig.onExportStart,
437
+ onExportComplete: exportConfig.onExportComplete,
438
+ onExportError: exportConfig.onExportError,
439
+ } : undefined}
440
+ savedViewsEnabled={savedViewsConfig?.enabled}
441
+ savedViews={savedViewsConfig?.views}
442
+ currentViewId={savedViewsConfig?.currentViewId}
443
+ savedViewsProps={savedViewsConfig?.enabled ? {
444
+ onSaveView: savedViewsConfig.onSaveView,
445
+ onUpdateView: savedViewsConfig.onUpdateView,
446
+ onDeleteView: savedViewsConfig.onDeleteView,
447
+ onLoadView: savedViewsConfig.onLoadView || (() => {}),
448
+ getCurrentViewState,
449
+ } : undefined}
450
+ />
451
+
452
+ {/* Filters */}
453
+ {filtersConfig?.enabled && filtersConfig.config && (
454
+ <TableFilters config={filtersConfig.config} filters={filters} />
455
+ )}
456
+
457
+ {/* Bulk Actions Bar */}
458
+ {selectionConfig?.enabled && bulkActions.length > 0 && (
459
+ <BulkActionBar
460
+ selectedCount={selection.getSelectedCount()}
461
+ selectAllPages={selection.selectAllPages}
462
+ totalCount={paginationConfig?.totalCount || sortedData.length}
463
+ bulkActions={bulkActions}
464
+ onClearSelection={selection.clearSelection}
465
+ onExecuteAction={handleExecuteBulkAction}
466
+ />
467
+ )}
468
+
469
+ {/* Selection summary - only show when no bulk actions (BulkActionBar handles this otherwise) */}
470
+ {selectionConfig?.enabled && selection.getSelectedCount() > 0 && bulkActions.length === 0 && (
471
+ <div className="flex items-center justify-between text-sm text-muted-foreground">
472
+ <div>
473
+ {selection.selectAllPages ? (
474
+ <span className="font-medium text-blue-600">
475
+ All {paginationConfig?.totalCount || sortedData.length} items selected
476
+ </span>
477
+ ) : (
478
+ <span>
479
+ {selection.getSelectedCount()} item{selection.getSelectedCount() === 1 ? '' : 's'} selected{allRowsSelected ? ' on this page' : ''}
480
+ </span>
481
+ )}
482
+ </div>
483
+
484
+ {/* Show "Select All Pages" button if not all pages selected and there are multiple pages */}
485
+ {!selection.selectAllPages &&
486
+ allRowsSelected &&
487
+ paginationConfig &&
488
+ (paginationConfig.totalCount || 0) > sortedData.length && (
489
+ <button
490
+ onClick={selection.selectAllPagesToggle}
491
+ className="text-blue-600 hover:text-blue-800 font-medium"
492
+ >
493
+ Select all {paginationConfig.totalCount} items
494
+ </button>
495
+ )}
496
+ </div>
497
+ )}
498
+
499
+ {/* Select all pages option - shown below bulk action bar when applicable */}
500
+ {selectionConfig?.enabled &&
501
+ bulkActions.length > 0 &&
502
+ !selection.selectAllPages &&
503
+ allRowsSelected &&
504
+ paginationConfig &&
505
+ (paginationConfig.totalCount || 0) > sortedData.length && (
506
+ <div className="flex items-center justify-center p-2 bg-blue-50 border border-blue-200 rounded-lg">
507
+ <button
508
+ onClick={selection.selectAllPagesToggle}
509
+ className="text-blue-600 hover:text-blue-800 font-medium text-sm"
510
+ >
511
+ Select all {paginationConfig.totalCount} items across all pages
512
+ </button>
513
+ </div>
514
+ )}
515
+
516
+ {/* Data Table */}
517
+ <DataTableCore
518
+ data={sortedData}
519
+ columns={displayColumns}
520
+ getRowId={getRowId}
521
+ selectionEnabled={selectionConfig?.enabled}
522
+ selectedIds={selection.selectedIds}
523
+ onToggleRow={selection.toggleRow}
524
+ onToggleAll={selection.toggleAll}
525
+ allRowsSelected={allRowsSelected}
526
+ renderSelectionCell={selectionConfig?.renderSelectionCell}
527
+ sortingEnabled={sortingEnabled}
528
+ sortBy={currentSortBy}
529
+ sortDirection={currentSortDirection}
530
+ onSort={handleSort}
531
+ columnReorderEnabled={columnReorderConfig?.enabled}
532
+ onColumnDragEnd={columnReorder.handleDragEnd}
533
+ columnResizeEnabled={columnResizeConfig?.enabled}
534
+ columnWidths={columnResize.columnWidths}
535
+ onColumnResizeStart={columnResize.startResize}
536
+ isResizing={columnResize.isResizing}
537
+ inlineEditEnabled={inlineEditConfig?.enabled}
538
+ onCellEdit={inlineEditConfig?.onSave}
539
+ rowActions={rowActions}
540
+ onRowClick={onRowClick}
541
+ loadingRows={loadingRows}
542
+ />
543
+
544
+ {/* Pagination */}
545
+ {paginationConfig?.enabled && (
546
+ <TablePagination
547
+ pagination={pagination}
548
+ totalFilteredCount={paginationConfig.totalCount}
549
+ />
550
+ )}
551
+ </div>
552
+ )
553
+ }