@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,301 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useRef } from 'react'
4
+ import { useSearchParams, useRouter, usePathname } from 'next/navigation'
5
+ import { SortState, FilterState } from '../types'
6
+
7
+ export interface UseTableURLConfig {
8
+ tableId: string
9
+ persistToUrl: boolean
10
+ debounceMs?: number
11
+ }
12
+
13
+ export interface TableURLState {
14
+ sortBy: string | null
15
+ sortDirection: 'asc' | 'desc'
16
+ filters: FilterState
17
+ page: number
18
+ search: string
19
+ }
20
+
21
+ export interface UseTableURLReturn {
22
+ // Read state from URL
23
+ getURLState: () => TableURLState
24
+
25
+ // Update individual state pieces
26
+ setSortToURL: (sort: SortState) => void
27
+ setFiltersToURL: (filters: FilterState) => void
28
+ setPageToURL: (page: number) => void
29
+ setSearchToURL: (search: string) => void
30
+
31
+ // Clear state
32
+ clearURLState: () => void
33
+
34
+ // Check if URL has state
35
+ hasURLState: () => boolean
36
+ }
37
+
38
+ /**
39
+ * Hook for persisting table state to URL query parameters.
40
+ * Enables shareability and browser navigation for table state.
41
+ *
42
+ * URL params are namespaced with tableId prefix:
43
+ * - {tableId}_sortBy: column ID
44
+ * - {tableId}_sortDir: 'asc' | 'desc'
45
+ * - {tableId}_page: current page number
46
+ * - {tableId}_search: search term
47
+ * - {tableId}_filter_{filterKey}: filter values (JSON encoded)
48
+ *
49
+ * @example
50
+ * const urlState = useTableURL({
51
+ * tableId: 'firms',
52
+ * persistToUrl: true,
53
+ * debounceMs: 300
54
+ * })
55
+ *
56
+ * // Read initial state from URL
57
+ * const initialState = urlState.getURLState()
58
+ *
59
+ * // Update URL when state changes
60
+ * urlState.setSortToURL({ sortBy: 'name', sortDirection: 'asc' })
61
+ * urlState.setSearchToURL('acme')
62
+ */
63
+ export function useTableURL({
64
+ tableId,
65
+ persistToUrl,
66
+ debounceMs = 300,
67
+ }: UseTableURLConfig): UseTableURLReturn {
68
+ const searchParams = useSearchParams()
69
+ const router = useRouter()
70
+ const pathname = usePathname()
71
+
72
+ // Debounce timer ref
73
+ const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
74
+
75
+ // Track if we're currently applying URL changes to avoid loops
76
+ const isApplyingURLRef = useRef(false)
77
+
78
+ /**
79
+ * Build namespaced param key
80
+ */
81
+ const getParamKey = useCallback((key: string): string => {
82
+ return `${tableId}_${key}`
83
+ }, [tableId])
84
+
85
+ /**
86
+ * Parse URL state from current search params
87
+ */
88
+ const getURLState = useCallback((): TableURLState => {
89
+ if (!persistToUrl) {
90
+ return {
91
+ sortBy: null,
92
+ sortDirection: 'asc',
93
+ filters: {},
94
+ page: 1,
95
+ search: '',
96
+ }
97
+ }
98
+
99
+ const sortBy = searchParams.get(getParamKey('sortBy'))
100
+ const sortDirection = searchParams.get(getParamKey('sortDir')) as 'asc' | 'desc' | null
101
+ const page = parseInt(searchParams.get(getParamKey('page')) || '1', 10)
102
+ const search = searchParams.get(getParamKey('search')) || ''
103
+
104
+ // Parse filters (any param starting with tableId_filter_)
105
+ const filters: FilterState = {}
106
+ const filterPrefix = getParamKey('filter_')
107
+
108
+ searchParams.forEach((value, key) => {
109
+ if (key.startsWith(filterPrefix)) {
110
+ const filterKey = key.substring(filterPrefix.length)
111
+ try {
112
+ // Try to parse as JSON first (for complex values)
113
+ filters[filterKey] = JSON.parse(value)
114
+ } catch {
115
+ // Fall back to raw string value
116
+ filters[filterKey] = value
117
+ }
118
+ }
119
+ })
120
+
121
+ return {
122
+ sortBy: sortBy || null,
123
+ sortDirection: sortDirection || 'asc',
124
+ filters,
125
+ page: isNaN(page) || page < 1 ? 1 : page,
126
+ search,
127
+ }
128
+ }, [searchParams, persistToUrl, getParamKey])
129
+
130
+ /**
131
+ * Update URL with new params (debounced)
132
+ */
133
+ const updateURL = useCallback((updates: Record<string, string | null>) => {
134
+ if (!persistToUrl) return
135
+
136
+ // Clear existing debounce timer
137
+ if (debounceTimerRef.current) {
138
+ clearTimeout(debounceTimerRef.current)
139
+ }
140
+
141
+ // Debounce the URL update
142
+ debounceTimerRef.current = setTimeout(() => {
143
+ const newParams = new URLSearchParams(searchParams.toString())
144
+
145
+ // Apply updates
146
+ Object.entries(updates).forEach(([key, value]) => {
147
+ if (value === null || value === '') {
148
+ newParams.delete(key)
149
+ } else {
150
+ newParams.set(key, value)
151
+ }
152
+ })
153
+
154
+ // Only update if params actually changed
155
+ const newParamsString = newParams.toString()
156
+ const currentParamsString = searchParams.toString()
157
+
158
+ if (newParamsString !== currentParamsString) {
159
+ isApplyingURLRef.current = true
160
+ const newURL = newParamsString ? `${pathname}?${newParamsString}` : pathname
161
+ router.replace(newURL, { scroll: false })
162
+
163
+ // Reset flag after a short delay
164
+ setTimeout(() => {
165
+ isApplyingURLRef.current = false
166
+ }, 50)
167
+ }
168
+ }, debounceMs)
169
+ }, [persistToUrl, searchParams, pathname, router, debounceMs])
170
+
171
+ /**
172
+ * Update sort state in URL
173
+ */
174
+ const setSortToURL = useCallback((sort: SortState) => {
175
+ if (!persistToUrl) return
176
+
177
+ updateURL({
178
+ [getParamKey('sortBy')]: sort.sortBy,
179
+ [getParamKey('sortDir')]: sort.sortBy ? sort.sortDirection : null,
180
+ // Reset to page 1 when sorting changes
181
+ [getParamKey('page')]: '1',
182
+ })
183
+ }, [persistToUrl, updateURL, getParamKey])
184
+
185
+ /**
186
+ * Update filters in URL
187
+ */
188
+ const setFiltersToURL = useCallback((filters: FilterState) => {
189
+ if (!persistToUrl) return
190
+
191
+ // Build updates object - clear old filters and set new ones
192
+ const updates: Record<string, string | null> = {}
193
+ const filterPrefix = getParamKey('filter_')
194
+
195
+ // Clear all existing filter params
196
+ searchParams.forEach((_, key) => {
197
+ if (key.startsWith(filterPrefix)) {
198
+ updates[key] = null
199
+ }
200
+ })
201
+
202
+ // Set new filter params
203
+ Object.entries(filters).forEach(([key, value]) => {
204
+ const paramKey = `${filterPrefix}${key}`
205
+ if (value === undefined || value === null || value === '') {
206
+ updates[paramKey] = null
207
+ } else if (typeof value === 'object') {
208
+ // Encode complex values as JSON
209
+ updates[paramKey] = JSON.stringify(value)
210
+ } else {
211
+ updates[paramKey] = String(value)
212
+ }
213
+ })
214
+
215
+ // Reset to page 1 when filters change
216
+ updates[getParamKey('page')] = '1'
217
+
218
+ updateURL(updates)
219
+ }, [persistToUrl, updateURL, getParamKey, searchParams])
220
+
221
+ /**
222
+ * Update page in URL
223
+ */
224
+ const setPageToURL = useCallback((page: number) => {
225
+ if (!persistToUrl) return
226
+
227
+ updateURL({
228
+ [getParamKey('page')]: page > 1 ? String(page) : null, // Remove param if page 1
229
+ })
230
+ }, [persistToUrl, updateURL, getParamKey])
231
+
232
+ /**
233
+ * Update search term in URL
234
+ */
235
+ const setSearchToURL = useCallback((search: string) => {
236
+ if (!persistToUrl) return
237
+
238
+ updateURL({
239
+ [getParamKey('search')]: search || null,
240
+ // Reset to page 1 when search changes
241
+ [getParamKey('page')]: '1',
242
+ })
243
+ }, [persistToUrl, updateURL, getParamKey])
244
+
245
+ /**
246
+ * Clear all table-related URL params
247
+ */
248
+ const clearURLState = useCallback(() => {
249
+ if (!persistToUrl) return
250
+
251
+ const newParams = new URLSearchParams(searchParams.toString())
252
+ const keysToDelete: string[] = []
253
+
254
+ // Find all params for this table
255
+ newParams.forEach((_, key) => {
256
+ if (key.startsWith(`${tableId}_`)) {
257
+ keysToDelete.push(key)
258
+ }
259
+ })
260
+
261
+ // Delete them
262
+ keysToDelete.forEach(key => newParams.delete(key))
263
+
264
+ const newURL = newParams.toString() ? `${pathname}?${newParams.toString()}` : pathname
265
+ router.replace(newURL, { scroll: false })
266
+ }, [persistToUrl, searchParams, pathname, router, tableId])
267
+
268
+ /**
269
+ * Check if URL has any state for this table
270
+ */
271
+ const hasURLState = useCallback((): boolean => {
272
+ if (!persistToUrl) return false
273
+
274
+ let hasState = false
275
+ searchParams.forEach((_, key) => {
276
+ if (key.startsWith(`${tableId}_`)) {
277
+ hasState = true
278
+ }
279
+ })
280
+ return hasState
281
+ }, [persistToUrl, searchParams, tableId])
282
+
283
+ // Cleanup debounce timer on unmount
284
+ useEffect(() => {
285
+ return () => {
286
+ if (debounceTimerRef.current) {
287
+ clearTimeout(debounceTimerRef.current)
288
+ }
289
+ }
290
+ }, [])
291
+
292
+ return {
293
+ getURLState,
294
+ setSortToURL,
295
+ setFiltersToURL,
296
+ setPageToURL,
297
+ setSearchToURL,
298
+ clearURLState,
299
+ hasURLState,
300
+ }
301
+ }
@@ -0,0 +1,16 @@
1
+ export { UnifiedTable } from './UnifiedTable'
2
+ export * from './types'
3
+
4
+ export { MobileView, Card, CardActions } from './components/MobileView'
5
+ export type {
6
+ MobileViewProps,
7
+ MobileCardConfig,
8
+ MobileCardField,
9
+ MobileCardAction,
10
+ CardProps,
11
+ CardActionsProps,
12
+ } from './components/MobileView'
13
+ export { MOBILE_BREAKPOINT } from './components/MobileView'
14
+
15
+ export * from './hooks'
16
+ export * from './utils'
@@ -0,0 +1,393 @@
1
+ import { ReactNode } from 'react'
2
+ import { LucideIcon } from 'lucide-react'
3
+
4
+ // Core table configuration
5
+ export interface UnifiedTableProps<TData> {
6
+ // Data & Columns
7
+ data: TData[]
8
+ columns: ColumnConfig<TData>[]
9
+
10
+ // Identification
11
+ tableId: string
12
+ getRowId: (row: TData) => string
13
+
14
+ // Pagination
15
+ pagination?: PaginationConfig
16
+
17
+ // Selection
18
+ selection?: SelectionConfig
19
+
20
+ // Filters
21
+ filters?: FiltersConfig
22
+
23
+ // Bulk Actions
24
+ bulkActions?: BulkAction[]
25
+
26
+ // Row Actions
27
+ rowActions?: RowAction<TData>[]
28
+
29
+ // Search
30
+ search?: SearchConfig
31
+
32
+ // Sorting
33
+ sorting?: SortingConfig
34
+
35
+ // Column Visibility
36
+ columnVisibility?: ColumnVisibilityConfig
37
+
38
+ // Column Reordering
39
+ columnReorder?: ColumnReorderConfig
40
+
41
+ // Column Resizing
42
+ columnResize?: ColumnResizeConfig
43
+
44
+ // Inline Editing
45
+ inlineEdit?: InlineEditConfig<TData>
46
+
47
+ // Saved Views
48
+ savedViews?: SavedViewsConfig
49
+
50
+ // URL Persistence
51
+ urlPersistence?: URLPersistenceConfig
52
+
53
+ // Export
54
+ export?: ExportConfig
55
+
56
+ // Navigation
57
+ onRowClick?: (row: TData) => void
58
+
59
+ // Mobile
60
+ mobileConfig?: MobileCardConfig<TData>
61
+
62
+ // Loading States
63
+ loading?: boolean
64
+ loadingRows?: Set<string>
65
+
66
+ // Customization
67
+ className?: string
68
+ emptyState?: ReactNode
69
+ errorState?: ReactNode
70
+ }
71
+
72
+ // Column Configuration
73
+ export interface ColumnConfig<TData> {
74
+ id: string
75
+ header: string | ((props: any) => ReactNode)
76
+ accessorKey?: string
77
+ accessorFn?: (row: TData) => any
78
+ cell?: (row: TData) => ReactNode
79
+ sortable?: boolean
80
+ sortingFn?: (a: TData, b: TData) => number
81
+ width?: string | number
82
+ minWidth?: string | number
83
+ maxWidth?: string | number
84
+ mobilePrimary?: boolean
85
+ mobileSecondary?: boolean
86
+ hideOnMobile?: boolean
87
+ hideable?: boolean // Whether this column can be hidden (default: true)
88
+ editable?: boolean // Whether this column can be edited inline (default: false)
89
+ editType?: 'text' | 'number' | 'select' | 'date' // Type of editor to use
90
+ editOptions?: string[] // Options for select type
91
+ validate?: (value: any, row: TData) => string | null // Validation function (returns error message or null)
92
+ }
93
+
94
+ // Pagination Configuration
95
+ export interface PaginationConfig {
96
+ enabled: boolean
97
+ pageSize: number
98
+ totalCount: number
99
+ currentPage: number
100
+ serverSide: boolean
101
+ onPageChange: (page: number) => void
102
+ }
103
+
104
+ // Selection Configuration
105
+ export interface SelectionConfig {
106
+ enabled: boolean
107
+ selectAllPages?: boolean
108
+ selectedIds?: Set<string>
109
+ onSelectionChange?: (ids: Set<string>) => void
110
+ onSelectAllPages?: (enabled: boolean) => void
111
+ renderSelectionCell?: (row: any) => ReactNode
112
+ }
113
+
114
+ // Filter Configuration
115
+ export interface FiltersConfig {
116
+ enabled: boolean
117
+ position: 'top' | 'side'
118
+ collapsible: boolean
119
+ config: FilterConfig
120
+ value: FilterState
121
+ onChange: (filters: FilterState) => void
122
+ presets?: FilterPreset[]
123
+ showPresets?: boolean // Show preset chips in toolbar (default: true if presets provided)
124
+ }
125
+
126
+ export interface FilterConfig {
127
+ sections: FilterSection[]
128
+ }
129
+
130
+ export interface FilterSection {
131
+ id: string
132
+ type: 'chips' | 'buckets' | 'dropdown' | 'search' | 'range' | 'date' | 'collapsible' | 'custom'
133
+ label?: string
134
+ filters?: FilterItem[]
135
+ buckets?: FilterBucket[]
136
+ // Custom filter handler for complex filter logic (type: 'custom')
137
+ customFilter?: CustomFilterHandler
138
+ }
139
+
140
+ export interface CustomFilterHandler {
141
+ // Render the custom filter UI
142
+ render: (value: any, onChange: (value: any) => void) => ReactNode
143
+ // Apply filter to data (client-side filtering)
144
+ apply?: <TData>(data: TData[], filterValue: any) => TData[]
145
+ // Convert filter value to API query params (server-side filtering)
146
+ toApiParams?: (filterValue: any) => Record<string, string>
147
+ // Default value for the filter
148
+ defaultValue?: any
149
+ // Check if filter is active (for showing clear button, etc.)
150
+ isActive?: (value: any) => boolean
151
+ }
152
+
153
+ export interface FilterItem {
154
+ id: string
155
+ label: string
156
+ type?: 'chips' | 'dropdown' | 'search' | 'checkbox'
157
+ options?: string[]
158
+ multiple?: boolean
159
+ }
160
+
161
+ export interface FilterBucket {
162
+ label: string
163
+ min: number
164
+ max: number
165
+ }
166
+
167
+ export interface FilterState {
168
+ [key: string]: any
169
+ }
170
+
171
+ export interface FilterPreset {
172
+ id: string
173
+ label: string
174
+ icon?: LucideIcon
175
+ filters: FilterState
176
+ description?: string // Tooltip text
177
+ }
178
+
179
+ // Bulk Actions
180
+ export interface BulkAction {
181
+ id: string
182
+ label: string
183
+ icon: LucideIcon
184
+ variant: 'default' | 'gradient-purple' | 'gradient-green' | 'gradient-indigo' | 'gradient-orange' | 'gradient-blue'
185
+ onClick: (selectedIds: Set<string>) => Promise<void> | void
186
+ maxSelection?: number
187
+ confirmMessage?: string
188
+ disabled?: (selectedIds: Set<string>) => boolean
189
+ }
190
+
191
+ // Row Actions
192
+ export interface RowAction<TData> {
193
+ id: string
194
+ label: string
195
+ icon?: LucideIcon
196
+ onClick: (row: TData) => void
197
+ disabled?: (row: TData) => boolean
198
+ destructive?: boolean
199
+ separator?: boolean
200
+ }
201
+
202
+ // Search Configuration
203
+ export interface SearchConfig {
204
+ enabled: boolean
205
+ placeholder: string
206
+ value: string
207
+ onChange: (value: string) => void
208
+ debounceMs?: number
209
+ autoFocus?: boolean
210
+ preserveFocus?: boolean
211
+ }
212
+
213
+ // Sorting Configuration
214
+ export interface SortingConfig {
215
+ enabled: boolean
216
+ serverSide: boolean
217
+ value?: SortState
218
+ onChange?: (sort: SortState) => void
219
+ }
220
+
221
+ export interface SortState {
222
+ sortBy: string | null
223
+ sortDirection: 'asc' | 'desc'
224
+ }
225
+
226
+ // URL Persistence Configuration
227
+ export interface URLPersistenceConfig {
228
+ enabled: boolean
229
+ debounceMs?: number
230
+ }
231
+
232
+ // Column Visibility Configuration
233
+ export interface ColumnVisibilityConfig {
234
+ enabled: boolean
235
+ defaultVisible?: string[] // Column IDs that are visible by default (if not set, all are visible)
236
+ alwaysVisible?: string[] // Column IDs that cannot be hidden
237
+ persistKey?: string // localStorage key to persist visibility state
238
+ }
239
+
240
+ export interface ColumnVisibilityState {
241
+ [columnId: string]: boolean
242
+ }
243
+
244
+ // Column Reorder Configuration
245
+ export interface ColumnReorderConfig {
246
+ enabled: boolean
247
+ initialOrder?: string[] | null // Initial column order (from saved preferences)
248
+ onOrderChange?: (order: string[]) => void // Callback when order changes (for persistence)
249
+ }
250
+
251
+ // Column Resize Configuration
252
+ export interface ColumnResizeConfig {
253
+ enabled: boolean
254
+ initialWidths?: { [columnId: string]: number } | null // Initial column widths (from saved preferences)
255
+ minWidth?: number // Minimum column width in pixels (default: 50)
256
+ onWidthChange?: (widths: { [columnId: string]: number }) => void // Callback when widths change
257
+ }
258
+
259
+ // Inline Edit Configuration
260
+ export interface InlineEditConfig<TData> {
261
+ enabled: boolean
262
+ onSave: (rowId: string, columnId: string, value: any, row: TData) => Promise<void> // Save callback
263
+ onCancel?: () => void // Cancel callback
264
+ optimisticUpdate?: boolean // Apply changes immediately before save completes (default: true)
265
+ }
266
+
267
+ // Saved View Configuration
268
+ export interface SavedView {
269
+ id: string
270
+ name: string
271
+ isDefault?: boolean
272
+ createdAt: string
273
+ // View settings
274
+ columnVisibility?: { [columnId: string]: boolean }
275
+ columnOrder?: string[]
276
+ sortBy?: string | null
277
+ sortDirection?: 'asc' | 'desc'
278
+ filters?: { [key: string]: any }
279
+ pageSize?: number
280
+ }
281
+
282
+ export interface SavedViewsConfig {
283
+ enabled: boolean
284
+ views?: SavedView[] // Initial saved views
285
+ currentViewId?: string | null // Currently active view
286
+ onSaveView: (view: Omit<SavedView, 'id' | 'createdAt'>) => Promise<SavedView> // Save a new view
287
+ onUpdateView?: (viewId: string, updates: Partial<SavedView>) => Promise<void> // Update existing view
288
+ onDeleteView?: (viewId: string) => Promise<void> // Delete a view
289
+ onLoadView?: (viewId: string) => void // Called when user loads a view
290
+ getCurrentViewState?: () => Omit<SavedView, 'id' | 'name' | 'createdAt'> // Get current table state for saving
291
+ }
292
+
293
+ // Export Configuration
294
+ export interface ExportConfig {
295
+ enabled: boolean
296
+ baseFilename?: string // Base filename for exports (default: 'export')
297
+ formats?: ('csv' | 'excel')[] // Available export formats (default: both)
298
+ showProgress?: boolean // Show progress during export (default: true)
299
+ onExportStart?: (format: 'csv' | 'excel', scope: 'all' | 'filtered' | 'selected') => void
300
+ onExportComplete?: (format: 'csv' | 'excel', scope: 'all' | 'filtered' | 'selected', rowCount: number) => void
301
+ onExportError?: (error: Error) => void
302
+ }
303
+
304
+ // Mobile Card Configuration
305
+ export interface MobileCardConfig<TData> {
306
+ titleKey: string
307
+ subtitleKey?: string
308
+ descriptionKey?: string
309
+ primaryFields: string[]
310
+ secondaryFields?: string[]
311
+ renderCustomContent?: (row: TData) => ReactNode
312
+ }
313
+
314
+ // Internal State Types
315
+ export interface TableState<TData> {
316
+ data: TData[]
317
+ filteredData: TData[]
318
+ displayedData: TData[]
319
+
320
+ // Pagination
321
+ currentPage: number
322
+ pageSize: number
323
+ totalPages: number
324
+ totalCount: number
325
+
326
+ // Selection
327
+ selectedIds: Set<string>
328
+ selectAllPages: boolean
329
+
330
+ // Filters
331
+ activeFilters: FilterState
332
+
333
+ // Sorting
334
+ sortBy: string | null
335
+ sortDirection: 'asc' | 'desc'
336
+
337
+ // Search
338
+ searchTerm: string
339
+ debouncedSearchTerm: string
340
+
341
+ // UI
342
+ loading: boolean
343
+ loadingRows: Set<string>
344
+ error: Error | null
345
+ viewMode: 'table' | 'card'
346
+ }
347
+
348
+ // Hook Return Types
349
+ export interface UseTableStateReturn<TData> {
350
+ state: TableState<TData>
351
+ setData: (data: TData[]) => void
352
+ setPage: (page: number) => void
353
+ setPageSize: (size: number) => void
354
+ setLoading: (loading: boolean) => void
355
+ setLoadingRows: (ids: Set<string>) => void
356
+ setError: (error: Error | null) => void
357
+ setViewMode: (mode: 'table' | 'card') => void
358
+ }
359
+
360
+ export interface UseSelectionReturn {
361
+ selectedIds: Set<string>
362
+ selectAllPages: boolean
363
+ toggleRow: (id: string) => void
364
+ toggleAll: () => void
365
+ selectAllPagesToggle: () => void
366
+ clearSelection: () => void
367
+ getSelectedCount: () => number
368
+ }
369
+
370
+ export interface UsePaginationReturn {
371
+ currentPage: number
372
+ pageSize: number
373
+ totalPages: number
374
+ totalCount: number
375
+ canGoNext: boolean
376
+ canGoPrevious: boolean
377
+ goToPage: (page: number) => void
378
+ goToFirstPage: () => void
379
+ goToLastPage: () => void
380
+ goToNextPage: () => void
381
+ goToPreviousPage: () => void
382
+ getPageNumbers: () => (number | '...')[]
383
+ getDisplayRange: () => { start: number; end: number }
384
+ }
385
+
386
+ export interface UseFiltersReturn {
387
+ filters: FilterState
388
+ setFilter: (key: string, value: any) => void
389
+ clearFilter: (key: string) => void
390
+ clearAllFilters: () => void
391
+ hasActiveFilters: () => boolean
392
+ getActiveFilterCount: () => number
393
+ }