@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,119 @@
1
+ /**
2
+ * CompletionRing — small SVG ring showing a 0–100 percentage.
3
+ *
4
+ * Used wherever the app wants to communicate "X% of this thing is
5
+ * done" in a compact, glanceable widget — currently on the Brand
6
+ * Profile rail and (queued) on the dashboard onboarding card.
7
+ *
8
+ * Sizing is configurable so the same primitive can render at 30px
9
+ * (rail) and 38–48px (dashboard) without duplicating the stroke maths.
10
+ *
11
+ * <CompletionRing percent={80} />
12
+ * <CompletionRing percent={pct} size={38} strokeWidth={3} />
13
+ * <CompletionRing percent={pct} showLabel={false} />
14
+ * <CompletionRing percent={pct} label={<Check className="h-3 w-3" />} />
15
+ *
16
+ * SVG is the right tool for ring geometry — the "no inline SVG" guidance
17
+ * applies to icons (use Lucide), not to chart primitives like this one.
18
+ */
19
+
20
+ import type { CSSProperties, ReactNode } from 'react'
21
+ import { cn } from './lib/utils'
22
+
23
+ export interface CompletionRingProps {
24
+ /** 0–100. Values outside the range are clamped. */
25
+ percent: number
26
+ /** Outer dimension in px. Default 30. */
27
+ size?: number
28
+ /** Ring stroke width in px. Default scales with size (size / 12). */
29
+ strokeWidth?: number
30
+ /**
31
+ * What to render in the middle. Defaults to the percent number with
32
+ * a `%` suffix. Pass `false`/`null` to hide. Pass a node (icon, text)
33
+ * to fully override.
34
+ */
35
+ label?: ReactNode | false
36
+ /** Convenience: false hides the default label. Same as `label={false}`. */
37
+ showLabel?: boolean
38
+ /** Track colour CSS value. Defaults to `--surface-overlay-strong`. */
39
+ trackColor?: string
40
+ /** Fill (progress arc) colour. Defaults to `--accent`. */
41
+ fillColor?: string
42
+ /** Extra classes / inline style on the wrapper. */
43
+ className?: string
44
+ style?: CSSProperties
45
+ }
46
+
47
+ export default function CompletionRing({
48
+ percent,
49
+ size = 30,
50
+ strokeWidth,
51
+ label,
52
+ showLabel = true,
53
+ trackColor = 'rgb(var(--surface-overlay-strong))',
54
+ fillColor = 'rgb(var(--accent))',
55
+ className,
56
+ style,
57
+ }: CompletionRingProps) {
58
+ const clamped = Math.max(0, Math.min(100, percent))
59
+ const sw = strokeWidth ?? Math.max(2, Math.round(size / 12))
60
+ const r = (size - sw) / 2
61
+ const cx = size / 2
62
+ const cy = size / 2
63
+ const circ = 2 * Math.PI * r
64
+ const dashoff = circ * (1 - clamped / 100)
65
+
66
+ // `label` (when provided) wins over `showLabel`. The default renders
67
+ // the percentage in tabular-nums at a size that fits comfortably in
68
+ // the ring at any sane diameter.
69
+ const explicitLabel = label === undefined ? undefined : label
70
+ const renderedLabel =
71
+ explicitLabel !== undefined
72
+ ? explicitLabel || null
73
+ : showLabel
74
+ ? `${Math.round(clamped)}%`
75
+ : null
76
+
77
+ return (
78
+ <div
79
+ className={cn('relative shrink-0', className)}
80
+ style={{ width: size, height: size, ...style }}
81
+ >
82
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
83
+ <circle
84
+ cx={cx}
85
+ cy={cy}
86
+ r={r}
87
+ fill="none"
88
+ stroke={trackColor}
89
+ strokeWidth={sw}
90
+ />
91
+ <circle
92
+ cx={cx}
93
+ cy={cy}
94
+ r={r}
95
+ fill="none"
96
+ stroke={fillColor}
97
+ strokeWidth={sw}
98
+ strokeLinecap="round"
99
+ strokeDasharray={circ}
100
+ strokeDashoffset={dashoff}
101
+ transform={`rotate(-90 ${cx} ${cy})`}
102
+ style={{ transition: 'stroke-dashoffset var(--dur-base) var(--ease-out)' }}
103
+ />
104
+ </svg>
105
+ {renderedLabel !== null && (
106
+ <div
107
+ className="absolute inset-0 flex items-center justify-center font-bold tabular-nums leading-none"
108
+ style={{
109
+ color: 'rgb(var(--foreground))',
110
+ // Auto-size the label to the ring so 30px and 48px both look right.
111
+ fontSize: Math.max(8, Math.round(size * 0.3)),
112
+ }}
113
+ >
114
+ {renderedLabel}
115
+ </div>
116
+ )}
117
+ </div>
118
+ )
119
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * CopyField — an input/output field with a copy button pinned to the
3
+ * top-right corner.
4
+ *
5
+ * The workbench input/output pattern from the tool prototypes: a single
6
+ * field a user types into and that a transform writes its result back
7
+ * into ("output replaces input"), with a one-click copy affordance
8
+ * always in reach.
9
+ *
10
+ * Wraps <Textarea> (`multiline`, the default) or <Input>. Owns the copy
11
+ * interaction end-to-end — clipboard write, a transient ✓ confirmation,
12
+ * and a success/error toast. The field content is right-padded so text
13
+ * never slides under the button.
14
+ *
15
+ * CopyField does NOT render a char/line count — that is caller state;
16
+ * render it in the surrounding footer.
17
+ *
18
+ * Workspace ADR: ADR-031 Decision 2.
19
+ */
20
+
21
+ import {
22
+ useId,
23
+ useState,
24
+ type ChangeEvent,
25
+ type CSSProperties,
26
+ } from 'react'
27
+ import { Check, Copy } from 'lucide-react'
28
+ import { Textarea } from './textarea'
29
+ import { Input } from './input'
30
+ import { toast } from './toast'
31
+ import { cn } from './lib/utils'
32
+
33
+ export interface CopyFieldProps {
34
+ /** Field value (controlled). */
35
+ value: string
36
+ /** Change handler. Omit for a read-only output field. */
37
+ onChange?: (value: string) => void
38
+ /** Multi-line textarea (default) or single-line input. */
39
+ multiline?: boolean
40
+ /** Textarea row count. Ignored when `multiline` is false. */
41
+ rows?: number
42
+ placeholder?: string
43
+ readOnly?: boolean
44
+ /** Visible field label — wires `<label htmlFor>`. */
45
+ label?: string
46
+ /** Keep the label for assistive tech but hide it visually. */
47
+ hideLabel?: boolean
48
+ /** Field id. Auto-generated when omitted. */
49
+ id?: string
50
+ /** Toast message on a successful copy. */
51
+ copyToastMessage?: string
52
+ className?: string
53
+ }
54
+
55
+ export function CopyField({
56
+ value,
57
+ onChange,
58
+ multiline = true,
59
+ rows = 10,
60
+ placeholder,
61
+ readOnly,
62
+ label,
63
+ hideLabel,
64
+ id,
65
+ copyToastMessage = 'Copied to clipboard',
66
+ className,
67
+ }: CopyFieldProps) {
68
+ const reactId = useId()
69
+ const fieldId = id ?? reactId
70
+ const [copied, setCopied] = useState(false)
71
+
72
+ const handleCopy = async () => {
73
+ if (value === '') return
74
+ let ok = false
75
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
76
+ try {
77
+ await navigator.clipboard.writeText(value)
78
+ ok = true
79
+ } catch {
80
+ ok = false
81
+ }
82
+ }
83
+ if (ok) {
84
+ setCopied(true)
85
+ setTimeout(() => setCopied(false), 1500)
86
+ toast.success(copyToastMessage)
87
+ } else {
88
+ toast.error('Clipboard unavailable')
89
+ }
90
+ }
91
+
92
+ // Reserve room on the right so text never slides under the button.
93
+ const fieldStyle: CSSProperties = { paddingRight: 44 }
94
+
95
+ return (
96
+ <div className={cn('flex flex-col gap-1.5', className)}>
97
+ {label && (
98
+ <label
99
+ htmlFor={fieldId}
100
+ className={cn(
101
+ 'text-[12px] font-semibold tracking-[0.02em]',
102
+ hideLabel && 'sr-only',
103
+ )}
104
+ style={{ color: 'rgb(var(--foreground))' }}
105
+ >
106
+ {label}
107
+ </label>
108
+ )}
109
+ <div className="relative">
110
+ {multiline ? (
111
+ <Textarea
112
+ id={fieldId}
113
+ value={value}
114
+ {...(onChange
115
+ ? {
116
+ onChange: (e: ChangeEvent<HTMLTextAreaElement>) =>
117
+ onChange(e.target.value),
118
+ }
119
+ : {})}
120
+ placeholder={placeholder}
121
+ readOnly={readOnly}
122
+ rows={rows}
123
+ style={fieldStyle}
124
+ />
125
+ ) : (
126
+ <Input
127
+ id={fieldId}
128
+ value={value}
129
+ {...(onChange
130
+ ? {
131
+ onChange: (e: ChangeEvent<HTMLInputElement>) =>
132
+ onChange(e.target.value),
133
+ }
134
+ : {})}
135
+ placeholder={placeholder}
136
+ readOnly={readOnly}
137
+ style={fieldStyle}
138
+ />
139
+ )}
140
+ <button
141
+ type="button"
142
+ onClick={handleCopy}
143
+ disabled={value === ''}
144
+ aria-label={copied ? 'Copied' : 'Copy'}
145
+ className="absolute inline-flex items-center justify-center transition-colors disabled:opacity-40 disabled:pointer-events-none"
146
+ style={{
147
+ top: 8,
148
+ right: 8,
149
+ height: 28,
150
+ width: 28,
151
+ borderRadius: 'var(--radius-xs)',
152
+ background: 'rgb(var(--bg-card))',
153
+ border: '1px solid rgb(var(--border))',
154
+ boxShadow: 'var(--shadow-sm)',
155
+ color: copied
156
+ ? 'rgb(var(--success))'
157
+ : 'rgb(var(--text-secondary))',
158
+ }}
159
+ >
160
+ {copied ? (
161
+ <Check className="h-3.5 w-3.5" />
162
+ ) : (
163
+ <Copy className="h-3.5 w-3.5" />
164
+ )}
165
+ </button>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * DataGridColumnOptionsMenu — toolbar dropdown for showing summary
3
+ * row + toggling column visibility + reordering columns via dnd-kit.
4
+ *
5
+ * Ported 2026-05-18 from gray-ui-csm
6
+ * https://github.com/Jason-uxui/gray-ui-csm/blob/baf8346ac0c1160282e0b173de1bffc594d9c9c3/components/data-grid/column-options-menu.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 per CLAUDE.md §6 (DEV-4)
13
+ * • DropdownMenu* imports come from the workspace BUILD_FRESH
14
+ * primitive (DEV-3). Trigger inherits the secondary Button visual
15
+ * per ADR-026 D4 (DQ-4) — by default the workspace
16
+ * DropdownMenuTrigger applies `.btn .btn-secondary`, so we just
17
+ * spread the icon + label inside it.
18
+ */
19
+
20
+ import { closestCenter, DndContext } from '@dnd-kit/core'
21
+ import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
22
+ import { SlidersHorizontal } from 'lucide-react'
23
+
24
+ import {
25
+ DropdownMenu,
26
+ DropdownMenuCheckboxItem,
27
+ DropdownMenuContent,
28
+ DropdownMenuGroup,
29
+ DropdownMenuLabel,
30
+ DropdownMenuSeparator,
31
+ DropdownMenuTrigger,
32
+ } from '../dropdown-menu'
33
+ import { SortableColumnOptionItem } from './sortable-parts'
34
+ import type {
35
+ DataGridColumn,
36
+ DataGridToolbarRenderProps,
37
+ } from './types'
38
+
39
+ type DataGridColumnOptionsMenuProps<ColumnId extends string> = Pick<
40
+ DataGridToolbarRenderProps<ColumnId>,
41
+ | 'showSummaries'
42
+ | 'onShowSummariesChange'
43
+ | 'visibleColumns'
44
+ | 'visibleColumnIds'
45
+ | 'hiddenColumns'
46
+ | 'toggleColumnVisibility'
47
+ | 'optionsSensors'
48
+ | 'onOptionColumnDragEnd'
49
+ | 'optionsDndContextId'
50
+ > & {
51
+ triggerLabel?: string
52
+ }
53
+
54
+ export function DataGridColumnOptionsMenu<ColumnId extends string>({
55
+ showSummaries,
56
+ onShowSummariesChange,
57
+ visibleColumns,
58
+ visibleColumnIds,
59
+ hiddenColumns,
60
+ toggleColumnVisibility,
61
+ optionsSensors,
62
+ onOptionColumnDragEnd,
63
+ optionsDndContextId,
64
+ triggerLabel = 'Options',
65
+ }: DataGridColumnOptionsMenuProps<ColumnId>) {
66
+ return (
67
+ <DropdownMenu>
68
+ <DropdownMenuTrigger aria-label="Open table options">
69
+ <SlidersHorizontal className="h-4 w-4" />
70
+ <span>{triggerLabel}</span>
71
+ </DropdownMenuTrigger>
72
+ <DropdownMenuContent align="end" className="w-56">
73
+ <DropdownMenuGroup>
74
+ <DropdownMenuLabel>Options</DropdownMenuLabel>
75
+ </DropdownMenuGroup>
76
+ <DropdownMenuSeparator />
77
+ <DropdownMenuCheckboxItem
78
+ checked={showSummaries}
79
+ onCheckedChange={(checked) => onShowSummariesChange(checked === true)}
80
+ >
81
+ Show summary row
82
+ </DropdownMenuCheckboxItem>
83
+
84
+ <DropdownMenuSeparator />
85
+ <DropdownMenuGroup>
86
+ <DropdownMenuLabel>Columns</DropdownMenuLabel>
87
+ </DropdownMenuGroup>
88
+ <DndContext
89
+ id={optionsDndContextId}
90
+ sensors={optionsSensors}
91
+ collisionDetection={closestCenter}
92
+ onDragEnd={onOptionColumnDragEnd}
93
+ >
94
+ <SortableContext
95
+ items={visibleColumns.map((column) => column.id)}
96
+ strategy={verticalListSortingStrategy}
97
+ >
98
+ {visibleColumns.map((column) => (
99
+ <SortableColumnOptionItem
100
+ key={column.id}
101
+ column={column}
102
+ checked
103
+ disabled={visibleColumnIds.length === 1}
104
+ onCheckedChange={(checked) =>
105
+ toggleColumnVisibility(column.id, checked)
106
+ }
107
+ />
108
+ ))}
109
+ </SortableContext>
110
+ </DndContext>
111
+
112
+ {hiddenColumns.length > 0 ? <DropdownMenuSeparator /> : null}
113
+ {hiddenColumns.map((column: DataGridColumn<ColumnId>) => (
114
+ <DropdownMenuCheckboxItem
115
+ key={column.id}
116
+ checked={false}
117
+ onCheckedChange={(checked) =>
118
+ toggleColumnVisibility(column.id, checked === true)
119
+ }
120
+ >
121
+ {column.label}
122
+ </DropdownMenuCheckboxItem>
123
+ ))}
124
+ </DropdownMenuContent>
125
+ </DropdownMenu>
126
+ )
127
+ }