@lovett/ui 0.0.1

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 (102) hide show
  1. package/README.md +28 -0
  2. package/package.json +53 -0
  3. package/src/accordion.tsx +153 -0
  4. package/src/atoms.tsx +197 -0
  5. package/src/badge.tsx +91 -0
  6. package/src/brand-logo-tile.tsx +200 -0
  7. package/src/bulk-action-bar.tsx +104 -0
  8. package/src/button-group.tsx +46 -0
  9. package/src/button.tsx +93 -0
  10. package/src/calculator-shell-v2.tsx +380 -0
  11. package/src/calculator-shell.tsx +310 -0
  12. package/src/card.tsx +369 -0
  13. package/src/checkbox.tsx +114 -0
  14. package/src/chip-nav.tsx +347 -0
  15. package/src/choropleth.tsx +62 -0
  16. package/src/code-block.tsx +235 -0
  17. package/src/completion-ring.tsx +119 -0
  18. package/src/copy-field.tsx +169 -0
  19. package/src/data-grid/column-options-menu.tsx +127 -0
  20. package/src/data-grid/data-grid.tsx +391 -0
  21. package/src/data-grid/drawer-panel.tsx +146 -0
  22. package/src/data-grid/index.ts +29 -0
  23. package/src/data-grid/sortable-parts.tsx +215 -0
  24. package/src/data-grid/table-body.tsx +238 -0
  25. package/src/data-grid/table-elements.tsx +135 -0
  26. package/src/data-grid/table-header.tsx +95 -0
  27. package/src/data-grid/table-overlays.tsx +78 -0
  28. package/src/data-grid/table-parts.tsx +15 -0
  29. package/src/data-grid/table-summary-footer.tsx +101 -0
  30. package/src/data-grid/table-view.tsx +176 -0
  31. package/src/data-grid/types.ts +141 -0
  32. package/src/data-grid/use-grid-columns.ts +311 -0
  33. package/src/data-grid/use-grid-editing.ts +154 -0
  34. package/src/device-frame.tsx +274 -0
  35. package/src/dropdown-button.tsx +221 -0
  36. package/src/dropdown-menu.tsx +450 -0
  37. package/src/empty-state.tsx +85 -0
  38. package/src/file-upload/file-drop-zone.tsx +156 -0
  39. package/src/file-upload/file-preview-grid.tsx +39 -0
  40. package/src/file-upload/file-preview-item.tsx +175 -0
  41. package/src/file-upload/file-thumbnail.tsx +98 -0
  42. package/src/file-upload/file-upload-button.tsx +95 -0
  43. package/src/file-upload/index.ts +13 -0
  44. package/src/file-upload/use-file-upload.ts +371 -0
  45. package/src/filter-bar.tsx +125 -0
  46. package/src/filter-dropdown.tsx +289 -0
  47. package/src/floating-drawer.tsx +420 -0
  48. package/src/folder-card.tsx +188 -0
  49. package/src/folder-tree-picker.tsx +511 -0
  50. package/src/format.ts +117 -0
  51. package/src/frame-stack.tsx +73 -0
  52. package/src/hero-form-card.tsx +471 -0
  53. package/src/index.ts +372 -0
  54. package/src/input.tsx +130 -0
  55. package/src/kbd.tsx +41 -0
  56. package/src/lib/layout.ts +22 -0
  57. package/src/lib/utils.ts +6 -0
  58. package/src/markdown.tsx +214 -0
  59. package/src/menu-button.tsx +142 -0
  60. package/src/meta-previews/CreativeMedia.tsx +119 -0
  61. package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
  62. package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
  63. package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
  64. package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
  65. package/src/meta-previews/MetaFeedPreview.tsx +294 -0
  66. package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
  67. package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
  68. package/src/meta-previews/MetaReelPreview.tsx +750 -0
  69. package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
  70. package/src/meta-previews/MetaSearchPreview.tsx +113 -0
  71. package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
  72. package/src/meta-previews/ProfileAvatar.tsx +79 -0
  73. package/src/meta-previews/defaults.ts +27 -0
  74. package/src/meta-previews/index.ts +37 -0
  75. package/src/meta-previews/types.ts +219 -0
  76. package/src/metric-card.tsx +117 -0
  77. package/src/modal.tsx +131 -0
  78. package/src/option-tile.tsx +252 -0
  79. package/src/page-hero.tsx +78 -0
  80. package/src/page-shell.tsx +440 -0
  81. package/src/pagination.tsx +86 -0
  82. package/src/pill-button.tsx +103 -0
  83. package/src/profile-section.tsx +389 -0
  84. package/src/progress-bar.tsx +84 -0
  85. package/src/radio.tsx +172 -0
  86. package/src/range-slider.tsx +161 -0
  87. package/src/resizable.tsx +282 -0
  88. package/src/search-bar.tsx +81 -0
  89. package/src/segmented-pill.tsx +143 -0
  90. package/src/shell.tsx +464 -0
  91. package/src/slider.tsx +107 -0
  92. package/src/sortable-table.tsx +198 -0
  93. package/src/sortable.tsx +137 -0
  94. package/src/stat-row.tsx +132 -0
  95. package/src/stats-grid.tsx +38 -0
  96. package/src/step-loader.tsx +141 -0
  97. package/src/styles.css +562 -0
  98. package/src/tabs.tsx +143 -0
  99. package/src/tag-chip-input.tsx +394 -0
  100. package/src/textarea.tsx +39 -0
  101. package/src/toast.tsx +59 -0
  102. package/src/tokens.css +579 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * SortableHeaderCell + SortableColumnOptionItem — dnd-kit-driven
