@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.
- package/README.md +537 -0
- package/package.json +80 -0
- package/src/components/index.ts +50 -0
- package/src/components/navigation/sidebar.tsx +178 -0
- package/src/components/ui/accordion.tsx +58 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +70 -0
- package/src/components/ui/card.tsx +68 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/collapsible.tsx +12 -0
- package/src/components/ui/dialog.tsx +122 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/ui/input.tsx +25 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/popover.tsx +31 -0
- package/src/components/ui/progress.tsx +28 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/select.tsx +160 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/textarea.tsx +24 -0
- package/src/components/ui/tooltip.tsx +30 -0
- package/src/components/unified-table/UnifiedTable.tsx +553 -0
- package/src/components/unified-table/__tests__/components/BulkActionBar.test.tsx +477 -0
- package/src/components/unified-table/__tests__/components/ExportButton.test.tsx +467 -0
- package/src/components/unified-table/__tests__/components/InlineEditCell.test.tsx +159 -0
- package/src/components/unified-table/__tests__/components/SavedViewsDropdown.test.tsx +128 -0
- package/src/components/unified-table/__tests__/components/TablePagination.test.tsx +374 -0
- package/src/components/unified-table/__tests__/hooks/useColumnReorder.test.ts +191 -0
- package/src/components/unified-table/__tests__/hooks/useColumnResize.test.ts +122 -0
- package/src/components/unified-table/__tests__/hooks/useColumnVisibility.test.ts +594 -0
- package/src/components/unified-table/__tests__/hooks/useFilters.test.ts +460 -0
- package/src/components/unified-table/__tests__/hooks/usePagination.test.ts +439 -0
- package/src/components/unified-table/__tests__/hooks/useResponsive.test.ts +421 -0
- package/src/components/unified-table/__tests__/hooks/useSelection.test.ts +367 -0
- package/src/components/unified-table/__tests__/hooks/useTableKeyboard.test.ts +803 -0
- package/src/components/unified-table/__tests__/hooks/useTableState.test.ts +210 -0
- package/src/components/unified-table/__tests__/integration/table-with-selection.test.tsx +624 -0
- package/src/components/unified-table/__tests__/utils/export.test.ts +427 -0
- package/src/components/unified-table/components/BulkActionBar/index.tsx +119 -0
- package/src/components/unified-table/components/DataTableCore/index.tsx +473 -0
- package/src/components/unified-table/components/InlineEditCell/index.tsx +159 -0
- package/src/components/unified-table/components/MobileView/Card.tsx +218 -0
- package/src/components/unified-table/components/MobileView/CardActions.tsx +126 -0
- package/src/components/unified-table/components/MobileView/README.md +411 -0
- package/src/components/unified-table/components/MobileView/index.tsx +77 -0
- package/src/components/unified-table/components/MobileView/types.ts +77 -0
- package/src/components/unified-table/components/TableFilters/index.tsx +298 -0
- package/src/components/unified-table/components/TablePagination/index.tsx +157 -0
- package/src/components/unified-table/components/Toolbar/ExportButton.tsx +229 -0
- package/src/components/unified-table/components/Toolbar/SavedViewsDropdown.tsx +251 -0
- package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +146 -0
- package/src/components/unified-table/components/Toolbar/index.tsx +3 -0
- package/src/components/unified-table/hooks/index.ts +21 -0
- package/src/components/unified-table/hooks/useColumnReorder.ts +90 -0
- package/src/components/unified-table/hooks/useColumnResize.ts +123 -0
- package/src/components/unified-table/hooks/useColumnVisibility.ts +92 -0
- package/src/components/unified-table/hooks/useFilters.ts +53 -0
- package/src/components/unified-table/hooks/usePagination.ts +120 -0
- package/src/components/unified-table/hooks/useResponsive.ts +50 -0
- package/src/components/unified-table/hooks/useSelection.ts +152 -0
- package/src/components/unified-table/hooks/useTableKeyboard.ts +206 -0
- package/src/components/unified-table/hooks/useTablePreferences.ts +198 -0
- package/src/components/unified-table/hooks/useTableState.ts +103 -0
- package/src/components/unified-table/hooks/useTableURL.test.tsx +921 -0
- package/src/components/unified-table/hooks/useTableURL.ts +301 -0
- package/src/components/unified-table/index.ts +16 -0
- package/src/components/unified-table/types.ts +393 -0
- package/src/components/unified-table/utils/export.ts +236 -0
- package/src/components/unified-table/utils/index.ts +4 -0
- package/src/components/unified-table/utils/renderers.ts +105 -0
- package/src/components/unified-table/utils/themes.ts +87 -0
- package/src/components/unified-table/utils/validation.ts +122 -0
- package/src/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/theme/contract.ts +46 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/tailwind.config.js +70 -0
- package/src/theme/tailwind.preset.ts +93 -0
- package/src/utils/cn.ts +6 -0
- 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
|
+
}
|