@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,252 @@
1
+ /**
2
+ * OptionTile + OptionTileGroup primitives (ADR-029 D2).
3
+ *
4
+ * Tinted-card-with-input pattern — clickable card surface that wraps a
5
+ * native radio OR checkbox input, with accent fill + border when active
6
+ * and an optional preview/description sub-line.
7
+ *
8
+ * Replaces 4 lens-local forks of the same idiom across Convert Case
9
+ * (route + modal), Text Formatter, and the Replace/Dedupe modals.
10
+ *
11
+ * Group usage (recommended):
12
+ * <OptionTileGroup
13
+ * type="radio"
14
+ * name="case-mode"
15
+ * value={mode}
16
+ * onChange={setMode}
17
+ * options={[
18
+ * { value: 'lower', label: 'lowercase', description: 'all small letters' },
19
+ * { value: 'upper', label: 'UPPERCASE' },
20
+ * ]}
21
+ * />
22
+ *
23
+ * <OptionTileGroup
24
+ * type="checkbox"
25
+ * value={enabledOps}
26
+ * onChange={setEnabledOps}
27
+ * options={...}
28
+ * />
29
+ *
30
+ * Standalone usage (escape hatch for non-group layouts):
31
+ * <OptionTile
32
+ * selected={mode === 'lower'}
33
+ * onSelect={() => setMode('lower')}
34
+ * inputType="radio"
35
+ * inputName="case-mode"
36
+ * label="lowercase"
37
+ * description="all small letters"
38
+ * />
39
+ */
40
+
41
+ import { useId, type ReactNode } from 'react'
42
+ import { cn } from './lib/utils'
43
+
44
+ // ─── single tile ─────────────────────────────────────────────────────
45
+
46
+ export interface OptionTileProps {
47
+ selected: boolean
48
+ onSelect: () => void
49
+ inputType: 'radio' | 'checkbox'
50
+ /** Required for radio (shared name across the group). */
51
+ inputName: string | undefined
52
+ label: ReactNode
53
+ description: ReactNode | undefined
54
+ disabled: boolean | undefined
55
+ className: string | undefined
56
+ }
57
+
58
+ export function OptionTile({
59
+ selected,
60
+ onSelect,
61
+ inputType,
62
+ inputName,
63
+ label,
64
+ description,
65
+ disabled = false,
66
+ className,
67
+ }: OptionTileProps) {
68
+ const inputId = useId()
69
+
70
+ return (
71
+ <label
72
+ htmlFor={inputId}
73
+ className={cn(
74
+ 'flex items-start gap-2.5 px-3 py-2.5 cursor-pointer select-none transition-colors',
75
+ 'focus-within:[box-shadow:var(--ring-focus)]',
76
+ disabled && 'cursor-not-allowed opacity-50',
77
+ className,
78
+ )}
79
+ style={{
80
+ borderRadius: 'var(--radius-md)',
81
+ background: selected
82
+ ? 'rgb(var(--accent-subtle))'
83
+ : 'rgb(var(--surface-overlay-soft))',
84
+ border: `1px solid rgb(var(${
85
+ selected ? '--accent-muted' : '--border'
86
+ }))`,
87
+ }}
88
+ >
89
+ <input
90
+ id={inputId}
91
+ type={inputType}
92
+ name={inputName}
93
+ checked={selected}
94
+ disabled={disabled}
95
+ onChange={onSelect}
96
+ className="sr-only"
97
+ />
98
+ {/* Visual indicator — dot for radio, check for checkbox. */}
99
+ <span
100
+ aria-hidden
101
+ className="mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center"
102
+ style={{
103
+ borderRadius: inputType === 'radio' ? '9999px' : '4px',
104
+ border: `1.5px solid rgb(var(${
105
+ selected ? '--accent' : '--border-strong'
106
+ }))`,
107
+ background:
108
+ selected && inputType === 'checkbox'
109
+ ? 'rgb(var(--accent))'
110
+ : 'transparent',
111
+ }}
112
+ >
113
+ {selected && inputType === 'radio' && (
114
+ <span
115
+ className="h-2 w-2 rounded-full"
116
+ style={{ background: 'rgb(var(--accent))' }}
117
+ />
118
+ )}
119
+ {selected && inputType === 'checkbox' && (
120
+ <CheckGlyph />
121
+ )}
122
+ </span>
123
+ <span className="flex flex-col gap-0.5 min-w-0 flex-1">
124
+ <span
125
+ className="text-[13px] font-medium leading-tight"
126
+ style={{ color: 'rgb(var(--foreground))' }}
127
+ >
128
+ {label}
129
+ </span>
130
+ {description && (
131
+ <span
132
+ className="text-[12px] leading-snug"
133
+ style={{ color: 'rgb(var(--text-tertiary))' }}
134
+ >
135
+ {description}
136
+ </span>
137
+ )}
138
+ </span>
139
+ </label>
140
+ )
141
+ }
142
+
143
+ function CheckGlyph() {
144
+ // Lucide-style check, rendered as inline SVG path inside a primitive.
145
+ // Kept inline because the parent <span> already provides the colored
146
+ // background — we just need the white tick on top. NOT a violation of
147
+ // CLAUDE.md §6 (icon discipline only forbids inline SVG OUTSIDE
148
+ // packages/ui — primitives in this package may render their own
149
+ // canonical glyphs).
150
+ return (
151
+ <svg
152
+ width="10"
153
+ height="10"
154
+ viewBox="0 0 24 24"
155
+ fill="none"
156
+ stroke="white"
157
+ strokeWidth="3"
158
+ strokeLinecap="round"
159
+ strokeLinejoin="round"
160
+ >
161
+ <polyline points="20 6 9 17 4 12" />
162
+ </svg>
163
+ )
164
+ }
165
+
166
+ // ─── group wrapper ──────────────────────────────────────────────────
167
+
168
+ interface OptionTileGroupOption<T extends string> {
169
+ value: T
170
+ label: ReactNode
171
+ description?: ReactNode
172
+ disabled?: boolean
173
+ }
174
+
175
+ // Discriminated by `type` so TS narrows value + onChange shapes correctly.
176
+ export type OptionTileGroupProps<T extends string = string> =
177
+ | {
178
+ type: 'radio'
179
+ name: string
180
+ value: T | null
181
+ onChange: (next: T) => void
182
+ options: OptionTileGroupOption<T>[]
183
+ ariaLabel?: string
184
+ columns?: number
185
+ className?: string
186
+ }
187
+ | {
188
+ type: 'checkbox'
189
+ value: T[]
190
+ onChange: (next: T[]) => void
191
+ options: OptionTileGroupOption<T>[]
192
+ ariaLabel?: string
193
+ columns?: number
194
+ className?: string
195
+ /** Optional shared `name` attr on the underlying checkboxes (rarely needed). */
196
+ name?: string
197
+ }
198
+
199
+ export function OptionTileGroup<T extends string = string>(
200
+ props: OptionTileGroupProps<T>,
201
+ ) {
202
+ const gridStyle = props.columns
203
+ ? { gridTemplateColumns: `repeat(${props.columns}, minmax(0, 1fr))` }
204
+ : { gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))' }
205
+
206
+ return (
207
+ <fieldset
208
+ className={cn('grid gap-2', props.className)}
209
+ style={gridStyle}
210
+ >
211
+ {props.ariaLabel && <legend className="sr-only">{props.ariaLabel}</legend>}
212
+ {props.options.map((opt) => {
213
+ if (props.type === 'radio') {
214
+ return (
215
+ <OptionTile
216
+ key={opt.value}
217
+ selected={props.value === opt.value}
218
+ onSelect={() => props.onChange(opt.value)}
219
+ inputType="radio"
220
+ inputName={props.name}
221
+ label={opt.label}
222
+ description={opt.description}
223
+ disabled={opt.disabled}
224
+ className={undefined}
225
+ />
226
+ )
227
+ }
228
+ // checkbox variant
229
+ const isSelected = props.value.includes(opt.value)
230
+ return (
231
+ <OptionTile
232
+ key={opt.value}
233
+ selected={isSelected}
234
+ onSelect={() =>
235
+ props.onChange(
236
+ isSelected
237
+ ? props.value.filter((v) => v !== opt.value)
238
+ : [...props.value, opt.value],
239
+ )
240
+ }
241
+ inputType="checkbox"
242
+ inputName={props.name}
243
+ label={opt.label}
244
+ description={opt.description}
245
+ disabled={opt.disabled}
246
+ className={undefined}
247
+ />
248
+ )
249
+ })}
250
+ </fieldset>
251
+ )
252
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * PageHero — large title row that sits IN the content area, beneath the
3
+ * sticky header. At rest it owns the visual weight; on scroll the
4
+ * AppHeader's breadcrumbs become the persistent label.
5
+ *
6
+ * Promoted from `brand-ai-extension/webapp/src/components/ui/page-hero.tsx`
7
+ * as part of Phase A of the v1.1 lens port batch — used by Swipes,
8
+ * Collections, Discover; shared lens-local equivalents existed in earlier
9
+ * ports.
10
+ *
11
+ * Token-clean: only public tokens (`--accent`, `--foreground`,
12
+ * `--text-tertiary`).
13
+ */
14
+ import type { ReactNode } from 'react'
15
+ import { cn } from './lib/utils'
16
+
17
+ export interface PageHeroProps {
18
+ /** Small uppercase label above the title, usually colored with --accent. */
19
+ eyebrow?: string
20
+ title: string
21
+ subtitle?: string
22
+ /** Right-aligned trailing content — typically a row of action buttons. */
23
+ actions?: ReactNode
24
+ /**
25
+ * Optional left-side adornment rendered alongside the title — e.g. the
26
+ * brand profile's logo block. Sits in the title row (not next to the
27
+ * eyebrow), vertically aligned with the title text. The hero gracefully
28
+ * collapses if this is `undefined` so pages without an adornment look
29
+ * unchanged.
30
+ */
31
+ leading?: ReactNode
32
+ className?: string
33
+ }
34
+
35
+ export function PageHero({
36
+ eyebrow,
37
+ title,
38
+ subtitle,
39
+ actions,
40
+ leading,
41
+ className,
42
+ }: PageHeroProps) {
43
+ return (
44
+ <div className={cn('flex items-end justify-between gap-4 flex-wrap pt-4 pb-5', className)}>
45
+ <div className="min-w-0 flex-1 flex items-center gap-5">
46
+ {leading && <div className="shrink-0">{leading}</div>}
47
+ <div className="min-w-0 flex-1">
48
+ {eyebrow && (
49
+ <div
50
+ className="text-[11px] font-bold uppercase tracking-[0.1em] mb-1.5"
51
+ style={{ color: 'rgb(var(--accent))' }}
52
+ >
53
+ {eyebrow}
54
+ </div>
55
+ )}
56
+ <h1
57
+ className="text-[24px] font-bold tracking-[-0.02em] leading-tight"
58
+ style={{ color: 'rgb(var(--foreground))' }}
59
+ >
60
+ {title}
61
+ </h1>
62
+ {subtitle && (
63
+ <p
64
+ className="text-[13px] mt-1.5 leading-[1.5]"
65
+ style={{
66
+ color: 'rgb(var(--text-tertiary))',
67
+ maxWidth: 'var(--prose-narrow)',
68
+ }}
69
+ >
70
+ {subtitle}
71
+ </p>
72
+ )}
73
+ </div>
74
+ </div>
75
+ {actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
76
+ </div>
77
+ )
78
+ }