@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,460 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useFilters } from '../../hooks/useFilters'
3
+ import { FilterState } from '../../types'
4
+
5
+ describe('useFilters', () => {
6
+ describe('Initialization', () => {
7
+ it('should initialize with empty filters', () => {
8
+ const { result } = renderHook(() => useFilters({}))
9
+
10
+ expect(result.current.filters).toEqual({})
11
+ expect(result.current.hasActiveFilters()).toBe(false)
12
+ expect(result.current.getActiveFilterCount()).toBe(0)
13
+ })
14
+
15
+ it('should initialize with provided filters', () => {
16
+ const initialFilters: FilterState = {
17
+ status: 'active',
18
+ category: 'tech',
19
+ }
20
+
21
+ const { result } = renderHook(() =>
22
+ useFilters({ initialFilters })
23
+ )
24
+
25
+ expect(result.current.filters).toEqual(initialFilters)
26
+ expect(result.current.hasActiveFilters()).toBe(true)
27
+ expect(result.current.getActiveFilterCount()).toBe(2)
28
+ })
29
+ })
30
+
31
+ describe('Set Filter', () => {
32
+ it('should set a single filter', () => {
33
+ const { result } = renderHook(() => useFilters({}))
34
+
35
+ act(() => {
36
+ result.current.setFilter('status', 'active')
37
+ })
38
+
39
+ expect(result.current.filters).toEqual({ status: 'active' })
40
+ expect(result.current.hasActiveFilters()).toBe(true)
41
+ expect(result.current.getActiveFilterCount()).toBe(1)
42
+ })
43
+
44
+ it('should set multiple filters', () => {
45
+ const { result } = renderHook(() => useFilters({}))
46
+
47
+ act(() => {
48
+ result.current.setFilter('status', 'active')
49
+ result.current.setFilter('category', 'tech')
50
+ result.current.setFilter('priority', 'high')
51
+ })
52
+
53
+ expect(result.current.filters).toEqual({
54
+ status: 'active',
55
+ category: 'tech',
56
+ priority: 'high',
57
+ })
58
+ expect(result.current.getActiveFilterCount()).toBe(3)
59
+ })
60
+
61
+ it('should update existing filter', () => {
62
+ const { result } = renderHook(() =>
63
+ useFilters({ initialFilters: { status: 'active' } })
64
+ )
65
+
66
+ act(() => {
67
+ result.current.setFilter('status', 'inactive')
68
+ })
69
+
70
+ expect(result.current.filters).toEqual({ status: 'inactive' })
71
+ })
72
+
73
+ it('should handle different value types', () => {
74
+ const { result } = renderHook(() => useFilters({}))
75
+
76
+ act(() => {
77
+ result.current.setFilter('string', 'value')
78
+ result.current.setFilter('number', 42)
79
+ result.current.setFilter('boolean', true)
80
+ result.current.setFilter('array', ['a', 'b', 'c'])
81
+ result.current.setFilter('object', { key: 'value' })
82
+ })
83
+
84
+ expect(result.current.filters).toEqual({
85
+ string: 'value',
86
+ number: 42,
87
+ boolean: true,
88
+ array: ['a', 'b', 'c'],
89
+ object: { key: 'value' },
90
+ })
91
+ })
92
+
93
+ it('should call onChange callback when setting filter', () => {
94
+ const onChange = jest.fn()
95
+ const { result } = renderHook(() => useFilters({ onChange }))
96
+
97
+ act(() => {
98
+ result.current.setFilter('status', 'active')
99
+ })
100
+
101
+ expect(onChange).toHaveBeenCalledWith({ status: 'active' })
102
+ })
103
+
104
+ it('should call onChange with all filters', () => {
105
+ const onChange = jest.fn()
106
+ const { result } = renderHook(() =>
107
+ useFilters({ initialFilters: { category: 'tech' }, onChange })
108
+ )
109
+
110
+ onChange.mockClear()
111
+
112
+ act(() => {
113
+ result.current.setFilter('status', 'active')
114
+ })
115
+
116
+ expect(onChange).toHaveBeenCalledWith({
117
+ category: 'tech',
118
+ status: 'active',
119
+ })
120
+ })
121
+ })
122
+
123
+ describe('Clear Filter', () => {
124
+ it('should clear a single filter', () => {
125
+ const { result } = renderHook(() =>
126
+ useFilters({
127
+ initialFilters: {
128
+ status: 'active',
129
+ category: 'tech',
130
+ },
131
+ })
132
+ )
133
+
134
+ act(() => {
135
+ result.current.clearFilter('status')
136
+ })
137
+
138
+ expect(result.current.filters).toEqual({ category: 'tech' })
139
+ expect(result.current.getActiveFilterCount()).toBe(1)
140
+ })
141
+
142
+ it('should clear non-existent filter without error', () => {
143
+ const { result } = renderHook(() =>
144
+ useFilters({ initialFilters: { status: 'active' } })
145
+ )
146
+
147
+ act(() => {
148
+ result.current.clearFilter('nonexistent')
149
+ })
150
+
151
+ expect(result.current.filters).toEqual({ status: 'active' })
152
+ })
153
+
154
+ it('should call onChange callback when clearing filter', () => {
155
+ const onChange = jest.fn()
156
+ const { result } = renderHook(() =>
157
+ useFilters({
158
+ initialFilters: { status: 'active', category: 'tech' },
159
+ onChange,
160
+ })
161
+ )
162
+
163
+ onChange.mockClear()
164
+
165
+ act(() => {
166
+ result.current.clearFilter('status')
167
+ })
168
+
169
+ expect(onChange).toHaveBeenCalledWith({ category: 'tech' })
170
+ })
171
+
172
+ it('should clear last filter', () => {
173
+ const { result } = renderHook(() =>
174
+ useFilters({ initialFilters: { status: 'active' } })
175
+ )
176
+
177
+ act(() => {
178
+ result.current.clearFilter('status')
179
+ })
180
+
181
+ expect(result.current.filters).toEqual({})
182
+ expect(result.current.hasActiveFilters()).toBe(false)
183
+ })
184
+ })
185
+
186
+ describe('Clear All Filters', () => {
187
+ it('should clear all filters', () => {
188
+ const { result } = renderHook(() =>
189
+ useFilters({
190
+ initialFilters: {
191
+ status: 'active',
192
+ category: 'tech',
193
+ priority: 'high',
194
+ },
195
+ })
196
+ )
197
+
198
+ act(() => {
199
+ result.current.clearAllFilters()
200
+ })
201
+
202
+ expect(result.current.filters).toEqual({})
203
+ expect(result.current.hasActiveFilters()).toBe(false)
204
+ expect(result.current.getActiveFilterCount()).toBe(0)
205
+ })
206
+
207
+ it('should call onChange callback when clearing all', () => {
208
+ const onChange = jest.fn()
209
+ const { result } = renderHook(() =>
210
+ useFilters({
211
+ initialFilters: { status: 'active', category: 'tech' },
212
+ onChange,
213
+ })
214
+ )
215
+
216
+ onChange.mockClear()
217
+
218
+ act(() => {
219
+ result.current.clearAllFilters()
220
+ })
221
+
222
+ expect(onChange).toHaveBeenCalledWith({})
223
+ })
224
+
225
+ it('should work when already empty', () => {
226
+ const { result } = renderHook(() => useFilters({}))
227
+
228
+ act(() => {
229
+ result.current.clearAllFilters()
230
+ })
231
+
232
+ expect(result.current.filters).toEqual({})
233
+ expect(result.current.hasActiveFilters()).toBe(false)
234
+ })
235
+ })
236
+
237
+ describe('Has Active Filters', () => {
238
+ it('should return false for empty filters', () => {
239
+ const { result } = renderHook(() => useFilters({}))
240
+
241
+ expect(result.current.hasActiveFilters()).toBe(false)
242
+ })
243
+
244
+ it('should return true when filters exist', () => {
245
+ const { result } = renderHook(() =>
246
+ useFilters({ initialFilters: { status: 'active' } })
247
+ )
248
+
249
+ expect(result.current.hasActiveFilters()).toBe(true)
250
+ })
251
+
252
+ it('should update when adding filter', () => {
253
+ const { result } = renderHook(() => useFilters({}))
254
+
255
+ expect(result.current.hasActiveFilters()).toBe(false)
256
+
257
+ act(() => {
258
+ result.current.setFilter('status', 'active')
259
+ })
260
+
261
+ expect(result.current.hasActiveFilters()).toBe(true)
262
+ })
263
+
264
+ it('should update when clearing filter', () => {
265
+ const { result } = renderHook(() =>
266
+ useFilters({ initialFilters: { status: 'active' } })
267
+ )
268
+
269
+ expect(result.current.hasActiveFilters()).toBe(true)
270
+
271
+ act(() => {
272
+ result.current.clearFilter('status')
273
+ })
274
+
275
+ expect(result.current.hasActiveFilters()).toBe(false)
276
+ })
277
+ })
278
+
279
+ describe('Get Active Filter Count', () => {
280
+ it('should return 0 for empty filters', () => {
281
+ const { result } = renderHook(() => useFilters({}))
282
+
283
+ expect(result.current.getActiveFilterCount()).toBe(0)
284
+ })
285
+
286
+ it('should return correct count', () => {
287
+ const { result } = renderHook(() =>
288
+ useFilters({
289
+ initialFilters: {
290
+ status: 'active',
291
+ category: 'tech',
292
+ priority: 'high',
293
+ },
294
+ })
295
+ )
296
+
297
+ expect(result.current.getActiveFilterCount()).toBe(3)
298
+ })
299
+
300
+ it('should update when adding filters', () => {
301
+ const { result } = renderHook(() => useFilters({}))
302
+
303
+ expect(result.current.getActiveFilterCount()).toBe(0)
304
+
305
+ act(() => {
306
+ result.current.setFilter('status', 'active')
307
+ })
308
+
309
+ expect(result.current.getActiveFilterCount()).toBe(1)
310
+
311
+ act(() => {
312
+ result.current.setFilter('category', 'tech')
313
+ })
314
+
315
+ expect(result.current.getActiveFilterCount()).toBe(2)
316
+ })
317
+
318
+ it('should update when removing filters', () => {
319
+ const { result } = renderHook(() =>
320
+ useFilters({
321
+ initialFilters: {
322
+ status: 'active',
323
+ category: 'tech',
324
+ },
325
+ })
326
+ )
327
+
328
+ expect(result.current.getActiveFilterCount()).toBe(2)
329
+
330
+ act(() => {
331
+ result.current.clearFilter('status')
332
+ })
333
+
334
+ expect(result.current.getActiveFilterCount()).toBe(1)
335
+ })
336
+ })
337
+
338
+ describe('Complex Filter Scenarios', () => {
339
+ it('should handle rapid filter changes', () => {
340
+ const { result } = renderHook(() => useFilters({}))
341
+
342
+ act(() => {
343
+ result.current.setFilter('a', 1)
344
+ result.current.setFilter('b', 2)
345
+ result.current.setFilter('c', 3)
346
+ result.current.clearFilter('b')
347
+ result.current.setFilter('d', 4)
348
+ })
349
+
350
+ expect(result.current.filters).toEqual({ a: 1, c: 3, d: 4 })
351
+ expect(result.current.getActiveFilterCount()).toBe(3)
352
+ })
353
+
354
+ it('should handle setting filter to undefined', () => {
355
+ const { result } = renderHook(() => useFilters({}))
356
+
357
+ act(() => {
358
+ result.current.setFilter('status', undefined)
359
+ })
360
+
361
+ expect(result.current.filters).toEqual({ status: undefined })
362
+ expect(result.current.hasActiveFilters()).toBe(true)
363
+ })
364
+
365
+ it('should handle setting filter to null', () => {
366
+ const { result } = renderHook(() => useFilters({}))
367
+
368
+ act(() => {
369
+ result.current.setFilter('status', null)
370
+ })
371
+
372
+ expect(result.current.filters).toEqual({ status: null })
373
+ expect(result.current.hasActiveFilters()).toBe(true)
374
+ })
375
+
376
+ it('should handle setting filter to empty string', () => {
377
+ const { result } = renderHook(() => useFilters({}))
378
+
379
+ act(() => {
380
+ result.current.setFilter('status', '')
381
+ })
382
+
383
+ expect(result.current.filters).toEqual({ status: '' })
384
+ expect(result.current.hasActiveFilters()).toBe(true)
385
+ })
386
+ })
387
+
388
+ describe('Callback Integration', () => {
389
+ it('should call onChange for each operation', () => {
390
+ const onChange = jest.fn()
391
+ const { result } = renderHook(() => useFilters({ onChange }))
392
+
393
+ act(() => {
394
+ result.current.setFilter('a', 1)
395
+ })
396
+
397
+ expect(onChange).toHaveBeenCalledTimes(1)
398
+
399
+ act(() => {
400
+ result.current.setFilter('b', 2)
401
+ })
402
+
403
+ expect(onChange).toHaveBeenCalledTimes(2)
404
+
405
+ act(() => {
406
+ result.current.clearFilter('a')
407
+ })
408
+
409
+ expect(onChange).toHaveBeenCalledTimes(3)
410
+
411
+ act(() => {
412
+ result.current.clearAllFilters()
413
+ })
414
+
415
+ expect(onChange).toHaveBeenCalledTimes(4)
416
+ })
417
+
418
+ it('should not call onChange when no callback provided', () => {
419
+ const { result } = renderHook(() => useFilters({}))
420
+
421
+ // Should not throw error
422
+ expect(() => {
423
+ act(() => {
424
+ result.current.setFilter('status', 'active')
425
+ result.current.clearFilter('status')
426
+ result.current.clearAllFilters()
427
+ })
428
+ }).not.toThrow()
429
+ })
430
+ })
431
+
432
+ describe('Memoization', () => {
433
+ it('should maintain filter reference when unchanged', () => {
434
+ const { result, rerender } = renderHook(() =>
435
+ useFilters({ initialFilters: { status: 'active' } })
436
+ )
437
+
438
+ const firstFilters = result.current.filters
439
+ rerender()
440
+
441
+ // Note: filters object will be different due to state management,
442
+ // but values should be the same
443
+ expect(result.current.filters).toEqual(firstFilters)
444
+ })
445
+
446
+ it('should provide stable function references', () => {
447
+ const { result, rerender } = renderHook(() => useFilters({}))
448
+
449
+ const firstSetFilter = result.current.setFilter
450
+ const firstClearFilter = result.current.clearFilter
451
+ const firstClearAllFilters = result.current.clearAllFilters
452
+
453
+ rerender()
454
+
455
+ expect(result.current.setFilter).toBe(firstSetFilter)
456
+ expect(result.current.clearFilter).toBe(firstClearFilter)
457
+ expect(result.current.clearAllFilters).toBe(firstClearAllFilters)
458
+ })
459
+ })
460
+ })