@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,427 @@
1
+ import { exportToCSV, exportToExcel, exportData, generateExportFilename } from '../../utils/export'
2
+ import { ColumnConfig } from '../../types'
3
+ import * as XLSX from 'xlsx'
4
+
5
+ // Mock the XLSX library
6
+ jest.mock('xlsx', () => ({
7
+ utils: {
8
+ aoa_to_sheet: jest.fn(),
9
+ book_new: jest.fn(),
10
+ book_append_sheet: jest.fn(),
11
+ },
12
+ write: jest.fn(),
13
+ }))
14
+
15
+ // Mock URL.createObjectURL and URL.revokeObjectURL
16
+ global.URL.createObjectURL = jest.fn(() => 'blob:mock-url')
17
+ global.URL.revokeObjectURL = jest.fn()
18
+
19
+ // Mock document methods
20
+ const mockClick = jest.fn()
21
+ const mockAppendChild = jest.fn()
22
+ const mockRemoveChild = jest.fn()
23
+
24
+ Object.defineProperty(document, 'createElement', {
25
+ writable: true,
26
+ value: jest.fn().mockReturnValue({
27
+ click: mockClick,
28
+ href: '',
29
+ download: '',
30
+ }),
31
+ })
32
+
33
+ Object.defineProperty(document.body, 'appendChild', {
34
+ writable: true,
35
+ value: mockAppendChild,
36
+ })
37
+
38
+ Object.defineProperty(document.body, 'removeChild', {
39
+ writable: true,
40
+ value: mockRemoveChild,
41
+ })
42
+
43
+ interface TestData {
44
+ id: string
45
+ name: string
46
+ age: number
47
+ isActive: boolean
48
+ company?: {
49
+ name: string
50
+ }
51
+ tags?: string[]
52
+ createdAt: Date
53
+ }
54
+
55
+ describe('Export Utilities', () => {
56
+ beforeEach(() => {
57
+ jest.clearAllMocks()
58
+ })
59
+
60
+ const sampleData: TestData[] = [
61
+ {
62
+ id: '1',
63
+ name: 'John Doe',
64
+ age: 30,
65
+ isActive: true,
66
+ company: { name: 'Acme Corp' },
67
+ tags: ['developer', 'manager'],
68
+ createdAt: new Date('2023-01-15'),
69
+ },
70
+ {
71
+ id: '2',
72
+ name: 'Jane Smith',
73
+ age: 25,
74
+ isActive: false,
75
+ company: { name: 'Tech Inc' },
76
+ tags: ['designer'],
77
+ createdAt: new Date('2023-02-20'),
78
+ },
79
+ {
80
+ id: '3',
81
+ name: 'Bob Johnson',
82
+ age: 35,
83
+ isActive: true,
84
+ createdAt: new Date('2023-03-10'),
85
+ },
86
+ ]
87
+
88
+ const sampleColumns: ColumnConfig<TestData>[] = [
89
+ {
90
+ id: 'id',
91
+ header: 'ID',
92
+ accessorKey: 'id',
93
+ },
94
+ {
95
+ id: 'name',
96
+ header: 'Name',
97
+ accessorKey: 'name',
98
+ },
99
+ {
100
+ id: 'age',
101
+ header: 'Age',
102
+ accessorKey: 'age',
103
+ },
104
+ {
105
+ id: 'isActive',
106
+ header: 'Active',
107
+ accessorKey: 'isActive',
108
+ },
109
+ {
110
+ id: 'company',
111
+ header: 'Company',
112
+ accessorKey: 'company.name',
113
+ },
114
+ {
115
+ id: 'tags',
116
+ header: 'Tags',
117
+ accessorFn: (row) => row.tags?.join(', ') || '',
118
+ },
119
+ {
120
+ id: 'createdAt',
121
+ header: 'Created At',
122
+ accessorKey: 'createdAt',
123
+ },
124
+ ]
125
+
126
+ describe('exportToCSV', () => {
127
+ it('should export data to CSV format', () => {
128
+ exportToCSV(sampleData, sampleColumns, 'test-export')
129
+
130
+ expect(mockClick).toHaveBeenCalled()
131
+ expect(mockAppendChild).toHaveBeenCalled()
132
+ expect(mockRemoveChild).toHaveBeenCalled()
133
+ })
134
+
135
+ it('should include headers by default', () => {
136
+ const createElementSpy = jest.spyOn(document, 'createElement')
137
+ exportToCSV(sampleData, sampleColumns, 'test-export')
138
+
139
+ const linkElement = createElementSpy.mock.results[0]?.value
140
+ expect(linkElement.download).toBe('test-export.csv')
141
+ })
142
+
143
+ it('should handle empty data', () => {
144
+ expect(() => {
145
+ exportToCSV([], sampleColumns, 'test-export')
146
+ }).toThrow('No data to export')
147
+ })
148
+
149
+ it('should handle empty columns', () => {
150
+ expect(() => {
151
+ exportToCSV(sampleData, [], 'test-export')
152
+ }).toThrow('No columns specified for export')
153
+ })
154
+
155
+ it('should properly escape CSV values with quotes and commas', () => {
156
+ const dataWithSpecialChars: TestData[] = [
157
+ {
158
+ id: '1',
159
+ name: 'John "The Boss" Doe',
160
+ age: 30,
161
+ isActive: true,
162
+ createdAt: new Date('2023-01-15'),
163
+ },
164
+ {
165
+ id: '2',
166
+ name: 'Smith, Jane',
167
+ age: 25,
168
+ isActive: false,
169
+ createdAt: new Date('2023-02-20'),
170
+ },
171
+ ]
172
+
173
+ expect(() => {
174
+ exportToCSV(dataWithSpecialChars, sampleColumns, 'test-special-chars')
175
+ }).not.toThrow()
176
+ })
177
+
178
+ it('should handle nested object accessors', () => {
179
+ expect(() => {
180
+ exportToCSV(sampleData, sampleColumns, 'test-nested')
181
+ }).not.toThrow()
182
+ })
183
+
184
+ it('should handle accessor functions', () => {
185
+ const columnsWithFunctions: ColumnConfig<TestData>[] = [
186
+ {
187
+ id: 'fullInfo',
188
+ header: 'Full Info',
189
+ accessorFn: (row) => `${row.name} (${row.age})`,
190
+ },
191
+ ]
192
+
193
+ expect(() => {
194
+ exportToCSV(sampleData, columnsWithFunctions, 'test-accessor-fn')
195
+ }).not.toThrow()
196
+ })
197
+
198
+ it('should format boolean values as Yes/No', () => {
199
+ const booleanColumns: ColumnConfig<TestData>[] = [
200
+ {
201
+ id: 'isActive',
202
+ header: 'Active',
203
+ accessorKey: 'isActive',
204
+ },
205
+ ]
206
+
207
+ expect(() => {
208
+ exportToCSV(sampleData, booleanColumns, 'test-boolean')
209
+ }).not.toThrow()
210
+ })
211
+
212
+ it('should handle null and undefined values', () => {
213
+ const dataWithNulls: TestData[] = [
214
+ {
215
+ id: '1',
216
+ name: 'John Doe',
217
+ age: 30,
218
+ isActive: true,
219
+ company: undefined,
220
+ createdAt: new Date('2023-01-15'),
221
+ },
222
+ ]
223
+
224
+ expect(() => {
225
+ exportToCSV(dataWithNulls, sampleColumns, 'test-nulls')
226
+ }).not.toThrow()
227
+ })
228
+
229
+ it('should format dates as ISO strings', () => {
230
+ const dateColumns: ColumnConfig<TestData>[] = [
231
+ {
232
+ id: 'createdAt',
233
+ header: 'Created',
234
+ accessorKey: 'createdAt',
235
+ },
236
+ ]
237
+
238
+ expect(() => {
239
+ exportToCSV(sampleData, dateColumns, 'test-dates')
240
+ }).not.toThrow()
241
+ })
242
+ })
243
+
244
+ describe('exportToExcel', () => {
245
+ beforeEach(() => {
246
+ ;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue({})
247
+ ;(XLSX.utils.book_new as jest.Mock).mockReturnValue({})
248
+ ;(XLSX.write as jest.Mock).mockReturnValue(new ArrayBuffer(8))
249
+ })
250
+
251
+ it('should export data to Excel format', () => {
252
+ exportToExcel(sampleData, sampleColumns, 'test-export')
253
+
254
+ expect(XLSX.utils.aoa_to_sheet).toHaveBeenCalled()
255
+ expect(XLSX.utils.book_new).toHaveBeenCalled()
256
+ expect(XLSX.utils.book_append_sheet).toHaveBeenCalled()
257
+ expect(XLSX.write).toHaveBeenCalled()
258
+ })
259
+
260
+ it('should create worksheet with correct name', () => {
261
+ exportToExcel(sampleData, sampleColumns, 'test-export')
262
+
263
+ expect(XLSX.utils.book_append_sheet).toHaveBeenCalledWith(
264
+ expect.anything(),
265
+ expect.anything(),
266
+ 'Data'
267
+ )
268
+ })
269
+
270
+ it('should handle empty data', () => {
271
+ expect(() => {
272
+ exportToExcel([], sampleColumns, 'test-export')
273
+ }).toThrow('No data to export')
274
+ })
275
+
276
+ it('should handle empty columns', () => {
277
+ expect(() => {
278
+ exportToExcel(sampleData, [], 'test-export')
279
+ }).toThrow('No columns specified for export')
280
+ })
281
+
282
+ it('should set column widths', () => {
283
+ const mockSheet = {}
284
+ ;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue(mockSheet)
285
+
286
+ exportToExcel(sampleData, sampleColumns, 'test-widths')
287
+
288
+ expect(mockSheet).toHaveProperty('!cols')
289
+ })
290
+
291
+ it('should trigger file download', () => {
292
+ exportToExcel(sampleData, sampleColumns, 'test-download')
293
+
294
+ expect(mockClick).toHaveBeenCalled()
295
+ expect(mockAppendChild).toHaveBeenCalled()
296
+ expect(mockRemoveChild).toHaveBeenCalled()
297
+ })
298
+ })
299
+
300
+ describe('exportData', () => {
301
+ it('should route to CSV export when format is csv', () => {
302
+ exportData(sampleData, sampleColumns, {
303
+ format: 'csv',
304
+ filename: 'test-export',
305
+ })
306
+
307
+ expect(mockClick).toHaveBeenCalled()
308
+ })
309
+
310
+ it('should route to Excel export when format is excel', () => {
311
+ ;(XLSX.utils.aoa_to_sheet as jest.Mock).mockReturnValue({})
312
+ ;(XLSX.utils.book_new as jest.Mock).mockReturnValue({})
313
+ ;(XLSX.write as jest.Mock).mockReturnValue(new ArrayBuffer(8))
314
+
315
+ exportData(sampleData, sampleColumns, {
316
+ format: 'excel',
317
+ filename: 'test-export',
318
+ })
319
+
320
+ expect(XLSX.utils.aoa_to_sheet).toHaveBeenCalled()
321
+ })
322
+
323
+ it('should throw error for unsupported format', () => {
324
+ expect(() => {
325
+ exportData(sampleData, sampleColumns, {
326
+ format: 'pdf' as any,
327
+ filename: 'test-export',
328
+ })
329
+ }).toThrow('Unsupported export format: pdf')
330
+ })
331
+
332
+ it('should respect includeHeaders option', () => {
333
+ exportData(sampleData, sampleColumns, {
334
+ format: 'csv',
335
+ filename: 'test-export',
336
+ includeHeaders: false,
337
+ })
338
+
339
+ expect(mockClick).toHaveBeenCalled()
340
+ })
341
+ })
342
+
343
+ describe('generateExportFilename', () => {
344
+ it('should generate filename with timestamp', () => {
345
+ const filename = generateExportFilename('export')
346
+
347
+ expect(filename).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
348
+ })
349
+
350
+ it('should handle custom base filename', () => {
351
+ const filename = generateExportFilename('my-custom-export')
352
+
353
+ expect(filename).toMatch(/^my-custom-export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
354
+ })
355
+
356
+ it('should generate unique filenames on consecutive calls', async () => {
357
+ const filename1 = generateExportFilename('export')
358
+ await new Promise((resolve) => setTimeout(resolve, 10))
359
+ const filename2 = generateExportFilename('export')
360
+
361
+ // They might be the same if called within the same second
362
+ // Just ensure they both match the pattern
363
+ expect(filename1).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
364
+ expect(filename2).toMatch(/^export_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/)
365
+ })
366
+ })
367
+
368
+ describe('Edge Cases', () => {
369
+ it('should handle large datasets', () => {
370
+ const largeData = Array.from({ length: 10000 }, (_, i) => ({
371
+ id: `${i}`,
372
+ name: `User ${i}`,
373
+ age: 20 + (i % 50),
374
+ isActive: i % 2 === 0,
375
+ createdAt: new Date('2023-01-01'),
376
+ }))
377
+
378
+ expect(() => {
379
+ exportToCSV(largeData, sampleColumns, 'large-export')
380
+ }).not.toThrow()
381
+ })
382
+
383
+ it('should handle columns with function headers', () => {
384
+ const columnsWithFunctionHeaders: ColumnConfig<TestData>[] = [
385
+ {
386
+ id: 'name',
387
+ header: () => 'Custom Name Header',
388
+ accessorKey: 'name',
389
+ },
390
+ ]
391
+
392
+ expect(() => {
393
+ exportToCSV(sampleData, columnsWithFunctionHeaders, 'test-function-header')
394
+ }).not.toThrow()
395
+ })
396
+
397
+ it('should handle data with array values', () => {
398
+ const dataWithArrays: TestData[] = [
399
+ {
400
+ id: '1',
401
+ name: 'John Doe',
402
+ age: 30,
403
+ isActive: true,
404
+ tags: ['tag1', 'tag2', 'tag3'],
405
+ createdAt: new Date('2023-01-15'),
406
+ },
407
+ ]
408
+
409
+ expect(() => {
410
+ exportToCSV(dataWithArrays, sampleColumns, 'test-arrays')
411
+ }).not.toThrow()
412
+ })
413
+
414
+ it('should handle columns without accessor', () => {
415
+ const columnsWithoutAccessor: ColumnConfig<TestData>[] = [
416
+ {
417
+ id: 'custom',
418
+ header: 'Custom Column',
419
+ },
420
+ ]
421
+
422
+ expect(() => {
423
+ exportToCSV(sampleData, columnsWithoutAccessor, 'test-no-accessor')
424
+ }).not.toThrow()
425
+ })
426
+ })
427
+ })
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { Button } from '../../../ui/button'
4
+ import { X } from 'lucide-react'
5
+ import { BulkAction } from '../../types'
6
+ import { cn } from '../../../../lib/utils'
7
+
8
+ interface BulkActionBarProps {
9
+ selectedCount: number
10
+ selectAllPages: boolean
11
+ totalCount: number
12
+ bulkActions: BulkAction[]
13
+ onClearSelection: () => void
14
+ onExecuteAction: (action: BulkAction) => void
15
+ className?: string
16
+ }
17
+
18
+ export function BulkActionBar({
19
+ selectedCount,
20
+ selectAllPages,
21
+ totalCount,
22
+ bulkActions,
23
+ onClearSelection,
24
+ onExecuteAction,
25
+ className,
26
+ }: BulkActionBarProps) {
27
+ const isVisible = selectedCount > 0
28
+
29
+ const getVariantClassName = (variant: BulkAction['variant']) => {
30
+ switch (variant) {
31
+ case 'gradient-purple':
32
+ return 'bg-gradient-to-r from-purple-600 to-blue-600 text-white hover:from-purple-700 hover:to-blue-700'
33
+ case 'gradient-green':
34
+ return 'bg-gradient-to-r from-green-600 to-teal-600 text-white hover:from-green-700 hover:to-teal-700'
35
+ case 'gradient-indigo':
36
+ return 'bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700'
37
+ case 'gradient-orange':
38
+ return 'bg-gradient-to-r from-orange-600 to-red-600 text-white hover:from-orange-700 hover:to-red-700'
39
+ case 'gradient-blue':
40
+ return 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:from-blue-700 hover:to-indigo-700'
41
+ default:
42
+ return ''
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div
48
+ className={cn(
49
+ 'transition-all duration-200 ease-in-out overflow-hidden',
50
+ isVisible
51
+ ? 'opacity-100 max-h-20'
52
+ : 'opacity-0 max-h-0 pointer-events-none',
53
+ className
54
+ )}
55
+ aria-hidden={!isVisible}
56
+ >
57
+ <div className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg border">
58
+ {/* Selection summary */}
59
+ <div className="flex items-center gap-2 text-sm font-medium">
60
+ {selectAllPages ? (
61
+ <span className="text-blue-600">
62
+ All <span className="font-semibold">{totalCount}</span> items selected
63
+ </span>
64
+ ) : (
65
+ <span>
66
+ {`${selectedCount} ${selectedCount === 1 ? 'item' : 'items'} selected`}
67
+ </span>
68
+ )}
69
+ </div>
70
+
71
+ {/* Divider */}
72
+ <div className="h-6 w-px bg-border mx-1" />
73
+
74
+ {/* Bulk action buttons */}
75
+ <div className="flex items-center gap-2 flex-1">
76
+ {bulkActions.map((action) => {
77
+ const Icon = action.icon
78
+ const isDisabled = action.disabled?.(new Set()) || false
79
+ const exceedsMaxSelection = !!(action.maxSelection && selectedCount > action.maxSelection)
80
+
81
+ return (
82
+ <Button
83
+ key={action.id}
84
+ size="sm"
85
+ variant={action.variant === 'default' ? 'default' : 'default'}
86
+ className={cn(
87
+ getVariantClassName(action.variant),
88
+ 'gap-2'
89
+ )}
90
+ onClick={() => onExecuteAction(action)}
91
+ disabled={isDisabled || exceedsMaxSelection}
92
+ title={
93
+ exceedsMaxSelection
94
+ ? `Maximum ${action.maxSelection} items can be selected for this action`
95
+ : action.label
96
+ }
97
+ >
98
+ <Icon className="h-4 w-4" />
99
+ {action.label}
100
+ {selectAllPages && totalCount > 0 && ` (${totalCount})`}
101
+ </Button>
102
+ )
103
+ })}
104
+ </div>
105
+
106
+ {/* Clear selection button */}
107
+ <Button
108
+ variant="outline"
109
+ size="sm"
110
+ onClick={onClearSelection}
111
+ className="gap-2"
112
+ >
113
+ <X className="h-4 w-4" />
114
+ Clear
115
+ </Button>
116
+ </div>
117
+ </div>
118
+ )
119
+ }