@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.
- package/README.md +28 -0
- package/package.json +53 -0
- package/src/accordion.tsx +153 -0
- package/src/atoms.tsx +197 -0
- package/src/badge.tsx +91 -0
- package/src/brand-logo-tile.tsx +200 -0
- package/src/bulk-action-bar.tsx +104 -0
- package/src/button-group.tsx +46 -0
- package/src/button.tsx +93 -0
- package/src/calculator-shell-v2.tsx +380 -0
- package/src/calculator-shell.tsx +310 -0
- package/src/card.tsx +369 -0
- package/src/checkbox.tsx +114 -0
- package/src/chip-nav.tsx +347 -0
- package/src/choropleth.tsx +62 -0
- package/src/code-block.tsx +235 -0
- package/src/completion-ring.tsx +119 -0
- package/src/copy-field.tsx +169 -0
- package/src/data-grid/column-options-menu.tsx +127 -0
- package/src/data-grid/data-grid.tsx +391 -0
- package/src/data-grid/drawer-panel.tsx +146 -0
- package/src/data-grid/index.ts +29 -0
- package/src/data-grid/sortable-parts.tsx +215 -0
- package/src/data-grid/table-body.tsx +238 -0
- package/src/data-grid/table-elements.tsx +135 -0
- package/src/data-grid/table-header.tsx +95 -0
- package/src/data-grid/table-overlays.tsx +78 -0
- package/src/data-grid/table-parts.tsx +15 -0
- package/src/data-grid/table-summary-footer.tsx +101 -0
- package/src/data-grid/table-view.tsx +176 -0
- package/src/data-grid/types.ts +141 -0
- package/src/data-grid/use-grid-columns.ts +311 -0
- package/src/data-grid/use-grid-editing.ts +154 -0
- package/src/device-frame.tsx +274 -0
- package/src/dropdown-button.tsx +221 -0
- package/src/dropdown-menu.tsx +450 -0
- package/src/empty-state.tsx +85 -0
- package/src/file-upload/file-drop-zone.tsx +156 -0
- package/src/file-upload/file-preview-grid.tsx +39 -0
- package/src/file-upload/file-preview-item.tsx +175 -0
- package/src/file-upload/file-thumbnail.tsx +98 -0
- package/src/file-upload/file-upload-button.tsx +95 -0
- package/src/file-upload/index.ts +13 -0
- package/src/file-upload/use-file-upload.ts +371 -0
- package/src/filter-bar.tsx +125 -0
- package/src/filter-dropdown.tsx +289 -0
- package/src/floating-drawer.tsx +420 -0
- package/src/folder-card.tsx +188 -0
- package/src/folder-tree-picker.tsx +511 -0
- package/src/format.ts +117 -0
- package/src/frame-stack.tsx +73 -0
- package/src/hero-form-card.tsx +471 -0
- package/src/index.ts +372 -0
- package/src/input.tsx +130 -0
- package/src/kbd.tsx +41 -0
- package/src/lib/layout.ts +22 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown.tsx +214 -0
- package/src/menu-button.tsx +142 -0
- package/src/meta-previews/CreativeMedia.tsx +119 -0
- package/src/meta-previews/InstagramExplorePreview.tsx +340 -0
- package/src/meta-previews/InstagramFeedCarousel.tsx +245 -0
- package/src/meta-previews/InstagramFeedPreview.tsx +259 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +228 -0
- package/src/meta-previews/MetaFeedPreview.tsx +294 -0
- package/src/meta-previews/MetaInstreamPreview.tsx +135 -0
- package/src/meta-previews/MetaMessengerPreview.tsx +795 -0
- package/src/meta-previews/MetaReelPreview.tsx +750 -0
- package/src/meta-previews/MetaRightColumnPreview.tsx +245 -0
- package/src/meta-previews/MetaSearchPreview.tsx +113 -0
- package/src/meta-previews/MetaStoryPreview.tsx +1132 -0
- package/src/meta-previews/ProfileAvatar.tsx +79 -0
- package/src/meta-previews/defaults.ts +27 -0
- package/src/meta-previews/index.ts +37 -0
- package/src/meta-previews/types.ts +219 -0
- package/src/metric-card.tsx +117 -0
- package/src/modal.tsx +131 -0
- package/src/option-tile.tsx +252 -0
- package/src/page-hero.tsx +78 -0
- package/src/page-shell.tsx +440 -0
- package/src/pagination.tsx +86 -0
- package/src/pill-button.tsx +103 -0
- package/src/profile-section.tsx +389 -0
- package/src/progress-bar.tsx +84 -0
- package/src/radio.tsx +172 -0
- package/src/range-slider.tsx +161 -0
- package/src/resizable.tsx +282 -0
- package/src/search-bar.tsx +81 -0
- package/src/segmented-pill.tsx +143 -0
- package/src/shell.tsx +464 -0
- package/src/slider.tsx +107 -0
- package/src/sortable-table.tsx +198 -0
- package/src/sortable.tsx +137 -0
- package/src/stat-row.tsx +132 -0
- package/src/stats-grid.tsx +38 -0
- package/src/step-loader.tsx +141 -0
- package/src/styles.css +562 -0
- package/src/tabs.tsx +143 -0
- package/src/tag-chip-input.tsx +394 -0
- package/src/textarea.tsx +39 -0
- package/src/toast.tsx +59 -0
- 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
|
+
}
|