@eggspot/ui 0.0.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 (74) hide show
  1. package/eslint.config.js +4 -0
  2. package/package.json +66 -0
  3. package/postcss.config.mjs +1 -0
  4. package/src/components/Button.machine.tsx +50 -0
  5. package/src/components/Button.tsx +249 -0
  6. package/src/components/Button.variants.tsx +186 -0
  7. package/src/components/ButtonGroup.tsx +56 -0
  8. package/src/components/Calendar.tsx +275 -0
  9. package/src/components/Calendar.utils.tsx +22 -0
  10. package/src/components/Checkbox.tsx +199 -0
  11. package/src/components/ConfirmDialog.tsx +183 -0
  12. package/src/components/DashboardLayout/DashboardLayout.tsx +348 -0
  13. package/src/components/DashboardLayout/SidebarNav.tsx +509 -0
  14. package/src/components/DashboardLayout/index.ts +33 -0
  15. package/src/components/DataTable/DataTable.tsx +557 -0
  16. package/src/components/DataTable/DataTableColumnHeader.tsx +122 -0
  17. package/src/components/DataTable/DataTableDisplaySettings.tsx +265 -0
  18. package/src/components/DataTable/DataTableFloatingBar.tsx +44 -0
  19. package/src/components/DataTable/DataTablePagination.tsx +168 -0
  20. package/src/components/DataTable/DataTableStates.tsx +69 -0
  21. package/src/components/DataTable/DataTableToolbarContainer.tsx +47 -0
  22. package/src/components/DataTable/hooks/use-data-table-settings.ts +101 -0
  23. package/src/components/DataTable/index.ts +7 -0
  24. package/src/components/DataTable/types/data-table.ts +97 -0
  25. package/src/components/DatePicker.tsx +213 -0
  26. package/src/components/DatePicker.utils.tsx +38 -0
  27. package/src/components/Datefield.tsx +109 -0
  28. package/src/components/Datefield.utils.ts +10 -0
  29. package/src/components/Dialog.tsx +167 -0
  30. package/src/components/Field.tsx +49 -0
  31. package/src/components/Filter/Filter.store.tsx +122 -0
  32. package/src/components/Filter/Filter.tsx +11 -0
  33. package/src/components/Filter/Filter.types.ts +107 -0
  34. package/src/components/Filter/FilterBar.tsx +38 -0
  35. package/src/components/Filter/FilterBuilder.tsx +158 -0
  36. package/src/components/Filter/FilterField/DateModeRowValue.tsx +250 -0
  37. package/src/components/Filter/FilterField/FilterAsyncSelect.tsx +191 -0
  38. package/src/components/Filter/FilterField/FilterDateMode.tsx +241 -0
  39. package/src/components/Filter/FilterField/FilterDateRange.tsx +169 -0
  40. package/src/components/Filter/FilterField/FilterSelect.tsx +208 -0
  41. package/src/components/Filter/FilterField/FilterSingleDate.tsx +277 -0
  42. package/src/components/Filter/FilterField/OptionItem.tsx +112 -0
  43. package/src/components/Filter/FilterField/index.ts +6 -0
  44. package/src/components/Filter/FilterRow.tsx +527 -0
  45. package/src/components/Filter/index.ts +17 -0
  46. package/src/components/Form.tsx +195 -0
  47. package/src/components/Heading.tsx +41 -0
  48. package/src/components/Input.tsx +221 -0
  49. package/src/components/InputOTP.tsx +78 -0
  50. package/src/components/Label.tsx +65 -0
  51. package/src/components/Layout.tsx +129 -0
  52. package/src/components/ListBox.tsx +97 -0
  53. package/src/components/Menu.tsx +152 -0
  54. package/src/components/NativeSelect.tsx +77 -0
  55. package/src/components/NumberInput.tsx +114 -0
  56. package/src/components/Popover.tsx +44 -0
  57. package/src/components/Provider.tsx +22 -0
  58. package/src/components/RadioGroup.tsx +191 -0
  59. package/src/components/Resizable.tsx +71 -0
  60. package/src/components/ScrollArea.tsx +57 -0
  61. package/src/components/Select.tsx +626 -0
  62. package/src/components/Select.utils.tsx +64 -0
  63. package/src/components/Separator.tsx +25 -0
  64. package/src/components/Sheet.tsx +147 -0
  65. package/src/components/Sonner.tsx +96 -0
  66. package/src/components/Spinner.tsx +30 -0
  67. package/src/components/Switch.tsx +51 -0
  68. package/src/components/Text.tsx +35 -0
  69. package/src/components/Tooltip.tsx +58 -0
  70. package/src/consts/config.ts +2 -0
  71. package/src/hooks/.gitkeep +0 -0
  72. package/src/lib/utils.ts +10 -0
  73. package/tsconfig.json +11 -0
  74. package/tsconfig.lint.json +8 -0