3
+ * column reorder UI.
4
+ *
5
+ * Ported 2026-05-18 from gray-ui-csm
6
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/sortable-parts.tsx
7
+ * License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
8
+ * Workspace ADR: ADR-025
9
+ *
10
+ * BRING_WITH_REFACTOR:
11
+ * • Token reskin to workspace vocabulary per ADR-025 D7
12
+ * • @tabler/icons-react → lucide-react per CLAUDE.md §6 (DEV-4)
13
+ * • Imports DropdownMenuCheckboxItem from the workspace
14
+ * dropdown-menu (BUILD_FRESH per DEV-3) and TableHead from the
15
+ * local table-elements (inlined per DEV-1)
16
+ */
17
+
18
+ import type {
19
+ KeyboardEvent as ReactKeyboardEvent,
20
+ PointerEvent as ReactPointerEvent,
21
+ } from 'react'
22
+
23
+ import { useSortable } from '@dnd-kit/sortable'
24
+ import { CSS } from '@dnd-kit/utilities'
25
+ import { ChevronDown, ChevronsUpDown, ChevronUp, GripVertical } from 'lucide-react'
26
+
27
+ import { cn } from '../lib/utils'
28
+ import { DropdownMenuCheckboxItem } from '../dropdown-menu'
29
+ import { TableHead } from './table-elements'
30
+
31
+ import type { DataGridColumn } from './types'
32
+
33
+ type SortableHeaderCellProps<ColumnId extends string> = {
34
+ column: DataGridColumn<ColumnId>
35
+ width: number
36
+ onResize: (
37
+ event: ReactPointerEvent<HTMLButtonElement>,
38
+ columnId: ColumnId,
39
+ ) => void
40
+ /** ADR-027 — current sort direction for THIS column, or null when this
41
+ * column is not the active sort. */
42
+ sortDir: 'asc' | 'desc' | null
43
+ /** ADR-027 — header click handler. When omitted the header is non-
44
+ * interactive for sort (column reorder + resize still work). */
45
+ onSortChange: ((columnId: ColumnId) => void) | undefined
46
+ }
47
+
48
+ type SortableColumnOptionItemProps<ColumnId extends string> = {
49
+ column: DataGridColumn<ColumnId>
50
+ checked: boolean
51
+ disabled: boolean
52
+ onCheckedChange: (checked: boolean) => void
53
+ }
54
+
55
+ function ColumnHead<ColumnId extends string>({
56
+ icon: Icon,
57
+ label,
58
+ }: Pick<DataGridColumn<ColumnId>, 'icon' | 'label'>) {
59
+ // `leading-tight` (1.25) keeps the header compact but gives descenders
60
+ // ('y','g','p','q') enough vertical room so they don't get clipped by
61
+ // the parent th's overflow-hidden. The previous `leading-none` (= font
62
+ // size) clipped the bottom of descenders.
63
+ return (
64
+ <span className="flex min-w-0 items-center gap-1.5 leading-tight">
65
+ <Icon className="h-3.5 w-3.5 shrink-0" />
66
+ <span className="truncate">{label}</span>
67
+ </span>
68
+ )
69
+ }
70
+
71
+ export function SortableColumnOptionItem<ColumnId extends string>({
72
+ column,
73
+ checked,
74
+ disabled,
75
+ onCheckedChange,
76
+ }: SortableColumnOptionItemProps<ColumnId>) {
77
+ const {
78
+ attributes,
79
+ listeners,
80
+ setNodeRef,
81
+ transform,
82
+ transition,
83
+ isDragging,
84
+ } = useSortable({
85
+ id: column.id,
86
+ disabled,
87
+ })
88
+
89
+ return (
90
+ <div
91
+ ref={setNodeRef}
92
+ style={{
93
+ transform: CSS.Translate.toString(transform),
94
+ transition,
95
+ }}
96
+ >
97
+ <DropdownMenuCheckboxItem
98
+ checked={checked}
99
+ disabled={disabled}
100
+ onCheckedChange={(next) => onCheckedChange(next === true)}
101
+ className={cn(
102
+ 'touch-none pl-1.5',
103
+ isDragging && 'opacity-70',
104
+ )}
105
+ style={isDragging ? { background: 'rgb(var(--surface-overlay))' } : undefined}
106
+ >
107
+ <span
108
+ {...attributes}
109
+ {...listeners}
110
+ role="button"
111
+ aria-label={`Reorder ${column.label}`}
112
+ onClick={(event) => {
113
+ event.preventDefault()
114
+ event.stopPropagation()
115
+ }}
116
+ className="inline-flex h-5 w-5 shrink-0 cursor-grab items-center justify-center rounded-[4px] transition-colors active:cursor-grabbing"
117
+ style={{ color: 'rgb(var(--text-muted))' }}
118
+ >
119
+ <GripVertical className="h-3.5 w-3.5" />
120
+ </span>
121
+ <span className="truncate">{column.label}</span>
122
+ </DropdownMenuCheckboxItem>
123
+ </div>
124
+ )
125
+ }
126
+
127
+ export function SortableHeaderCell<ColumnId extends string>({
128
+ column,
129
+ width,
130
+ onResize,
131
+ sortDir,
132
+ onSortChange,
133
+ }: SortableHeaderCellProps<ColumnId>) {
134
+ // ADR-027 — column sortability: explicit `sortable: false` opts out;
135
+ // missing `onSortChange` also disables (no parent handler).
136
+ const isSortable =
137
+ column.sortable !== false && typeof onSortChange === 'function'
138
+ const ariaSort: 'ascending' | 'descending' | 'none' = !isSortable
139
+ ? 'none'
140
+ : sortDir === 'asc'
141
+ ? 'ascending'
142
+ : sortDir === 'desc'
143
+ ? 'descending'
144
+ : 'none'
145
+
146
+ function handleSortClick() {
147
+ if (!isSortable) return
148
+ onSortChange!(column.id)
149
+ }
150
+
151
+ function handleSortKey(event: ReactKeyboardEvent<HTMLDivElement>) {
152
+ if (!isSortable) return
153
+ if (event.key === 'Enter' || event.key === ' ') {
154
+ event.preventDefault()
155
+ onSortChange!(column.id)
156
+ }
157
+ }
158
+
159
+ return (
160
+ <TableHead
161
+ aria-sort={ariaSort}
162
+ tabIndex={isSortable ? 0 : undefined}
163
+ onClick={isSortable ? handleSortClick : undefined}
164
+ onKeyDown={isSortable ? handleSortKey : undefined}
165
+ className={cn(
166
+ 'group/column-head relative h-10 overflow-hidden px-2 text-sm',
167
+ isSortable && 'cursor-pointer select-none',
168
+ )}
169
+ style={{
170
+ width,
171
+ minWidth: width,
172
+ transition: 'none',
173
+ borderRight: '1px solid rgb(var(--border))',
174
+ background: 'rgb(var(--surface-overlay-soft))',
175
+ color: 'rgb(var(--foreground))',
176
+ }}
177
+ >
178
+ <div className="flex h-full min-w-0 items-center justify-between gap-1 overflow-hidden">
179
+ <ColumnHead icon={column.icon} label={column.label} />
180
+ {isSortable &&
181
+ (sortDir === 'asc' ? (
182
+ <ChevronUp
183
+ className="h-3.5 w-3.5 shrink-0"
184
+ style={{ color: 'rgb(var(--foreground))' }}
185
+ aria-hidden
186
+ />
187
+ ) : sortDir === 'desc' ? (
188
+ <ChevronDown
189
+ className="h-3.5 w-3.5 shrink-0"
190
+ style={{ color: 'rgb(var(--foreground))' }}
191
+ aria-hidden
192
+ />
193
+ ) : (
194
+ <ChevronsUpDown
195
+ className="h-3.5 w-3.5 shrink-0 opacity-40"
196
+ style={{ color: 'rgb(var(--text-muted))' }}
197
+ aria-hidden
198
+ />
199
+ ))}
200
+ </div>
201
+ <button
202
+ type="button"
203
+ aria-label={`Resize ${column.label} column`}
204
+ className="absolute top-0 right-0 flex h-full w-3 cursor-col-resize items-center justify-center transition-colors"
205
+ style={{ color: 'rgb(var(--text-muted))' }}
206
+ onPointerDown={(event) => {
207
+ // Don't bubble the resize-pointer-down up to the th's onClick
208
+ // sort handler — resize is its own interaction.
209
+ event.stopPropagation()
210
+ onResize(event, column.id)
211
+ }}
212
+ />
213
+ </TableHead>
214
+ )
215
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * DataGridTableBody — row + cell render with editing state, focus,
3
+ * keyboard navigation, and the open-drawer affordance.
4
+ *
5
+ * Ported 2026-05-18 from gray-ui-csm
6
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/table-body.tsx
7
+ * License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
8
+ * Workspace ADR: ADR-025
9
+ *
10
+ * BRING_WITH_REFACTOR:
11
+ * • Token reskin per ADR-025 D7
12
+ * • @tabler/icons-react → lucide-react (DEV-4)
13
+ * • Checkbox + Button from workspace primitives (DEV-2 + existing
14
+ * @lovett/ui Button)
15
+ * • Source's `size="xs"` Button maps to workspace `size="sm"`
16
+ * (workspace Button only ships sm/md/lg)
17
+ * • Inline edit input uses workspace token vocabulary directly
18
+ * rather than the Input primitive — the input is intentionally
19
+ * compact (h-7) and edge-to-edge inside the cell, not a typical
20
+ * form input.
21
+ */
22
+
23
+ import type {
24
+ KeyboardEvent as ReactKeyboardEvent,
25
+ ReactNode,
26
+ RefObject,
27
+ } from 'react'
28
+
29
+ import { ExternalLink } from 'lucide-react'
30
+
31
+ import { cn } from '../lib/utils'
32
+ import { Button } from '../button'
33
+ import { Checkbox } from '../checkbox'
34
+ import { TableBody, TableCell, TableRow } from './table-elements'
35
+
36
+ import type {
37
+ DataGridColumn,
38
+ DataGridRowBase,
39
+ EditingCell,
40
+ } from './types'
41
+
42
+ type DataGridTableBodyProps<
43
+ Row extends DataGridRowBase,
44
+ ColumnId extends string,
45
+ > = {
46
+ visibleRows: Row[]
47
+ getRowLabel: (row: Row) => string
48
+ visibleColumns: DataGridColumn<ColumnId>[]
49
+ editingCell: EditingCell<ColumnId> | null
50
+ isEditableColumn: (columnId: ColumnId) => boolean
51
+ startEditing: (row: Row, columnId: ColumnId) => void
52
+ onCellKeyDown: (
53
+ event: ReactKeyboardEvent<HTMLTableCellElement>,
54
+ row: Row,
55
+ rowIndex: number,
56
+ column: DataGridColumn<ColumnId>,
57
+ colIndex: number,
58
+ ) => void
59
+ inputRef: RefObject<HTMLInputElement | null>
60
+ draftValue: string
61
+ setDraftValue: (value: string) => void
62
+ commitEdit: () => void
63
+ cancelEdit: () => void
64
+ canOpenDrawer: (columnId: ColumnId) => boolean
65
+ renderCell: (row: Row, column: DataGridColumn<ColumnId>) => ReactNode
66
+ onOpenDrawer: (cell: EditingCell<ColumnId>) => void
67
+ selectedRowIds: string[]
68
+ onToggleRowSelection: (rowId: string, checked: boolean) => void
69
+ columnWidths: Record<ColumnId, number>
70
+ draggingColumnId: ColumnId | null
71
+ }
72
+
73
+ export function DataGridTableBody<
74
+ Row extends DataGridRowBase,
75
+ ColumnId extends string,
76
+ >({
77
+ visibleRows,
78
+ getRowLabel,
79
+ visibleColumns,
80
+ editingCell,
81
+ isEditableColumn,
82
+ startEditing,
83
+ onCellKeyDown,
84
+ inputRef,
85
+ draftValue,
86
+ setDraftValue,
87
+ commitEdit,
88
+ cancelEdit,
89
+ canOpenDrawer,
90
+ renderCell,
91
+ onOpenDrawer,
92
+ selectedRowIds,
93
+ onToggleRowSelection,
94
+ columnWidths,
95
+ draggingColumnId,
96
+ }: DataGridTableBodyProps<Row, ColumnId>) {
97
+ return (
98
+ <TableBody>
99
+ {visibleRows.map((row, rowIndex) => (
100
+ <TableRow key={row.id}>
101
+ <TableCell
102
+ className="h-10 px-0 text-center"
103
+ style={{ borderRight: '1px solid rgb(var(--border))' }}
104
+ >
105
+ <Checkbox
106
+ aria-label={`Select ${getRowLabel(row)}`}
107
+ checked={selectedRowIds.includes(row.id)}
108
+ onCheckedChange={(checked) =>
109
+ onToggleRowSelection(row.id, checked === true)
110
+ }
111
+ className="mx-auto"
112
+ />
113
+ </TableCell>
114
+ {visibleColumns.map((column, colIndex) => {
115
+ const isEditing =
116
+ editingCell?.rowId === row.id &&
117
+ editingCell.columnId === column.id
118
+ const editable = isEditableColumn(column.id)
119
+ const showDrawerAction = canOpenDrawer(column.id)
120
+ const isDraggingThisColumn = draggingColumnId === column.id
121
+
122
+ return (
123
+ <TableCell
124
+ key={`${row.id}-${column.id}`}
125
+ data-grid-cell="true"
126
+ data-row-index={rowIndex}
127
+ data-col-index={colIndex}
128
+ tabIndex={isEditing ? -1 : 0}
129
+ className={cn(
130
+ 'group/cell relative h-10 overflow-hidden px-2 py-1 whitespace-nowrap outline-none',
131
+ editable && 'cursor-text',
132
+ )}
133
+ style={{
134
+ width: columnWidths[column.id],
135
+ minWidth: columnWidths[column.id],
136
+ borderRight: '1px solid rgb(var(--border))',
137
+ background: isDraggingThisColumn
138
+ ? 'rgb(var(--surface-overlay-soft))'
139
+ : undefined,
140
+ }}
141
+ onDoubleClick={() => {
142
+ if (editable) startEditing(row, column.id)
143
+ }}
144
+ onKeyDown={(event) =>
145
+ onCellKeyDown(event, row, rowIndex, column, colIndex)
146
+ }
147
+ >
148
+ {isEditing ? (
149
+ <input
150
+ ref={inputRef}
151
+ value={draftValue}
152
+ onChange={(event) => setDraftValue(event.target.value)}
153
+ onBlur={commitEdit}
154
+ onKeyDown={(event) => {
155
+ if (event.key === 'Enter') {
156
+ event.preventDefault()
157
+ commitEdit()
158
+ }
159
+
160
+ if (event.key === 'Escape') {
161
+ event.preventDefault()
162
+ cancelEdit()
163
+ }
164
+ }}
165
+ className="h-7 w-full px-2 text-xs outline-none rounded-[var(--radius-sm)]"
166
+ style={{
167
+ background: 'rgb(var(--bg-input))',
168
+ border: '1px solid rgb(var(--border-strong))',
169
+ color: 'rgb(var(--foreground))',
170
+ }}
171
+ />
172
+ ) : (
173
+ <>
174
+ <span
175
+ aria-hidden
176
+ className="pointer-events-none absolute inset-0 transition-colors duration-150"
177
+ style={{ border: '1px solid transparent' }}
178
+ />
179
+ <div
180
+ className={cn(
181
+ 'min-w-0 truncate transition-[padding] duration-150',
182
+ showDrawerAction &&
183
+ 'group-hover/cell:pr-16 group-focus-within/cell:pr-16',
184
+ )}
185
+ style={{ color: 'rgb(var(--foreground))' }}
186
+ >
187
+ {renderCell(row, column)}
188
+ </div>
189
+ {showDrawerAction ? (
190
+ <div className="pointer-events-none absolute top-1/2 right-1.5 z-10 -translate-y-1/2 opacity-0 transition-all group-focus-within/cell:pointer-events-auto group-focus-within/cell:opacity-100 group-hover/cell:pointer-events-auto group-hover/cell:opacity-100">
191
+ <Button
192
+ type="button"
193
+ variant="outline"
194
+ size="sm"
195
+ aria-label={`Open details for ${getRowLabel(row)} ${column.label}`}
196
+ title="Open drawer"
197
+ className="h-7 px-2.5"
198
+ onPointerDown={(event) => {
199
+ event.stopPropagation()
200
+ }}
201
+ onDoubleClick={(event) => {
202
+ event.stopPropagation()
203
+ }}
204
+ onClick={(event) => {
205
+ event.stopPropagation()
206
+ const originElement =
207
+ event.currentTarget.closest('td') ??
208
+ event.currentTarget
209
+ const originRect =
210
+ originElement.getBoundingClientRect()
211
+
212
+ onOpenDrawer({
213
+ rowId: row.id,
214
+ columnId: column.id,
215
+ originRect: {
216
+ x: originRect.x,
217
+ y: originRect.y,
218
+ width: originRect.width,
219
+ height: originRect.height,
220
+ },
221
+ })
222
+ }}
223
+ leadingIcon={<ExternalLink className="h-3.5 w-3.5" />}
224
+ >
225
+ Open
226
+ </Button>
227
+ </div>
228
+ ) : null}
229
+ </>
230
+ )}
231
+ </TableCell>
232
+ )
233
+ })}
234
+ </TableRow>
235
+ ))}
236
+ </TableBody>
237
+ )
238
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Table* — thin <table> / <thead> / <tbody> sub-components used
3
+ * internally by DataGrid.
4
+ *
5
+ * Inlined locally (NOT promoted to a top-level @lovett/ui Table
6
+ * primitive) per ADR-025 DEVIATION-1. Recon §5 anticipated this
7
+ * ("Probably LEAVE_BEHIND — DataGrid's table-view supersedes a
8
+ * standalone Table primitive"); the deviation realizes that
9
+ * assumption by keeping these wrappers scoped to data-grid/.
10
+ *
11
+ * Inspired by gray-ui-csm `components/ui/table.tsx` (MIT,
12
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/ui/table.tsx)
13
+ * — see packages/ui/licenses/gray-ui-csm-LICENSE.txt.
14
+ *
15
+ * Workspace ADR: ADR-025
16
+ * Ported 2026-05-18
17
+ */
18
+
19
+ import {
20
+ forwardRef,
21
+ type HTMLAttributes,
22
+ type ThHTMLAttributes,
23
+ type TdHTMLAttributes,
24
+ } from 'react'
25
+ import { cn } from '../lib/utils'
26
+
27
+ export const Table = forwardRef<
28
+ HTMLTableElement,
29
+ HTMLAttributes<HTMLTableElement> & { containerClassName?: string | undefined }
30
+ >(function Table({ className, containerClassName, ...props }, ref) {
31
+ return (
32
+ <div
33
+ className={cn('relative w-full overflow-auto', containerClassName)}
34
+ style={{
35
+ border: '1px solid rgb(var(--border))',
36
+ borderRadius: 'var(--radius-lg)',
37
+ }}
38
+ >
39
+ <table
40
+ ref={ref}
41
+ className={cn('w-full caption-bottom text-sm', className)}
42
+ {...props}
43
+ />
44
+ </div>
45
+ )
46
+ })
47
+
48
+ export const TableHeader = forwardRef<
49
+ HTMLTableSectionElement,
50
+ HTMLAttributes<HTMLTableSectionElement>
51
+ >(function TableHeader({ className, ...props }, ref) {
52
+ return (
53
+ <thead
54
+ ref={ref}
55
+ className={cn(className)}
56
+ style={{ borderBottom: '1px solid rgb(var(--border))' }}
57
+ {...props}
58
+ />
59
+ )
60
+ })
61
+
62
+ export const TableBody = forwardRef<
63
+ HTMLTableSectionElement,
64
+ HTMLAttributes<HTMLTableSectionElement>
65
+ >(function TableBody({ className, ...props }, ref) {
66
+ return (
67
+ <tbody
68
+ ref={ref}
69
+ className={cn('[&_tr:last-child]:border-0', className)}
70
+ {...props}
71
+ />
72
+ )
73
+ })
74
+
75
+ export const TableFooter = forwardRef<
76
+ HTMLTableSectionElement,
77
+ HTMLAttributes<HTMLTableSectionElement>
78
+ >(function TableFooter({ className, ...props }, ref) {
79
+ return (
80
+ <tfoot
81
+ ref={ref}
82
+ className={cn('font-normal [&>tr]:last:border-b-0', className)}
83
+ style={{
84
+ borderTop: '1px solid rgb(var(--border))',
85
+ background: 'rgb(var(--surface-overlay-soft))',
86
+ }}
87
+ {...props}
88
+ />
89
+ )
90
+ })
91
+
92
+ export const TableRow = forwardRef<
93
+ HTMLTableRowElement,
94
+ HTMLAttributes<HTMLTableRowElement>
95
+ >(function TableRow({ className, ...props }, ref) {
96
+ // border-b is a Tailwind class so the `[&_tr:last-child]:border-0`
97
+ // selector on <TableBody> can override it. (Previously this was an
98
+ // inline style, which `border-0` couldn't override — combined with
99
+ // the new outer container border it would have produced a double
100
+ // line on the bottom edge.)
101
+ return (
102
+ <tr
103
+ ref={ref}
104
+ className={cn(
105
+ 'transition-colors',
106
+ 'border-b border-[rgb(var(--border))]',
107
+ 'hover:bg-[rgb(var(--surface-overlay-soft))]',
108
+ 'data-[state=selected]:bg-[rgb(var(--surface-overlay))]',
109
+ className,
110
+ )}
111
+ {...props}
112
+ />
113
+ )
114
+ })
115
+
116
+ export const TableHead = forwardRef<
117
+ HTMLTableCellElement,
118
+ ThHTMLAttributes<HTMLTableCellElement>
119
+ >(function TableHead({ className, ...props }, ref) {
120
+ return (
121
+ <th
122
+ ref={ref}
123
+ className={cn('h-10 px-4 text-left align-middle font-medium', className)}
124
+ style={{ color: 'rgb(var(--text-muted))' }}
125
+ {...props}
126
+ />
127
+ )
128
+ })
129
+
130
+ export const TableCell = forwardRef<
131
+ HTMLTableCellElement,
132
+ TdHTMLAttributes<HTMLTableCellElement>
133
+ >(function TableCell({ className, ...props }, ref) {
134
+ return <td ref={ref} className={cn('p-4 align-middle', className)} {...props} />
135
+ })
@@ -0,0 +1,95 @@
1
+ /**
2
+ * DataGridTableHeader — the column-header row including the
3
+ * select-all checkbox cell.
4
+ *
5
+ * Ported 2026-05-18 from gray-ui-csm
6
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/table-header.tsx
7
+ * License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
8
+ * Workspace ADR: ADR-025
9
+ *
10
+ * BRING_WITH_REFACTOR:
11
+ * • Token reskin per ADR-025 D7
12
+ * • Checkbox imported from workspace BUILD_FRESH primitive (DEV-2)
13
+ * • Table sub-components from local table-elements (DEV-1)
14
+ */
15
+
16
+ import type { PointerEvent as ReactPointerEvent } from 'react'
17
+
18
+ import { Checkbox } from '../checkbox'
19
+ import { TableHead, TableHeader, TableRow } from './table-elements'
20
+
21
+ import { SortableHeaderCell } from './sortable-parts'
22
+ import type { DataGridColumn, DataGridSortState } from './types'
23
+
24
+ const CONTROL_COLUMN_WIDTH = 40
25
+
26
+ type DataGridTableHeaderProps<ColumnId extends string> = {
27
+ visibleColumns: DataGridColumn<ColumnId>[]
28
+ columnWidths: Record<ColumnId, number>
29
+ allVisibleRowsSelected: boolean
30
+ someVisibleRowsSelected: boolean
31
+ onToggleAllRows: (checked: boolean) => void
32
+ onResizeStart: (
33
+ event: ReactPointerEvent<HTMLButtonElement>,
34
+ columnId: ColumnId,
35
+ ) => void
36
+ sort: DataGridSortState<ColumnId> | null
37
+ onSortChange: ((columnId: ColumnId) => void) | undefined
38
+ }
39
+
40
+ export function DataGridTableHeader<ColumnId extends string>({
41
+ visibleColumns,
42
+ columnWidths,
43
+ allVisibleRowsSelected,
44
+ someVisibleRowsSelected,
45
+ onToggleAllRows,
46
+ onResizeStart,
47
+ sort,
48
+ onSortChange,
49
+ }: DataGridTableHeaderProps<ColumnId>) {
50
+ return (
51
+ <>
52
+ <colgroup>
53
+ <col style={{ width: CONTROL_COLUMN_WIDTH }} />
54
+ {visibleColumns.map((column) => (
55
+ <col
56
+ key={`col-${column.id}`}
57
+ style={{ width: columnWidths[column.id] }}
58
+ />
59
+ ))}
60
+ </colgroup>
61
+
62
+ <TableHeader style={{ background: 'rgb(var(--bg-card))' }}>
63
+ <TableRow style={{ background: 'rgb(var(--surface-overlay-soft))' }}>
64
+ <TableHead
65
+ className="h-10 w-10 px-0 text-center"
66
+ style={{
67
+ borderRight: '1px solid rgb(var(--border))',
68
+ background: 'rgb(var(--surface-overlay-soft))',
69
+ }}
70
+ >
71
+ <Checkbox
72
+ aria-label="Select all rows"
73
+ checked={allVisibleRowsSelected}
74
+ indeterminate={someVisibleRowsSelected}
75
+ onCheckedChange={(checked) => onToggleAllRows(checked === true)}
76
+ className="mx-auto"
77
+ />
78
+ </TableHead>
79
+ {visibleColumns.map((column) => (
80
+ <SortableHeaderCell
81
+ key={column.id}
82
+ column={column}
83
+ width={columnWidths[column.id]}
84
+ onResize={onResizeStart}
85
+ sortDir={
86
+ sort?.columnId === column.id ? sort.dir : null
87
+ }
88
+ onSortChange={onSortChange}
89
+ />
90
+ ))}
91
+ </TableRow>
92
+ </TableHeader>
93
+ </>
94
+ )
95
+ }