@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
package/src/card.tsx ADDED
@@ -0,0 +1,369 @@
1
+ import {
2
+ Children,
3
+ forwardRef,
4
+ isValidElement,
5
+ type HTMLAttributes,
6
+ type ReactNode,
7
+ } from 'react'
8
+ import { cn } from './lib/utils'
9
+
10
+ /**
11
+ * Card — head/body/foot anatomy.
12
+ *
13
+ * Usage:
14
+ * <Card interactive onClick={...}>
15
+ * <Card.Head>
16
+ * <IconTile bg={...} fg={...}><Icon/></IconTile>
17
+ * <div className="flex-1 min-w-0">
18
+ * <Card.Title>Roof Replacement</Card.Title>
19
+ * <Card.Desc>Full tear-off and replacement...</Card.Desc>
20
+ * </div>
21
+ * </Card.Head>
22
+ * <Card.Body>
23
+ * <TagRow tags={['25-Year Warranty', 'Metal & Asphalt']}/>
24
+ * </Card.Body>
25
+ * <Card.Foot>
26
+ * <Card.Meta>...</Card.Meta>
27
+ * <Card.Actions>...</Card.Actions>
28
+ * </Card.Foot>
29
+ * </Card>
30
+ *
31
+ * `interactive` adds cursor + hover lift + reveal-on-hover for Card.Actions.
32
+ */
33
+
34
+ type DivProps = HTMLAttributes<HTMLDivElement>
35
+
36
+ interface CardProps extends DivProps {
37
+ interactive?: boolean
38
+ }
39
+
40
+ const CardRoot = forwardRef<HTMLDivElement, CardProps>(function Card(
41
+ { className, interactive, children, ...rest },
42
+ ref,
43
+ ) {
44
+ // Auto-wrap Head + Body (and any sibling content) into a single
45
+ // tray, with Foot left outside on the outer frame. This gives every
46
+ // card the canonical "gray frame + white focal panel + footer on
47
+ // frame" look without callers needing to restructure their JSX.
48
+ //
49
+ // Two opt-outs:
50
+ // 1. If a child is <Card.Tray> directly, the caller is managing
51
+ // tray layout themselves (asset card pattern) — render flat.
52
+ // 2. If no child is <Card.Head>/<Card.Body>/<Card.Foot>, this is
53
+ // a "custom content" card (e.g. brand-profile logo, history row,
54
+ // mini stat) — render flat so the auto-wrap doesn't add unwanted
55
+ // inset margin to small or one-off layouts.
56
+ const childArray = Children.toArray(children)
57
+ const hasExplicitTray = childArray.some(
58
+ (child) => isValidElement(child) && child.type === Tray,
59
+ )
60
+ const hasNamedSlot = childArray.some(
61
+ (child) =>
62
+ isValidElement(child) &&
63
+ (child.type === Head || child.type === Body || child.type === Foot),
64
+ )
65
+ const shouldAutoWrap = !hasExplicitTray && hasNamedSlot
66
+
67
+ let renderedChildren: ReactNode = children
68
+ if (shouldAutoWrap) {
69
+ const trayChildren: ReactNode[] = []
70
+ const footChildren: ReactNode[] = []
71
+ childArray.forEach((child) => {
72
+ if (isValidElement(child) && child.type === Foot) {
73
+ footChildren.push(child)
74
+ } else {
75
+ trayChildren.push(child)
76
+ }
77
+ })
78
+ renderedChildren = (
79
+ <>
80
+ {trayChildren.length > 0 && (
81
+ <div className="ds-card-tray flex-1">{trayChildren}</div>
82
+ )}
83
+ {footChildren}
84
+ </>
85
+ )
86
+ }
87
+
88
+ return (
89
+ <div
90
+ ref={ref}
91
+ data-interactive={interactive ? 'true' : undefined}
92
+ data-framed={shouldAutoWrap ? 'true' : undefined}
93
+ className={cn(
94
+ 'ds-card-surface group/card flex flex-col h-full overflow-hidden',
95
+ className,
96
+ )}
97
+ {...rest}
98
+ >
99
+ {renderedChildren}
100
+ </div>
101
+ )
102
+ })
103
+
104
+ function Head({ className, ...rest }: DivProps) {
105
+ return (
106
+ <div
107
+ className={cn('flex items-start gap-3 px-5 pt-5', className)}
108
+ {...rest}
109
+ />
110
+ )
111
+ }
112
+
113
+ function Body({ className, ...rest }: DivProps) {
114
+ return <div className={cn('px-5 py-3 pb-4', className)} {...rest} />
115
+ }
116
+
117
+ function Tray({ className, ...rest }: DivProps) {
118
+ return <div className={cn('ds-card-tray', className)} {...rest} />
119
+ }
120
+
121
+ function Foot({ className, ...rest }: DivProps) {
122
+ return (
123
+ <div
124
+ className={cn(
125
+ 'mt-auto flex items-center justify-between gap-3 px-5 py-3 text-[12px]',
126
+ className,
127
+ )}
128
+ style={{
129
+ color: 'rgb(var(--text-tertiary))',
130
+ ...rest.style,
131
+ }}
132
+ {...rest}
133
+ />
134
+ )
135
+ }
136
+
137
+ function Title({
138
+ children,
139
+ className,
140
+ ...rest
141
+ }: HTMLAttributes<HTMLDivElement>) {
142
+ return (
143
+ <div
144
+ className={cn(
145
+ 'text-[15px] font-semibold leading-[1.3] truncate flex-1 min-w-0',
146
+ className,
147
+ )}
148
+ style={{ color: 'rgb(var(--foreground))', ...rest.style }}
149
+ {...rest}
150
+ >
151
+ {children}
152
+ </div>
153
+ )
154
+ }
155
+
156
+ function Desc({
157
+ children,
158
+ className,
159
+ ...rest
160
+ }: HTMLAttributes<HTMLDivElement> & { clamp?: 2 | 3 }) {
161
+ const clamp = (rest as { clamp?: 2 | 3 }).clamp ?? 2
162
+ return (
163
+ <div
164
+ className={cn(
165
+ 'text-[13px] leading-[1.5] mt-1.5',
166
+ clamp === 2 && 'line-clamp-2',
167
+ clamp === 3 && 'line-clamp-3',
168
+ className,
169
+ )}
170
+ style={{ color: 'rgb(var(--text-secondary))', ...rest.style }}
171
+ {...rest}
172
+ >
173
+ {children}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ function Meta({ className, ...rest }: DivProps) {
179
+ return (
180
+ <div
181
+ className={cn('flex items-center gap-3 min-w-0', className)}
182
+ {...rest}
183
+ />
184
+ )
185
+ }
186
+
187
+ function MetaItem({
188
+ icon,
189
+ children,
190
+ className,
191
+ }: {
192
+ icon?: ReactNode
193
+ children: ReactNode
194
+ className?: string
195
+ }) {
196
+ return (
197
+ <div
198
+ className={cn('flex items-center gap-1.5 text-[12px]', className)}
199
+ style={{ color: 'rgb(var(--text-tertiary))' }}
200
+ >
201
+ {icon && (
202
+ <span style={{ opacity: 0.7 }} className="shrink-0 inline-flex">
203
+ {icon}
204
+ </span>
205
+ )}
206
+ <span className="truncate tnum">{children}</span>
207
+ </div>
208
+ )
209
+ }
210
+
211
+ function Actions({ className, ...rest }: DivProps) {
212
+ return (
213
+ <div
214
+ className={cn('flex items-center gap-1', className)}
215
+ onClick={(e) => e.stopPropagation()}
216
+ {...rest}
217
+ />
218
+ )
219
+ }
220
+
221
+ interface ActionButtonProps extends HTMLAttributes<HTMLButtonElement> {
222
+ danger?: boolean
223
+ }
224
+
225
+ const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
226
+ function ActionButton({ className, danger, children, ...rest }, ref) {
227
+ return (
228
+ <button
229
+ ref={ref}
230
+ className={cn(
231
+ 'flex h-7 w-7 items-center justify-center rounded-[6px] transition-colors',
232
+ className,
233
+ )}
234
+ style={{
235
+ background: 'transparent',
236
+ color: 'rgb(var(--text-tertiary))',
237
+ border: '1px solid rgb(var(--border))',
238
+ }}
239
+ onMouseEnter={(e) => {
240
+ if (danger) {
241
+ e.currentTarget.style.color = 'rgb(var(--destructive))'
242
+ e.currentTarget.style.borderColor = 'rgb(var(--destructive) / 0.4)'
243
+ e.currentTarget.style.background =
244
+ 'rgb(var(--destructive) / 0.08)'
245
+ } else {
246
+ e.currentTarget.style.color = 'rgb(var(--foreground))'
247
+ e.currentTarget.style.borderColor = 'rgb(var(--border-strong))'
248
+ e.currentTarget.style.background = 'rgb(var(--surface-overlay))'
249
+ }
250
+ }}
251
+ onMouseLeave={(e) => {
252
+ e.currentTarget.style.color = 'rgb(var(--text-tertiary))'
253
+ e.currentTarget.style.borderColor = 'rgb(var(--border))'
254
+ e.currentTarget.style.background = 'transparent'
255
+ }}
256
+ {...rest}
257
+ >
258
+ {children}
259
+ </button>
260
+ )
261
+ },
262
+ )
263
+
264
+ /**
265
+ * Icon-on-color tile used in card heads. Pass tone colors as inline rgb()
266
+ * strings so they can reference semantic tokens (info/success/etc.).
267
+ */
268
+ function IconTile({
269
+ bg,
270
+ fg,
271
+ size = 38,
272
+ className,
273
+ children,
274
+ }: {
275
+ bg: string
276
+ fg: string
277
+ size?: number
278
+ className?: string
279
+ children: ReactNode
280
+ }) {
281
+ return (
282
+ <span
283
+ className={cn(
284
+ 'inline-flex items-center justify-center rounded-[10px] shrink-0',
285
+ className,
286
+ )}
287
+ style={{
288
+ width: size,
289
+ height: size,
290
+ background: bg,
291
+ color: fg,
292
+ }}
293
+ >
294
+ {children}
295
+ </span>
296
+ )
297
+ }
298
+
299
+ /** Tag row helper — reads `--tag-bg`/`--border` automatically. */
300
+ function TagRow({
301
+ tags,
302
+ max = 4,
303
+ className,
304
+ }: {
305
+ tags: string[]
306
+ max?: number
307
+ className?: string
308
+ }) {
309
+ const shown = tags.slice(0, max)
310
+ const overflow = tags.length - max
311
+ return (
312
+ <div className={cn('flex flex-wrap gap-1.5', className)}>
313
+ {shown.map((t, i) => (
314
+ <span
315
+ key={i}
316
+ className="inline-flex items-center px-2.5 py-[3px] rounded-full text-[11.5px] font-medium whitespace-nowrap"
317
+ style={{
318
+ background: 'rgb(var(--surface-overlay))',
319
+ border: '1px solid rgb(var(--border))',
320
+ color: 'rgb(var(--text-secondary))',
321
+ }}
322
+ >
323
+ {t}
324
+ </span>
325
+ ))}
326
+ {overflow > 0 && (
327
+ <span
328
+ className="text-[11px] font-semibold self-center"
329
+ style={{ color: 'rgb(var(--text-muted))' }}
330
+ >
331
+ +{overflow}
332
+ </span>
333
+ )}
334
+ </div>
335
+ )
336
+ }
337
+
338
+ // Compose the API
339
+ type CardComponent = typeof CardRoot & {
340
+ Head: typeof Head
341
+ Body: typeof Body
342
+ Tray: typeof Tray
343
+ Foot: typeof Foot
344
+ Title: typeof Title
345
+ Desc: typeof Desc
346
+ Meta: typeof Meta
347
+ MetaItem: typeof MetaItem
348
+ Actions: typeof Actions
349
+ ActionButton: typeof ActionButton
350
+ IconTile: typeof IconTile
351
+ TagRow: typeof TagRow
352
+ }
353
+
354
+ const Card = CardRoot as CardComponent
355
+ Card.Head = Head
356
+ Card.Body = Body
357
+ Card.Tray = Tray
358
+ Card.Foot = Foot
359
+ Card.Title = Title
360
+ Card.Desc = Desc
361
+ Card.Meta = Meta
362
+ Card.MetaItem = MetaItem
363
+ Card.Actions = Actions
364
+ Card.ActionButton = ActionButton
365
+ Card.IconTile = IconTile
366
+ Card.TagRow = TagRow
367
+
368
+ export default Card
369
+ export { IconTile, TagRow }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Checkbox — workspace-native checkbox primitive.
3
+ *
4
+ * Inspired by gray-ui-csm `components/ui/checkbox.tsx` (MIT,
5
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/ui/checkbox.tsx)
6
+ * — see packages/ui/licenses/gray-ui-csm-LICENSE.txt.
7
+ *
8
+ * BUILD_FRESH per ADR-025 DEVIATION-2: source ships on
9
+ * `@base-ui/react/checkbox`, which is NOT in CLAUDE.md §1 allowed
10
+ * deps. The shape (icon-in-square + indeterminate state +
11
+ * accessible Root API) and the surface API (`checked`,
12
+ * `indeterminate`, `onCheckedChange`) carry over; the implementation
13
+ * is a native `<input type="checkbox">` styled with our tokens.
14
+ *
15
+ * Workspace ADR: ADR-025
16
+ * Ported 2026-05-18
17
+ */
18
+
19
+ import {
20
+ forwardRef,
21
+ useEffect,
22
+ useRef,
23
+ type InputHTMLAttributes,
24
+ } from 'react'
25
+ import { Check, Minus } from 'lucide-react'
26
+ import { cn } from './lib/utils'
27
+
28
+ export interface CheckboxProps
29
+ extends Omit<
30
+ InputHTMLAttributes<HTMLInputElement>,
31
+ 'type' | 'onChange' | 'checked'
32
+ > {
33
+ checked?: boolean
34
+ indeterminate?: boolean
35
+ onCheckedChange?: (checked: boolean) => void
36
+ }
37
+
38
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
39
+ function Checkbox(
40
+ { checked, indeterminate, onCheckedChange, className, disabled, ...rest },
41
+ forwardedRef,
42
+ ) {
43
+ const internalRef = useRef<HTMLInputElement | null>(null)
44
+
45
+ // Compose forwarded ref + internal ref so we can drive
46
+ // `.indeterminate` (a DOM-only property — not reflected as an
47
+ // attribute and not settable via React props).
48
+ const setRef = (node: HTMLInputElement | null) => {
49
+ internalRef.current = node
50
+ if (typeof forwardedRef === 'function') forwardedRef(node)
51
+ else if (forwardedRef)
52
+ (forwardedRef as { current: HTMLInputElement | null }).current = node
53
+ }
54
+
55
+ useEffect(() => {
56
+ if (internalRef.current) {
57
+ internalRef.current.indeterminate = Boolean(indeterminate)
58
+ }
59
+ }, [indeterminate])
60
+
61
+ const isChecked = Boolean(checked)
62
+ const isIndeterminate = Boolean(indeterminate)
63
+ const showIcon = isChecked || isIndeterminate
64
+
65
+ return (
66
+ <span
67
+ data-slot="checkbox"
68
+ data-state={
69
+ isIndeterminate
70
+ ? 'indeterminate'
71
+ : isChecked
72
+ ? 'checked'
73
+ : 'unchecked'
74
+ }
75
+ className={cn(
76
+ 'relative inline-flex items-center justify-center shrink-0',
77
+ 'h-4 w-4 rounded-[4px]',
78
+ 'transition-colors',
79
+ disabled && 'opacity-50 cursor-not-allowed',
80
+ className,
81
+ )}
82
+ style={{
83
+ background: showIcon
84
+ ? 'rgb(var(--accent))'
85
+ : 'rgb(var(--bg-input))',
86
+ border: showIcon
87
+ ? '1.5px solid rgb(var(--accent))'
88
+ : '1.5px solid rgb(var(--border-strong))',
89
+ // Checkmark on the brand-red fill — matches the `.btn-primary`
90
+ // pattern in styles.css (`color: white`) — fixed white reads
91
+ // cleanly in both light and dark mode on the accent fill.
92
+ color: 'white',
93
+ }}
94
+ >
95
+ <input
96
+ ref={setRef}
97
+ type="checkbox"
98
+ checked={isChecked}
99
+ disabled={disabled}
100
+ onChange={(event) => onCheckedChange?.(event.target.checked)}
101
+ className="absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed"
102
+ {...rest}
103
+ />
104
+ {showIcon ? (
105
+ isIndeterminate ? (
106
+ <Minus className="h-3 w-3 pointer-events-none" strokeWidth={3} />
107
+ ) : (
108
+ <Check className="h-3 w-3 pointer-events-none" strokeWidth={3} />
109
+ )
110
+ ) : null}
111
+ </span>
112
+ )
113
+ },
114
+ )