@@ -0,0 +1,557 @@
1
+ import { memo, RefObject, useEffect, useMemo, useRef } from "react"
2
+ import { Checkbox } from "@eggspot/ui/components/Checkbox"
3
+ import { DataTableProps } from "@eggspot/ui/components/DataTable/types/data-table"
4
+ import { cn } from "@eggspot/ui/lib/utils"
5
+ import {
6
+ Cell,
7
+ ColumnDef,
8
+ ColumnPinningState,
9
+ flexRender,
10
+ getCoreRowModel,
11
+ getFilteredRowModel,
12
+ getPaginationRowModel,
13
+ getSortedRowModel,
14
+ Header,
15
+ Table,
16
+ useReactTable,
17
+ } from "@tanstack/react-table"
18
+ import { useVirtualizer } from "@tanstack/react-virtual"
19
+
20
+ import { FilterValue } from "../Filter"
21
+ import { DataTableDisplaySettings } from "./DataTableDisplaySettings"
22
+ import { DataTableFloatingBar } from "./DataTableFloatingBar"
23
+ import { DataTablePagination } from "./DataTablePagination"
24
+ import {
25
+ DataTableEmpty,
26
+ DataTableError,
27
+ DataTableLoading,
28
+ } from "./DataTableStates"
29
+ import { DataTableToolbarContainer } from "./DataTableToolbarContainer"
30
+ import { useDataTableSettings } from "./hooks/use-data-table-settings"
31
+
32
+ function HeaderCell({
33
+ header,
34
+ isPinned,
35
+ }: {
36
+ header: Header<any, any>
37
+ isPinned?: "left" | "right"
38
+ }) {
39
+ const isResizing = header.column.getIsResizing()
40
+ return (
41
+ <th
42
+ key={header.id}
43
+ className={cn(
44
+ "text-gray-11 bg-gray-4 relative h-11 px-4 text-left align-middle font-semibold",
45
+ isPinned === "left" &&
46
+ "sticky left-0 z-20 shadow-[-6px_0_2px_-6px_var(--gray-6)_inset]",
47
+ isPinned === "right" &&
48
+ "sticky right-0 z-20 shadow-[6px_0_2px_-6px_var(--gray-6)_inset]",
49
+ header.column.getCanResize() && "group"
50
+ )}
51
+ style={{
52
+ width: header.getSize(),
53
+ minWidth: header.column.columnDef.minSize,
54
+ maxWidth: header.column.columnDef.maxSize,
55
+ ...(isPinned === "left" && {
56
+ left: header.column.getStart("left"),
57
+ }),
58
+ ...(isPinned === "right" && {
59
+ right: header.column.getAfter("right"),
60
+ }),
61
+ }}
62
+ >
63
+ {header.isPlaceholder
64
+ ? null
65
+ : flexRender(header.column.columnDef.header, header.getContext())}
66
+ {header.column.getCanResize() && (
67
+ <div
68
+ data-slot="data-table-resize-handle"
69
+ onMouseDown={header.getResizeHandler()}
70
+ onTouchStart={header.getResizeHandler()}
71
+ className={cn(
72
+ "group/resize-handle absolute top-0 right-0 z-10 flex h-full w-2 cursor-col-resize touch-none items-center justify-center select-none"
73
+ )}
74
+ >
75
+ <div
76
+ className={cn(
77
+ "bg-gray-7 group-hover/resize-handle:bg-gray-11 h-6 w-px transition-colors duration-150",
78
+ isResizing && "bg-accent-9!"
79
+ )}
80
+ />
81
+ </div>
82
+ )}
83
+ </th>
84
+ )
85
+ }
86
+
87
+ function TableCell({
88
+ cell,
89
+ isPinned,
90
+ }: {
91
+ cell: Cell<any, any>
92
+ isPinned?: "left" | "right"
93
+ }) {
94
+ return (
95
+ <td
96
+ key={cell.id}
97
+ title={cell.getValue()}
98
+ className={cn(
99
+ "group-hover:bg-gray-3 truncate px-4 py-1 align-middle text-sm",
100
+ "group-data-[state=selected]:bg-gray-4",
101
+ isPinned === "left" &&
102
+ "bg-gray-2 group-data-[state=selected]:bg-gray-5 sticky left-0 z-10 shadow-[-6px_0_2px_-6px_var(--gray-6)_inset]",
103
+ isPinned === "right" &&
104
+ "bg-gray-2 group-data-[state=selected]:bg-gray-5 sticky right-0 z-10 shadow-[6px_0_2px_-6px_var(--gray-6)_inset]"
105
+ )}
106
+ style={{
107
+ width: cell.column.getSize(),
108
+ minWidth: cell.column.columnDef.minSize,
109
+ maxWidth: cell.column.columnDef.maxSize,
110
+ ...(isPinned === "left" && {
111
+ left: cell.column.getStart("left"),
112
+ }),
113
+ ...(isPinned === "right" && {
114
+ right: cell.column.getAfter("right"),
115
+ }),
116
+ }}
117
+ >
118
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
119
+ </td>
120
+ )
121
+ }
122
+
123
+ function TableBody({
124
+ table,
125
+ tableContainerRef,
126
+ estimatedRowHeight,
127
+ }: {
128
+ table: Table<any>
129
+ tableContainerRef: RefObject<HTMLDivElement | null>
130
+ estimatedRowHeight: number
131
+ }) {
132
+ const { rows } = table.getRowModel()
133
+ const rowVirtualizer = useVirtualizer({
134
+ count: rows.length,
135
+ getScrollElement: () => tableContainerRef.current,
136
+ estimateSize: () => estimatedRowHeight,
137
+ overscan: 10,
138
+ })
139
+
140
+ const virtualRows = rowVirtualizer.getVirtualItems()
141
+ const totalSize = rowVirtualizer.getTotalSize()
142
+
143
+ const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start || 0 : 0
144
+ const paddingBottom =
145
+ virtualRows.length > 0
146
+ ? totalSize - (virtualRows[virtualRows.length - 1]?.end || 0)
147
+ : 0
148
+
149
+ const leftPinnedColumns = table.getLeftVisibleLeafColumns()
150
+ const rightPinnedColumns = table.getRightVisibleLeafColumns()
151
+ const centerColumns = table.getCenterVisibleLeafColumns()
152
+
153
+ return (
154
+ <tbody>
155
+ {paddingTop > 0 && (
156
+ <tr>
157
+ <td style={{ height: `${paddingTop}px` }} />
158
+ </tr>
159
+ )}
160
+ {virtualRows.map((virtualRow) => {
161
+ const row = rows[virtualRow.index]
162
+
163
+ if (!row) return null
164
+
165
+ const isSelected = row.getIsSelected()
166
+
167
+ return (
168
+ <tr
169
+ key={row.id}
170
+ data-state={isSelected ? "selected" : undefined}
171
+ className={cn(
172
+ "group border-gray-3 h-11 border-b transition-colors"
173
+ )}
174
+ >
175
+ {leftPinnedColumns.map((column) => {
176
+ const cell = row
177
+ .getVisibleCells()
178
+ .find((c) => c.column.id === column.id)
179
+ return cell ? (
180
+ <TableCell key={cell.id} cell={cell} isPinned="left" />
181
+ ) : null
182
+ })}
183
+ {centerColumns.map((column) => {
184
+ const cell = row
185
+ .getVisibleCells()
186
+ .find((c) => c.column.id === column.id)
187
+ return cell ? <TableCell key={cell.id} cell={cell} /> : null
188
+ })}
189
+ {rightPinnedColumns.map((column) => {
190
+ const cell = row
191
+ .getVisibleCells()
192
+ .find((c) => c.column.id === column.id)
193
+ return cell ? (
194
+ <TableCell key={cell.id} cell={cell} isPinned="right" />
195
+ ) : null
196
+ })}
197
+ </tr>
198
+ )
199
+ })}
200
+ {paddingBottom > 0 && (
201
+ <tr>
202
+ <td style={{ height: `${paddingBottom}px` }} />
203
+ </tr>
204
+ )}
205
+ </tbody>
206
+ )
207
+ }
208
+
209
+ const MemoizedTableBody = memo(
210
+ TableBody,
211
+ (prev, next) => prev.table.options.data === next.table.options.data
212
+ ) as typeof TableBody
213
+
214
+ export function DataTable<TData extends { id: string | number }>({
215
+ // Core data
216
+ columns,
217
+ data,
218
+ dataSource = "server",
219
+
220
+ // Loading/error states
221
+ isLoading = false,
222
+ isFetching,
223
+ isError = false,
224
+ errorMessage,
225
+
226
+ // Components
227
+ filterBar,
228
+ actionBar,
229
+
230
+ // State for server-side operations
231
+ initialState,
232
+ onStateChange,
233
+ rowCount,
234
+
235
+ // Selection
236
+ enableRowSelection = true,
237
+
238
+ // Display settings
239
+ storageKey = "default-table",
240
+ defaultColumnVisibility = {},
241
+ defaultColumnSizing = {},
242
+ defaultColumnPinning = { left: [], right: [] },
243
+ defaultColumnOrder,
244
+
245
+ // Virtualization
246
+ estimatedRowHeight = 44,
247
+
248
+ // Custom empty state
249
+ emptyStateTitle,
250
+ emptyStateDescription,
251
+ }: DataTableProps<TData>) {
252
+ const rendered = useRef(false)
253
+ const tableContainerRef = useRef<HTMLDivElement>(null)
254
+
255
+ // Calculate default column order from columns if not provided
256
+ const calculatedDefaultOrder = useMemo(() => {
257
+ if (defaultColumnOrder) return defaultColumnOrder
258
+ return [
259
+ "select",
260
+ ...(columns
261
+ .map(
262
+ (col) =>
263
+ (col as { accessorKey?: string }).accessorKey ||
264
+ (col as { id?: string }).id
265
+ )
266
+ .filter(Boolean) as string[]),
267
+ ]
268
+ }, [columns, defaultColumnOrder])
269
+
270
+ const {
271
+ settings,
272
+ updateColumnVisibility,
273
+ updateColumnSizing,
274
+ updateColumnPinning,
275
+ updateColumnOrder,
276
+ resetToDefaults,
277
+ } = useDataTableSettings({
278
+ storageKey,
279
+ defaultColumnVisibility,
280
+ defaultColumnSizing,
281
+ defaultColumnPinning,
282
+ defaultColumnOrder: calculatedDefaultOrder,
283
+ })
284
+
285
+ // Selection column
286
+ const selectionColumn: ColumnDef<TData> = {
287
+ id: "select",
288
+ size: 48,
289
+ minSize: 48,
290
+ maxSize: 48,
291
+ enableResizing: false,
292
+ enableSorting: false,
293
+ enableHiding: false,
294
+ header: ({ table }) => {
295
+ return (
296
+ <div className="flex items-center justify-center">
297
+ <Checkbox
298
+ isSelected={table.getIsAllPageRowsSelected()}
299
+ isIndeterminate={table.getIsSomePageRowsSelected()}
300
+ onChange={(isSelected) => {
301
+ table.toggleAllPageRowsSelected(isSelected)
302
+ }}
303
+ aria-label="Select all"
304
+ className="translate-y-[2px]"
305
+ />
306
+ </div>
307
+ )
308
+ },
309
+ cell: ({ row }) => (
310
+ <div className="flex items-center justify-center">
311
+ <Checkbox
312
+ isSelected={row.getIsSelected()}
313
+ isDisabled={!row.getCanSelect()}
314
+ isIndeterminate={row.getIsSomeSelected()}
315
+ onChange={row.getToggleSelectedHandler()}
316
+ aria-label="Select row"
317
+ className="translate-y-[2px]"
318
+ />
319
+ </div>
320
+ ),
321
+ }
322
+
323
+ const allColumns = useMemo(() => {
324
+ if (enableRowSelection) {
325
+ return [selectionColumn, ...columns]
326
+ }
327
+ return columns
328
+ }, [columns, enableRowSelection])
329
+
330
+ // Always pin selection column to the left
331
+ const columnPinning = useMemo((): ColumnPinningState => {
332
+ const left = settings.columnPinning.left || []
333
+ const right = settings.columnPinning.right || []
334
+
335
+ // Ensure "select" is always first in left pinned columns when row selection is enabled
336
+ if (enableRowSelection && !left.includes("select")) {
337
+ return { left: ["select", ...left], right }
338
+ }
339
+ return { left, right }
340
+ }, [settings.columnPinning, enableRowSelection])
341
+
342
+ const table = useReactTable({
343
+ data,
344
+ columns: allColumns,
345
+ initialState: {
346
+ pagination: {
347
+ pageSize: 20,
348
+ },
349
+ ...initialState,
350
+ },
351
+ rowCount: rowCount,
352
+ state: {
353
+ columnVisibility: settings.columnVisibility,
354
+ columnSizing: settings.columnSizing,
355
+ columnPinning,
356
+ columnOrder:
357
+ settings.columnOrder.length > 0
358
+ ? settings.columnOrder
359
+ : calculatedDefaultOrder,
360
+ },
361
+ enableColumnResizing: true,
362
+ columnResizeMode: "onChange",
363
+ getCoreRowModel: getCoreRowModel(),
364
+ getSortedRowModel: getSortedRowModel(),
365
+ getPaginationRowModel: getPaginationRowModel(),
366
+ getFilteredRowModel: getFilteredRowModel(),
367
+ globalFilterFn: "auto",
368
+ onColumnVisibilityChange: (updater) => {
369
+ const newVisibility =
370
+ typeof updater === "function"
371
+ ? updater(settings.columnVisibility)
372
+ : updater
373
+ updateColumnVisibility(newVisibility)
374
+ },
375
+ onColumnSizingChange: (updater) => {
376
+ const newSizing =
377
+ typeof updater === "function" ? updater(settings.columnSizing) : updater
378
+ updateColumnSizing(newSizing)
379
+ },
380
+ onColumnPinningChange: (updater) => {
381
+ const newPinning =
382
+ typeof updater === "function"
383
+ ? updater(settings.columnPinning)
384
+ : updater
385
+ updateColumnPinning(newPinning)
386
+ },
387
+ onColumnOrderChange: (updater) => {
388
+ const newOrder =
389
+ typeof updater === "function" ? updater(settings.columnOrder) : updater
390
+ updateColumnOrder(newOrder)
391
+ },
392
+ getRowId: (row) => String(row.id),
393
+ manualPagination: dataSource === "server",
394
+ manualFiltering: dataSource === "server",
395
+ manualSorting: dataSource === "server",
396
+ })
397
+
398
+ const { rows } = table.getRowModel()
399
+
400
+ // Get pinned and center columns
401
+ const leftPinnedColumns = table.getLeftVisibleLeafColumns()
402
+ const rightPinnedColumns = table.getRightVisibleLeafColumns()
403
+ const centerColumns = table.getCenterVisibleLeafColumns()
404
+
405
+ const state = table.getState()
406
+
407
+ // column filters props (client side)
408
+ const columnFilters: FilterValue = useMemo(
409
+ () =>
410
+ state.columnFilters.reduce((acc, filter) => {
411
+ try {
412
+ acc[filter.id] = filter.value as any
413
+ return acc
414
+ } catch (error) {
415
+ return acc
416
+ }
417
+ }, {} as FilterValue),
418
+ [state.columnFilters]
419
+ )
420
+ const setColumnFilters = (filters: FilterValue) => {
421
+ table.setColumnFilters(
422
+ Object.entries(filters).map(([id, value]) => ({ id, value }))
423
+ )
424
+ }
425
+
426
+ // Notify parent of filters change
427
+ useEffect(() => {
428
+ onStateChange?.(state)
429
+
430
+ setTimeout(() => {
431
+ rendered.current = true
432
+ }, 200)
433
+
434
+ if (rendered.current) {
435
+ table.setPageIndex(0)
436
+ }
437
+ }, [
438
+ state.pagination.pageSize,
439
+ state.sorting,
440
+ state.globalFilter,
441
+ state.columnFilters,
442
+ ])
443
+
444
+ // Notify parent of page index change
445
+ useEffect(() => {
446
+ onStateChange?.(state)
447
+ }, [state.pagination.pageIndex])
448
+
449
+ const clearSelection = () => {
450
+ table.resetRowSelection()
451
+ }
452
+
453
+ return (
454
+ <div className="flex h-full flex-col overflow-hidden">
455
+ {/* Toolbar */}
456
+ <DataTableToolbarContainer table={table}>
457
+ <div>
458
+ {filterBar?.({ value: columnFilters, onChange: setColumnFilters })}
459
+ </div>
460
+ <DataTableDisplaySettings
461
+ table={table}
462
+ onResetToDefaults={resetToDefaults}
463
+ />
464
+ </DataTableToolbarContainer>
465
+
466
+ {/* Table Container */}
467
+ <div ref={tableContainerRef} className="relative flex-1 overflow-auto">
468
+ <table
469
+ data-fetching={isFetching ? "true" : undefined}
470
+ className="w-full table-fixed border-collapse data-[fetching=true]:opacity-60"
471
+ >
472
+ <thead className="sticky top-0 z-30">
473
+ {table.getHeaderGroups().map((headerGroup) => (
474
+ <tr key={headerGroup.id}>
475
+ {leftPinnedColumns.map((column) => {
476
+ const header = headerGroup.headers.find(
477
+ (h) => h.column.id === column.id
478
+ )
479
+ return header ? (
480
+ <HeaderCell
481
+ key={header.id}
482
+ header={header}
483
+ isPinned="left"
484
+ />
485
+ ) : null
486
+ })}
487
+ {centerColumns.map((column) => {
488
+ const header = headerGroup.headers.find(
489
+ (h) => h.column.id === column.id
490
+ )
491
+ return header ? (
492
+ <HeaderCell key={header.id} header={header} />
493
+ ) : null
494
+ })}
495
+ {rightPinnedColumns.map((column) => {
496
+ const header = headerGroup.headers.find(
497
+ (h) => h.column.id === column.id
498
+ )
499
+ return header ? (
500
+ <HeaderCell
501
+ key={header.id}
502
+ header={header}
503
+ isPinned="right"
504
+ />
505
+ ) : null
506
+ })}
507
+ </tr>
508
+ ))}
509
+ </thead>
510
+ {isLoading ? (
511
+ <DataTableLoading />
512
+ ) : isError ? (
513
+ <DataTableError message={errorMessage} />
514
+ ) : rows.length === 0 ? (
515
+ <DataTableEmpty
516
+ title={emptyStateTitle}
517
+ description={emptyStateDescription}
518
+ />
519
+ ) : (
520
+ <>
521
+ {table.getState().columnSizingInfo.isResizingColumn ? (
522
+ <MemoizedTableBody
523
+ table={table}
524
+ tableContainerRef={tableContainerRef}
525
+ estimatedRowHeight={estimatedRowHeight}
526
+ />
527
+ ) : (
528
+ <TableBody
529
+ table={table}
530
+ tableContainerRef={tableContainerRef}
531
+ estimatedRowHeight={estimatedRowHeight}
532
+ />
533
+ )}
534
+ </>
535
+ )}
536
+ </table>
537
+ </div>
538
+
539
+ <DataTablePagination
540
+ pageIndex={table.getState().pagination.pageIndex}
541
+ pageSize={table.getState().pagination.pageSize}
542
+ totalRows={table.getRowCount()}
543
+ onPageChange={table.setPageIndex}
544
+ onPageSizeChange={table.setPageSize}
545
+ />
546
+
547
+ {actionBar && enableRowSelection && (
548
+ <DataTableFloatingBar
549
+ selectedCount={Object.keys(table.getState().rowSelection).length}
550
+ onClearSelection={clearSelection}
551
+ >
552
+ {actionBar?.({ table, clearSelection })}
553
+ </DataTableFloatingBar>
554
+ )}
555
+ </div>
556
+ )
557
+ }
@@ -0,0 +1,122 @@
1
+ import { Button } from "@eggspot/ui/components/Button"
2
+ import { cn } from "@eggspot/ui/lib/utils"
3
+ import { Column } from "@tanstack/react-table"
4
+ import {
5
+ ArrowDown,
6
+ ArrowUp,
7
+ ArrowUpDown,
8
+ EyeOff,
9
+ Pin,
10
+ PinOff,
11
+ } from "lucide-react"
12
+
13
+ import {
14
+ Menu,
15
+ MenuItem,
16
+ MenuPopover,
17
+ MenuSeparator,
18
+ MenuTrigger,
19
+ } from "../Menu"
20
+
21
+ interface DataTableColumnHeaderProps<TData, TValue>
22
+ extends React.HTMLAttributes<HTMLDivElement> {
23
+ column: Column<TData, TValue>
24
+ title: string
25
+ enableSorting?: boolean
26
+ enablePinning?: boolean
27
+ enableHiding?: boolean
28
+ }
29
+
30
+ export function DataTableColumnHeader<TData, TValue>({
31
+ column,
32
+ title,
33
+ className,
34
+ enableSorting = true,
35
+ enablePinning = true,
36
+ enableHiding = true,
37
+ }: DataTableColumnHeaderProps<TData, TValue>) {
38
+ const isSorted = column.getIsSorted()
39
+ const isPinned = column.getIsPinned()
40
+
41
+ if (!enableSorting && !enablePinning && !enableHiding) {
42
+ return <div className={cn("flex items-center", className)}>{title}</div>
43
+ }
44
+
45
+ return (
46
+ <div className={cn("flex items-center gap-1", className)}>
47
+ <MenuTrigger>
48
+ <Button
49
+ variant="ghost"
50
+ size="sm"
51
+ className={cn(
52
+ "text-gray-11 hover:bg-gray-5! hover:text-gray-12 aria-expanded:bg-gray-5! aria-expanded:text-gray-12 -ml-2 h-8 transition-colors",
53
+ isSorted && "text-gray-12 font-semibold"
54
+ )}
55
+ rightIcon={
56
+ isSorted === "desc" ? (
57
+ <ArrowDown />
58
+ ) : isSorted === "asc" ? (
59
+ <ArrowUp />
60
+ ) : (
61
+ <ArrowUpDown />
62
+ )
63
+ }
64
+ >
65
+ {title}
66
+ </Button>
67
+ <MenuPopover>
68
+ <Menu>
69
+ {enableSorting && (
70
+ <>
71
+ <MenuItem onClick={() => column.toggleSorting(false)}>
72
+ <ArrowUp className="text-gray-11 mr-2 h-3.5 w-3.5" />
73
+ Sort ascending
74
+ </MenuItem>
75
+ <MenuItem onClick={() => column.toggleSorting(true)}>
76
+ <ArrowDown className="text-gray-11 mr-2 h-3.5 w-3.5" />
77
+ Sort descending
78
+ </MenuItem>
79
+ {isSorted && (
80
+ <MenuItem onClick={() => column.clearSorting()}>
81
+ <ArrowUpDown className="text-gray-11 mr-2 h-3.5 w-3.5" />
82
+ Clear sorting
83
+ </MenuItem>
84
+ )}
85
+ {(enablePinning || enableHiding) && <MenuSeparator />}
86
+ </>
87
+ )}
88
+ {enablePinning && (
89
+ <>
90
+ {isPinned !== "left" && (
91
+ <MenuItem onClick={() => column.pin("left")}>
92
+ <Pin className="text-gray-11 mr-2 h-3.5 w-3.5 rotate-45" />
93
+ Pin to left
94
+ </MenuItem>
95
+ )}
96
+ {isPinned !== "right" && (
97
+ <MenuItem onClick={() => column.pin("right")}>
98
+ <Pin className="text-gray-11 mr-2 h-3.5 w-3.5 -rotate-45" />
99
+ Pin to right
100
+ </MenuItem>
101
+ )}
102
+ {isPinned && (
103
+ <MenuItem onClick={() => column.pin(false)}>
104
+ <PinOff className="text-gray-11 mr-2 h-3.5 w-3.5" />
105
+ Unpin column
106
+ </MenuItem>
107
+ )}
108
+ {enableHiding && <MenuSeparator />}
109
+ </>
110
+ )}
111
+ {enableHiding && (
112
+ <MenuItem onClick={() => column.toggleVisibility(false)}>
113
+ <EyeOff className="text-gray-11 mr-2 h-3.5 w-3.5" />
114
+ Hide column
115
+ </MenuItem>
116
+ )}
117
+ </Menu>
118
+ </MenuPopover>
119
+ </MenuTrigger>
120
+ </div>
121
+ )
122
+ }