@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,311 @@
1
+ /**
2
+ * useGridColumns — column width / visibility / drag-reorder state.
3
+ *
4
+ * Ported 2026-05-18 from gray-ui-csm
5
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/use-grid-columns.ts
6
+ * License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
7
+ * Workspace ADR: ADR-025
8
+ *
9
+ * BRING_WITH_REFACTOR: pure TS port — no styling changes; only
10
+ * relative-path adjustment for the workspace tree.
11
+ */
12
+
13
+ import {
14
+ useCallback,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ type PointerEvent as ReactPointerEvent,
20
+ type RefObject,
21
+ } from 'react'
22
+
23
+ import {
24
+ type DragEndEvent,
25
+ type DragOverEvent,
26
+ type DragStartEvent,
27
+ } from '@dnd-kit/core'
28
+ import { arrayMove } from '@dnd-kit/sortable'
29
+
30
+ import type { DataGridColumn } from './types'
31
+
32
+ function createInitialColumnWidths<ColumnId extends string>(
33
+ columns: DataGridColumn<ColumnId>[],
34
+ ): Record<ColumnId, number> {
35
+ return columns.reduce<Record<ColumnId, number>>(
36
+ (acc, column) => {
37
+ acc[column.id] = column.defaultWidth
38
+ return acc
39
+ },
40
+ {} as Record<ColumnId, number>,
41
+ )
42
+ }
43
+
44
+ type UseGridColumnsParams<ColumnId extends string> = {
45
+ columns: DataGridColumn<ColumnId>[]
46
+ tableRef: RefObject<HTMLTableElement | null>
47
+ minColumnWidth: number
48
+ controlColumnWidth: number
49
+ }
50
+
51
+ export function useGridColumns<ColumnId extends string>({
52
+ columns,
53
+ tableRef,
54
+ minColumnWidth,
55
+ controlColumnWidth,
56
+ }: UseGridColumnsParams<ColumnId>) {
57
+ const columnOrder = useMemo(
58
+ () => columns.map((column) => column.id),
59
+ [columns],
60
+ )
61
+
62
+ const [columnWidths, setColumnWidths] = useState<
63
+ Record<ColumnId, number>
64
+ >(() => createInitialColumnWidths(columns))
65
+ const [visibleColumnIds, setVisibleColumnIds] = useState<ColumnId[]>(
66
+ () => columnOrder,
67
+ )
68
+ const [draggingColumnId, setDraggingColumnId] =
69
+ useState<ColumnId | null>(null)
70
+ const [dragOverColumnId, setDragOverColumnId] =
71
+ useState<ColumnId | null>(null)
72
+ const [dragOverlayHeight, setDragOverlayHeight] = useState(280)
73
+ const [dropIndicatorLeft, setDropIndicatorLeft] = useState<
74
+ number | null
75
+ >(null)
76
+ const [dragTableRect, setDragTableRect] = useState<{
77
+ top: number
78
+ height: number
79
+ } | null>(null)
80
+
81
+ const resizingRef = useRef<{
82
+ columnId: ColumnId
83
+ startX: number
84
+ startWidth: number
85
+ } | null>(null)
86
+ const orderBeforeDragRef = useRef<ColumnId[]>(columnOrder)
87
+
88
+ useEffect(() => {
89
+ const allowedIds = new Set(columnOrder)
90
+
91
+ setColumnWidths((current) => {
92
+ const next = createInitialColumnWidths(columns)
93
+ columns.forEach((column) => {
94
+ if (typeof current[column.id] === 'number') {
95
+ next[column.id] = current[column.id]
96
+ }
97
+ })
98
+ return next
99
+ })
100
+
101
+ setVisibleColumnIds((current) => {
102
+ const kept = current.filter((id) => allowedIds.has(id))
103
+ const missing = columnOrder.filter((id) => !kept.includes(id))
104
+ return [...kept, ...missing]
105
+ })
106
+
107
+ orderBeforeDragRef.current = columnOrder
108
+ }, [columns, columnOrder])
109
+
110
+ useEffect(() => {
111
+ const handlePointerMove = (event: PointerEvent) => {
112
+ const activeResize = resizingRef.current
113
+ if (!activeResize) return
114
+
115
+ const column = columns.find((item) => item.id === activeResize.columnId)
116
+ const resolvedMinWidth = column?.minWidth ?? minColumnWidth
117
+ const deltaX = event.clientX - activeResize.startX
118
+ const nextWidth = Math.max(
119
+ resolvedMinWidth,
120
+ activeResize.startWidth + deltaX,
121
+ )
122
+
123
+ setColumnWidths((currentWidths) => ({
124
+ ...currentWidths,
125
+ [activeResize.columnId]: nextWidth,
126
+ }))
127
+ }
128
+
129
+ const handlePointerUp = () => {
130
+ if (!resizingRef.current) return
131
+ resizingRef.current = null
132
+ document.body.style.userSelect = ''
133
+ document.body.style.cursor = ''
134
+ }
135
+
136
+ window.addEventListener('pointermove', handlePointerMove)
137
+ window.addEventListener('pointerup', handlePointerUp)
138
+
139
+ return () => {
140
+ window.removeEventListener('pointermove', handlePointerMove)
141
+ window.removeEventListener('pointerup', handlePointerUp)
142
+ document.body.style.userSelect = ''
143
+ document.body.style.cursor = ''
144
+ }
145
+ }, [columns, minColumnWidth])
146
+
147
+ const visibleColumns = useMemo(() => {
148
+ return visibleColumnIds
149
+ .map((columnId) => columns.find((column) => column.id === columnId))
150
+ .filter((column): column is DataGridColumn<ColumnId> => Boolean(column))
151
+ }, [columns, visibleColumnIds])
152
+
153
+ const gridMinWidth = useMemo(() => {
154
+ const contentWidth = visibleColumns.reduce((sum, column) => {
155
+ return sum + (columnWidths[column.id] ?? column.defaultWidth)
156
+ }, 0)
157
+
158
+ return controlColumnWidth + contentWidth
159
+ }, [columnWidths, controlColumnWidth, visibleColumns])
160
+
161
+ const hiddenColumns = useMemo(() => {
162
+ return columns.filter((column) => !visibleColumnIds.includes(column.id))
163
+ }, [columns, visibleColumnIds])
164
+
165
+ const toggleColumnVisibility = useCallback(
166
+ (columnId: ColumnId, visible: boolean) => {
167
+ setVisibleColumnIds((currentColumns) => {
168
+ if (visible) {
169
+ if (currentColumns.includes(columnId)) return currentColumns
170
+ return [...currentColumns, columnId]
171
+ }
172
+
173
+ if (currentColumns.length === 1 && currentColumns.includes(columnId)) {
174
+ return currentColumns
175
+ }
176
+
177
+ return currentColumns.filter((id) => id !== columnId)
178
+ })
179
+ },
180
+ [],
181
+ )
182
+
183
+ const handleColumnDragStart = useCallback(
184
+ (event: DragStartEvent) => {
185
+ const activeId = String(event.active.id) as ColumnId
186
+ const tableRect = tableRef.current?.getBoundingClientRect()
187
+ orderBeforeDragRef.current = visibleColumnIds
188
+ setDraggingColumnId(activeId)
189
+ setDragOverColumnId(activeId)
190
+ setDragOverlayHeight(tableRect?.height ?? 280)
191
+ if (tableRect) {
192
+ setDragTableRect({ top: tableRect.top, height: tableRect.height })
193
+ }
194
+ },
195
+ [tableRef, visibleColumnIds],
196
+ )
197
+
198
+ const handleColumnDragOver = useCallback(
199
+ (event: DragOverEvent) => {
200
+ if (!event.over) return
201
+
202
+ const activeId = String(event.active.id) as ColumnId
203
+ const overId = String(event.over.id) as ColumnId
204
+ const overRect = event.over.rect
205
+ setDragOverColumnId(overId)
206
+
207
+ const currentIndex = visibleColumnIds.indexOf(activeId)
208
+ const targetIndex = visibleColumnIds.indexOf(overId)
209
+ const indicatorLeft =
210
+ currentIndex > -1 && targetIndex > -1 && currentIndex < targetIndex
211
+ ? overRect.left + overRect.width
212
+ : overRect.left
213
+
214
+ setDropIndicatorLeft(indicatorLeft)
215
+
216
+ if (activeId === overId) return
217
+
218
+ setVisibleColumnIds((currentColumns) => {
219
+ const oldIndex = currentColumns.indexOf(activeId)
220
+ const newIndex = currentColumns.indexOf(overId)
221
+
222
+ if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) {
223
+ return currentColumns
224
+ }
225
+
226
+ return arrayMove(currentColumns, oldIndex, newIndex)
227
+ })
228
+ },
229
+ [visibleColumnIds],
230
+ )
231
+
232
+ const handleColumnDragEnd = useCallback((event: DragEndEvent) => {
233
+ if (!event.over) {
234
+ setVisibleColumnIds(orderBeforeDragRef.current)
235
+ }
236
+
237
+ setDraggingColumnId(null)
238
+ setDragOverColumnId(null)
239
+ setDropIndicatorLeft(null)
240
+ setDragTableRect(null)
241
+ }, [])
242
+
243
+ const handleOptionColumnDragEnd = useCallback((event: DragEndEvent) => {
244
+ if (!event.over) return
245
+
246
+ const activeId = String(event.active.id) as ColumnId
247
+ const overId = String(event.over.id) as ColumnId
248
+
249
+ if (activeId === overId) return
250
+
251
+ setVisibleColumnIds((currentColumns) => {
252
+ const oldIndex = currentColumns.indexOf(activeId)
253
+ const newIndex = currentColumns.indexOf(overId)
254
+
255
+ if (oldIndex === -1 || newIndex === -1) return currentColumns
256
+
257
+ return arrayMove(currentColumns, oldIndex, newIndex)
258
+ })
259
+ }, [])
260
+
261
+ const handleColumnDragCancel = useCallback(() => {
262
+ setVisibleColumnIds(orderBeforeDragRef.current)
263
+ setDraggingColumnId(null)
264
+ setDragOverColumnId(null)
265
+ setDropIndicatorLeft(null)
266
+ setDragTableRect(null)
267
+ }, [])
268
+
269
+ const beginResize = useCallback(
270
+ (event: ReactPointerEvent<HTMLButtonElement>, columnId: ColumnId) => {
271
+ event.preventDefault()
272
+ event.stopPropagation()
273
+
274
+ resizingRef.current = {
275
+ columnId,
276
+ startX: event.clientX,
277
+ startWidth: columnWidths[columnId],
278
+ }
279
+
280
+ document.body.style.userSelect = 'none'
281
+ document.body.style.cursor = 'col-resize'
282
+ },
283
+ [columnWidths],
284
+ )
285
+
286
+ const dragOverlayColumn = useMemo(() => {
287
+ if (!draggingColumnId) return null
288
+ return columns.find((column) => column.id === draggingColumnId) ?? null
289
+ }, [columns, draggingColumnId])
290
+
291
+ return {
292
+ columnWidths,
293
+ visibleColumnIds,
294
+ draggingColumnId,
295
+ dragOverColumnId,
296
+ dragOverlayHeight,
297
+ dropIndicatorLeft,
298
+ dragTableRect,
299
+ visibleColumns,
300
+ gridMinWidth,
301
+ hiddenColumns,
302
+ toggleColumnVisibility,
303
+ handleColumnDragStart,
304
+ handleColumnDragOver,
305
+ handleColumnDragEnd,
306
+ handleOptionColumnDragEnd,
307
+ handleColumnDragCancel,
308
+ beginResize,
309
+ dragOverlayColumn,
310
+ }
311
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * useGridEditing — cell editing draft / commit / keyboard navigation.
3
+ *
4
+ * Ported 2026-05-18 from gray-ui-csm
5
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/use-grid-editing.ts
6
+ * License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
7
+ * Workspace ADR: ADR-025
8
+ *
9
+ * BRING_WITH_REFACTOR: pure TS port — relative-path adjustment only.
10
+ */
11
+
12
+ import {
13
+ useCallback,
14
+ useEffect,
15
+ useState,
16
+ type KeyboardEvent as ReactKeyboardEvent,
17
+ type RefObject,
18
+ } from 'react'
19
+
20
+ import type { DataGridColumn, DataGridRowBase, EditingCell } from './types'
21
+
22
+ type UseGridEditingParams<
23
+ Row extends DataGridRowBase,
24
+ ColumnId extends string,
25
+ > = {
26
+ gridRef: RefObject<HTMLDivElement | null>
27
+ inputRef: RefObject<HTMLInputElement | null>
28
+ visibleRowCount: number
29
+ visibleColumnCount: number
30
+ isEditableColumn: (columnId: ColumnId) => boolean
31
+ getCellEditValue: (row: Row, columnId: ColumnId) => string
32
+ applyCellEdit: (row: Row, columnId: ColumnId, nextValue: string) => Row
33
+ commitRows: (updater: (currentRows: Row[]) => Row[]) => void
34
+ }
35
+
36
+ export function useGridEditing<
37
+ Row extends DataGridRowBase,
38
+ ColumnId extends string,
39
+ >({
40
+ gridRef,
41
+ inputRef,
42
+ visibleRowCount,
43
+ visibleColumnCount,
44
+ isEditableColumn,
45
+ getCellEditValue,
46
+ applyCellEdit,
47
+ commitRows,
48
+ }: UseGridEditingParams<Row, ColumnId>) {
49
+ const [editingCell, setEditingCell] =
50
+ useState<EditingCell<ColumnId> | null>(null)
51
+ const [draftValue, setDraftValue] = useState('')
52
+
53
+ useEffect(() => {
54
+ if (!editingCell) return
55
+ inputRef.current?.focus()
56
+ inputRef.current?.select()
57
+ }, [editingCell, inputRef])
58
+
59
+ const startEditing = useCallback(
60
+ (row: Row, columnId: ColumnId) => {
61
+ if (!isEditableColumn(columnId)) return
62
+ setEditingCell({ rowId: row.id, columnId })
63
+ setDraftValue(getCellEditValue(row, columnId))
64
+ },
65
+ [getCellEditValue, isEditableColumn],
66
+ )
67
+
68
+ const commitEdit = useCallback(() => {
69
+ if (!editingCell) return
70
+
71
+ commitRows((currentRows) =>
72
+ currentRows.map((row) => {
73
+ if (row.id !== editingCell.rowId) return row
74
+ return applyCellEdit(row, editingCell.columnId, draftValue)
75
+ }),
76
+ )
77
+ setEditingCell(null)
78
+ setDraftValue('')
79
+ }, [applyCellEdit, commitRows, draftValue, editingCell])
80
+
81
+ const cancelEdit = useCallback(() => {
82
+ setEditingCell(null)
83
+ setDraftValue('')
84
+ }, [])
85
+
86
+ const focusCell = useCallback(
87
+ (rowIndex: number, colIndex: number) => {
88
+ const targetCell = gridRef.current?.querySelector<HTMLElement>(
89
+ `[data-grid-cell="true"][data-row-index="${rowIndex}"][data-col-index="${colIndex}"]`,
90
+ )
91
+ targetCell?.focus()
92
+ },
93
+ [gridRef],
94
+ )
95
+
96
+ const handleCellKeyDown = useCallback(
97
+ (
98
+ event: ReactKeyboardEvent<HTMLTableCellElement>,
99
+ row: Row,
100
+ rowIndex: number,
101
+ column: DataGridColumn<ColumnId>,
102
+ colIndex: number,
103
+ ) => {
104
+ const maxRowIndex = visibleRowCount - 1
105
+ const maxColIndex = visibleColumnCount - 1
106
+
107
+ if (event.key === 'Enter' && isEditableColumn(column.id)) {
108
+ event.preventDefault()
109
+ startEditing(row, column.id)
110
+ return
111
+ }
112
+
113
+ if (event.key === 'ArrowRight') {
114
+ event.preventDefault()
115
+ focusCell(rowIndex, Math.min(colIndex + 1, maxColIndex))
116
+ return
117
+ }
118
+
119
+ if (event.key === 'ArrowLeft') {
120
+ event.preventDefault()
121
+ focusCell(rowIndex, Math.max(colIndex - 1, 0))
122
+ return
123
+ }
124
+
125
+ if (event.key === 'ArrowDown') {
126
+ event.preventDefault()
127
+ focusCell(Math.min(rowIndex + 1, maxRowIndex), colIndex)
128
+ return
129
+ }
130
+
131
+ if (event.key === 'ArrowUp') {
132
+ event.preventDefault()
133
+ focusCell(Math.max(rowIndex - 1, 0), colIndex)
134
+ }
135
+ },
136
+ [
137
+ focusCell,
138
+ isEditableColumn,
139
+ startEditing,
140
+ visibleColumnCount,
141
+ visibleRowCount,
142
+ ],
143
+ )
144
+
145
+ return {
146
+ editingCell,
147
+ draftValue,
148
+ setDraftValue,
149
+ startEditing,
150
+ commitEdit,
151
+ cancelEdit,
152
+ handleCellKeyDown,
153
+ }
154
+ }