@lovett/ui 0.0.4 → 0.0.6
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/dist/index.d.ts +2659 -0
- package/dist/index.js +14451 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +636 -0
- package/dist/tokens.css +700 -0
- package/package.json +44 -17
- package/src/__tests__/button.test.tsx +137 -0
- package/src/__tests__/card.test.tsx +103 -0
- package/src/__tests__/dead-render.test.tsx +117 -0
- package/src/__tests__/input.test.tsx +134 -0
- package/src/__tests__/modal.test.tsx +154 -0
- package/src/__tests__/page-shell.test.tsx +128 -0
- package/src/__tests__/setup.ts +43 -0
- package/src/__tests__/token-shape.test.ts +193 -0
- package/src/allocation-sparkbar.tsx +90 -0
- package/src/card.tsx +1 -1
- package/src/collapsible-card.tsx +85 -0
- package/src/data-grid/table-body.tsx +8 -1
- package/src/dropdown-menu.tsx +1 -1
- package/src/floating-status-bar.tsx +112 -0
- package/src/folder-tree-picker.tsx +5 -6
- package/src/frame-stack.tsx +27 -10
- package/src/hero-form-card.tsx +2 -2
- package/src/icons/brand.tsx +187 -0
- package/src/index.ts +43 -0
- package/src/lib/clipboard.ts +14 -0
- package/src/lib/color.ts +111 -0
- package/src/meta-cell.tsx +52 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
- package/src/meta-previews/MetaFeedPreview.tsx +1 -1
- package/src/microsoft-logo.tsx +33 -0
- package/src/modal.tsx +77 -6
- package/src/pill-button.tsx +23 -5
- package/src/profile-section.tsx +40 -9
- package/src/sortable-table.tsx +5 -1
- package/src/styles.css +74 -0
- package/src/tabs.tsx +4 -0
- package/src/tag-chip-input.tsx +1 -1
- package/src/theme-v2.css +466 -0
- package/src/token-badge.tsx +92 -0
- package/src/tokens.css +181 -60
- package/src/v2/README.md +208 -0
- package/src/v2/__demo__/showcase.tsx +1045 -0
- package/src/v2/action.tsx +91 -0
- package/src/v2/callout.tsx +76 -0
- package/src/v2/document-section.tsx +82 -0
- package/src/v2/document-shell.tsx +0 -0
- package/src/v2/field-row.tsx +113 -0
- package/src/v2/icons.tsx +165 -0
- package/src/v2/index.ts +147 -0
- package/src/v2/layout.tsx +293 -0
- package/src/v2/progress-track.tsx +89 -0
- package/src/v2/stat-tile.tsx +129 -0
- package/src/v2/states.tsx +271 -0
- package/src/v2/status-pill.tsx +74 -0
- package/src/v2/theme.css +1861 -0
- package/src/v2/timeline.tsx +81 -0
- package/src/v2/tokens.ts +228 -0
- package/src/value-chip.tsx +76 -0
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lovett/ui v2 — showcase
|
|
3
|
+
*
|
|
4
|
+
* Every v2 primitive, in every state, on one page. This is the review
|
|
5
|
+
* surface: if a change is not visible here, it is not reviewable.
|
|
6
|
+
*
|
|
7
|
+
* Mount it anywhere:
|
|
8
|
+
*
|
|
9
|
+
* import '@lovett/ui/src/v2/theme.css'
|
|
10
|
+
* import { V2Showcase } from '@lovett/ui/src/v2/__demo__/showcase'
|
|
11
|
+
* root.render(<V2Showcase />)
|
|
12
|
+
*
|
|
13
|
+
* The appearance toggle writes `data-theme` onto the showcase root
|
|
14
|
+
* rather than `<html>`, so the same page can be checked in both
|
|
15
|
+
* appearances without touching the host document.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useState, type ReactNode } from 'react'
|
|
19
|
+
import {
|
|
20
|
+
Action,
|
|
21
|
+
Callout,
|
|
22
|
+
DefinitionList,
|
|
23
|
+
DocumentSection,
|
|
24
|
+
DocumentShell,
|
|
25
|
+
EmptyState,
|
|
26
|
+
ErrorState,
|
|
27
|
+
FieldRow,
|
|
28
|
+
Grid,
|
|
29
|
+
IconChevronRight,
|
|
30
|
+
IconRetry,
|
|
31
|
+
Inline,
|
|
32
|
+
LoadingSkeleton,
|
|
33
|
+
Panel,
|
|
34
|
+
ProgressTrack,
|
|
35
|
+
Prose,
|
|
36
|
+
Section,
|
|
37
|
+
Stack,
|
|
38
|
+
StatTile,
|
|
39
|
+
StatusPill,
|
|
40
|
+
Timeline,
|
|
41
|
+
Toolbar,
|
|
42
|
+
space,
|
|
43
|
+
type CalloutTone,
|
|
44
|
+
type StatusTone,
|
|
45
|
+
type TimelineItem,
|
|
46
|
+
} from '../index'
|
|
47
|
+
|
|
48
|
+
/* ── Local helpers (showcase only) ───────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
function Row({ label, children }: { label: string; children: ReactNode }) {
|
|
51
|
+
return (
|
|
52
|
+
<Stack gap={2}>
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
56
|
+
letterSpacing: 'var(--lv-tr-2xs)',
|
|
57
|
+
textTransform: 'uppercase',
|
|
58
|
+
fontWeight: 600,
|
|
59
|
+
color: 'var(--lv-text-faint)',
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{label}
|
|
63
|
+
</div>
|
|
64
|
+
<Inline gap={3} wrap align="center">
|
|
65
|
+
{children}
|
|
66
|
+
</Inline>
|
|
67
|
+
</Stack>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Swatch({ token, name }: { token: string; name: string }) {
|
|
72
|
+
return (
|
|
73
|
+
<Stack gap={1} style={{ minWidth: 0 }}>
|
|
74
|
+
<div className="lv-swatch" style={{ background: `var(${token})` }} />
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
78
|
+
color: 'var(--lv-text-tertiary)',
|
|
79
|
+
}}
|
|
80
|
+
className="lv-truncate"
|
|
81
|
+
>
|
|
82
|
+
{name}
|
|
83
|
+
</div>
|
|
84
|
+
</Stack>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const TIMELINE_ITEMS: readonly TimelineItem[] = [
|
|
89
|
+
{
|
|
90
|
+
id: 'ingest',
|
|
91
|
+
title: 'Source material ingested',
|
|
92
|
+
meta: '09:14',
|
|
93
|
+
state: 'done',
|
|
94
|
+
description: '41 pages across 6 documents. Deduplicated to 28 unique sections.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'extract',
|
|
98
|
+
title: 'Claims extracted',
|
|
99
|
+
meta: '09:22',
|
|
100
|
+
state: 'done',
|
|
101
|
+
description: 'Every claim carries a citation back to its source paragraph.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'review',
|
|
105
|
+
title: 'Awaiting human review',
|
|
106
|
+
meta: 'now',
|
|
107
|
+
state: 'current',
|
|
108
|
+
description:
|
|
109
|
+
'Three claims could not be traced to a primary source and are held back.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'publish',
|
|
113
|
+
title: 'Publish to workspace',
|
|
114
|
+
meta: '—',
|
|
115
|
+
state: 'todo',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'syndicate',
|
|
119
|
+
title: 'Syndicate to channel partners',
|
|
120
|
+
meta: '—',
|
|
121
|
+
state: 'blocked',
|
|
122
|
+
description: 'Blocked: partner agreement has not been countersigned.',
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
const CALLOUT_TONES: readonly CalloutTone[] = ['info', 'success', 'warn', 'danger']
|
|
127
|
+
const PILL_TONES: readonly StatusTone[] = [
|
|
128
|
+
'neutral',
|
|
129
|
+
'accent',
|
|
130
|
+
'success',
|
|
131
|
+
'warning',
|
|
132
|
+
'danger',
|
|
133
|
+
'info',
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
/* ══════════════════════════════════════════════════════════════════ */
|
|
137
|
+
|
|
138
|
+
export function V2Showcase() {
|
|
139
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light')
|
|
140
|
+
const [progress, setProgress] = useState(64)
|
|
141
|
+
const [loading, setLoading] = useState(false)
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="lv-root" data-theme={theme} style={{ minHeight: '100vh' }}>
|
|
145
|
+
<DocumentShell
|
|
146
|
+
eyebrow="Design system"
|
|
147
|
+
title="@lovett/ui v2 — every primitive, every state"
|
|
148
|
+
status={<StatusPill tone="accent">Review</StatusPill>}
|
|
149
|
+
actions={
|
|
150
|
+
<>
|
|
151
|
+
<Action
|
|
152
|
+
variant="ghost"
|
|
153
|
+
size="sm"
|
|
154
|
+
onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
|
|
155
|
+
>
|
|
156
|
+
{theme === 'light' ? 'Dark' : 'Light'}
|
|
157
|
+
</Action>
|
|
158
|
+
<Action variant="secondary" size="sm">
|
|
159
|
+
Export
|
|
160
|
+
</Action>
|
|
161
|
+
{/* The one primary action on this view. */}
|
|
162
|
+
<Action variant="primary" size="sm">
|
|
163
|
+
Approve
|
|
164
|
+
</Action>
|
|
165
|
+
</>
|
|
166
|
+
}
|
|
167
|
+
sections={[
|
|
168
|
+
{ id: 'foundations', label: 'Foundations' },
|
|
169
|
+
{ id: 'ladder', label: 'Surface ladder' },
|
|
170
|
+
{ id: 'layout', label: 'Layout' },
|
|
171
|
+
{ id: 'actions', label: 'Actions', count: 12 },
|
|
172
|
+
{ id: 'document', label: 'Document' },
|
|
173
|
+
{ id: 'fields', label: 'Fields', count: 7 },
|
|
174
|
+
{ id: 'stats', label: 'Stats' },
|
|
175
|
+
{ id: 'callouts', label: 'Callouts', count: 4 },
|
|
176
|
+
{ id: 'pills', label: 'Status pills' },
|
|
177
|
+
{ id: 'progress', label: 'Progress' },
|
|
178
|
+
{ id: 'timeline', label: 'Timeline' },
|
|
179
|
+
{ id: 'states', label: 'Empty / loading / error' },
|
|
180
|
+
{ id: 'overflow', label: 'Overflow' },
|
|
181
|
+
]}
|
|
182
|
+
>
|
|
183
|
+
{/* ── Foundations ───────────────────────────────────────── */}
|
|
184
|
+
<DocumentSection
|
|
185
|
+
id="foundations"
|
|
186
|
+
title="Foundations"
|
|
187
|
+
description="OKLCH ramps, constant hue, chroma as a percentage of each hue's sRGB maximum at that lightness. Nothing clips; no hue outranks another."
|
|
188
|
+
>
|
|
189
|
+
<Stack gap={5}>
|
|
190
|
+
<Section eyebrow="Neutral" title="Hue 255, chroma ≤ 0.023" flush>
|
|
191
|
+
<Grid gap={2} minColumnWidth={72}>
|
|
192
|
+
{[
|
|
193
|
+
'0',
|
|
194
|
+
'50',
|
|
195
|
+
'100',
|
|
196
|
+
'200',
|
|
197
|
+
'300',
|
|
198
|
+
'400',
|
|
199
|
+
'500',
|
|
200
|
+
'600',
|
|
201
|
+
'700',
|
|
202
|
+
'800',
|
|
203
|
+
'900',
|
|
204
|
+
'950',
|
|
205
|
+
].map((s) => (
|
|
206
|
+
<Swatch key={s} token={`--lv-n-${s}`} name={`n-${s}`} />
|
|
207
|
+
))}
|
|
208
|
+
</Grid>
|
|
209
|
+
</Section>
|
|
210
|
+
|
|
211
|
+
<Section eyebrow="Accent" title="Hue 27.5 — the house red" flush>
|
|
212
|
+
<Grid gap={2} minColumnWidth={72}>
|
|
213
|
+
{[
|
|
214
|
+
'50',
|
|
215
|
+
'100',
|
|
216
|
+
'200',
|
|
217
|
+
'300',
|
|
218
|
+
'400',
|
|
219
|
+
'500',
|
|
220
|
+
'600',
|
|
221
|
+
'700',
|
|
222
|
+
'800',
|
|
223
|
+
'900',
|
|
224
|
+
'950',
|
|
225
|
+
].map((s) => (
|
|
226
|
+
<Swatch key={s} token={`--lv-a-${s}`} name={`a-${s}`} />
|
|
227
|
+
))}
|
|
228
|
+
</Grid>
|
|
229
|
+
</Section>
|
|
230
|
+
|
|
231
|
+
<Section
|
|
232
|
+
eyebrow="Semantic"
|
|
233
|
+
title="Held far apart on the wheel"
|
|
234
|
+
description="danger is burnt orange, not red — the brand is red, and two reds on one page cannot be told apart. Every tone is also carried by an icon and a word."
|
|
235
|
+
flush
|
|
236
|
+
>
|
|
237
|
+
<Grid gap={2} minColumnWidth={100}>
|
|
238
|
+
<Swatch token="--lv-success-mark" name="success 150°" />
|
|
239
|
+
<Swatch token="--lv-warning-mark" name="warning 95°" />
|
|
240
|
+
<Swatch token="--lv-danger-mark" name="danger 48°" />
|
|
241
|
+
<Swatch token="--lv-info-mark" name="info 290°" />
|
|
242
|
+
<Swatch token="--lv-accent-mark" name="accent 27.5°" />
|
|
243
|
+
</Grid>
|
|
244
|
+
</Section>
|
|
245
|
+
|
|
246
|
+
<Section eyebrow="Series" title="Constant L 0.60, chroma per hue" flush>
|
|
247
|
+
<Grid gap={2} minColumnWidth={72}>
|
|
248
|
+
{[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
|
|
249
|
+
<Swatch key={n} token={`--lv-series-${n}`} name={`series-${n}`} />
|
|
250
|
+
))}
|
|
251
|
+
</Grid>
|
|
252
|
+
</Section>
|
|
253
|
+
|
|
254
|
+
<Section eyebrow="Type" title="Named by role, never by size" flush>
|
|
255
|
+
<Stack gap={3}>
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
fontSize: 'var(--lv-text-3xl)',
|
|
259
|
+
lineHeight: 'var(--lv-lh-3xl)',
|
|
260
|
+
letterSpacing: 'var(--lv-tr-3xl)',
|
|
261
|
+
fontWeight: 600,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
3xl — tightened display type
|
|
265
|
+
</div>
|
|
266
|
+
<div
|
|
267
|
+
style={{
|
|
268
|
+
fontSize: 'var(--lv-text-xl)',
|
|
269
|
+
lineHeight: 'var(--lv-lh-xl)',
|
|
270
|
+
letterSpacing: 'var(--lv-tr-xl)',
|
|
271
|
+
fontWeight: 600,
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
xl — a document title
|
|
275
|
+
</div>
|
|
276
|
+
<div
|
|
277
|
+
style={{
|
|
278
|
+
fontSize: 'var(--lv-text-lg)',
|
|
279
|
+
lineHeight: 'var(--lv-lh-lg)',
|
|
280
|
+
fontWeight: 600,
|
|
281
|
+
}}
|
|
282
|
+
>
|
|
283
|
+
lg — a section title
|
|
284
|
+
</div>
|
|
285
|
+
<Prose>
|
|
286
|
+
Prose sits at 16px / 1.62 and is capped at 68 characters, because a
|
|
287
|
+
brief is read rather than scanned. Long lines make it hard for the
|
|
288
|
+
eye to find the start of the next one, and{' '}
|
|
289
|
+
<code style={{ fontFamily: 'var(--lv-font-mono)' }}>
|
|
290
|
+
text-wrap: pretty
|
|
291
|
+
</code>{' '}
|
|
292
|
+
keeps the last line from stranding a single word.
|
|
293
|
+
</Prose>
|
|
294
|
+
<div className="lv-num" style={{ fontSize: 'var(--lv-text-sm)' }}>
|
|
295
|
+
Tabular figures: 1,284,930 · 0.001 · 11:11:11 · 99.9%
|
|
296
|
+
</div>
|
|
297
|
+
<div
|
|
298
|
+
style={{
|
|
299
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
300
|
+
letterSpacing: 'var(--lv-tr-2xs)',
|
|
301
|
+
textTransform: 'uppercase',
|
|
302
|
+
fontWeight: 600,
|
|
303
|
+
color: 'var(--lv-text-tertiary)',
|
|
304
|
+
}}
|
|
305
|
+
>
|
|
306
|
+
2xs — small caps get positive tracking
|
|
307
|
+
</div>
|
|
308
|
+
</Stack>
|
|
309
|
+
</Section>
|
|
310
|
+
</Stack>
|
|
311
|
+
</DocumentSection>
|
|
312
|
+
|
|
313
|
+
{/* ── Surface ladder ────────────────────────────────────── */}
|
|
314
|
+
<DocumentSection
|
|
315
|
+
id="ladder"
|
|
316
|
+
title="Surface ladder"
|
|
317
|
+
description="page → shell → section → card → inset. Each tier one notch brighter than the one behind it; inset goes the other way on purpose."
|
|
318
|
+
>
|
|
319
|
+
<div
|
|
320
|
+
style={{
|
|
321
|
+
background: 'var(--lv-surface-page)',
|
|
322
|
+
padding: space(4),
|
|
323
|
+
borderRadius: 'var(--lv-radius-xl)',
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
<div
|
|
327
|
+
style={{
|
|
328
|
+
background: 'var(--lv-surface-shell)',
|
|
329
|
+
padding: space(4),
|
|
330
|
+
borderRadius: 'var(--lv-radius-lg)',
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<Section eyebrow="section" title="A titled region">
|
|
334
|
+
<Stack gap={3}>
|
|
335
|
+
<div
|
|
336
|
+
style={{
|
|
337
|
+
background: 'var(--lv-surface-card)',
|
|
338
|
+
boxShadow: 'var(--lv-elev-2)',
|
|
339
|
+
borderRadius: 'var(--lv-radius-lg)',
|
|
340
|
+
padding: space(1),
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<div style={{ padding: space(3) }}>
|
|
344
|
+
<div
|
|
345
|
+
style={{
|
|
346
|
+
fontSize: 'var(--lv-text-sm)',
|
|
347
|
+
fontWeight: 600,
|
|
348
|
+
marginBottom: space(2),
|
|
349
|
+
}}
|
|
350
|
+
>
|
|
351
|
+
card — 10px radius, 4px frame
|
|
352
|
+
</div>
|
|
353
|
+
{/* THE move: a recessed panel inside a card. */}
|
|
354
|
+
<Panel>
|
|
355
|
+
<Stack gap={1}>
|
|
356
|
+
<div
|
|
357
|
+
style={{
|
|
358
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
359
|
+
letterSpacing: 'var(--lv-tr-2xs)',
|
|
360
|
+
textTransform: 'uppercase',
|
|
361
|
+
fontWeight: 600,
|
|
362
|
+
color: 'var(--lv-text-tertiary)',
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
inset panel
|
|
366
|
+
</div>
|
|
367
|
+
<div style={{ fontSize: 'var(--lv-text-sm)' }}>
|
|
368
|
+
The recess is the strongest single move in the system.
|
|
369
|
+
Put the thing the reader actually came for in here.
|
|
370
|
+
</div>
|
|
371
|
+
</Stack>
|
|
372
|
+
</Panel>
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<Inline gap={3} wrap align="stretch">
|
|
377
|
+
<Panel tone="quiet" style={{ flex: 1, minWidth: 200 }}>
|
|
378
|
+
<div style={{ fontSize: 'var(--lv-text-sm)' }}>
|
|
379
|
+
Panel tone="quiet" — hairline, no fill
|
|
380
|
+
</div>
|
|
381
|
+
</Panel>
|
|
382
|
+
<Panel tone="raised" style={{ flex: 1, minWidth: 200 }}>
|
|
383
|
+
<div style={{ fontSize: 'var(--lv-text-sm)' }}>
|
|
384
|
+
Panel tone="raised" — a floating tray
|
|
385
|
+
</div>
|
|
386
|
+
</Panel>
|
|
387
|
+
</Inline>
|
|
388
|
+
|
|
389
|
+
<Row label="Elevation">
|
|
390
|
+
{[0, 1, 2, 3, 4].map((n) => (
|
|
391
|
+
<div
|
|
392
|
+
key={n}
|
|
393
|
+
style={{
|
|
394
|
+
background: 'var(--lv-surface-card)',
|
|
395
|
+
boxShadow: `var(--lv-elev-${n})`,
|
|
396
|
+
borderRadius: 'var(--lv-radius-md)',
|
|
397
|
+
padding: `${space(2)} ${space(3)}`,
|
|
398
|
+
fontSize: 'var(--lv-text-xs)',
|
|
399
|
+
fontWeight: 500,
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
elev-{n}
|
|
403
|
+
</div>
|
|
404
|
+
))}
|
|
405
|
+
</Row>
|
|
406
|
+
</Stack>
|
|
407
|
+
</Section>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
</DocumentSection>
|
|
411
|
+
|
|
412
|
+
{/* ── Layout ────────────────────────────────────────────── */}
|
|
413
|
+
<DocumentSection
|
|
414
|
+
id="layout"
|
|
415
|
+
title="Layout primitives"
|
|
416
|
+
description="Gap takes a step on the 4px grid and nothing else. gap={13} does not compile."
|
|
417
|
+
>
|
|
418
|
+
<Stack gap={5}>
|
|
419
|
+
<Section eyebrow="Toolbar" title="Controls float above content" flush>
|
|
420
|
+
<Toolbar aria-label="Showcase toolbar">
|
|
421
|
+
<Action variant="ghost" size="sm">
|
|
422
|
+
Filter
|
|
423
|
+
</Action>
|
|
424
|
+
<Action variant="ghost" size="sm">
|
|
425
|
+
Sort
|
|
426
|
+
</Action>
|
|
427
|
+
<Toolbar.Divider />
|
|
428
|
+
<Action variant="ghost" size="sm">
|
|
429
|
+
Columns
|
|
430
|
+
</Action>
|
|
431
|
+
<Toolbar.Spacer />
|
|
432
|
+
<StatusPill tone="neutral">28 items</StatusPill>
|
|
433
|
+
<Action variant="secondary" size="sm">
|
|
434
|
+
Save view
|
|
435
|
+
</Action>
|
|
436
|
+
</Toolbar>
|
|
437
|
+
</Section>
|
|
438
|
+
|
|
439
|
+
<Section
|
|
440
|
+
eyebrow="Grid"
|
|
441
|
+
title="minColumnWidth, not breakpoints"
|
|
442
|
+
description="Columns are added as space allows. The layout breaks where the content stops fitting, not at a device preset."
|
|
443
|
+
flush
|
|
444
|
+
>
|
|
445
|
+
<Grid gap={3} minColumnWidth={180}>
|
|
446
|
+
{['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo'].map((n) => (
|
|
447
|
+
<Panel key={n} tone="quiet">
|
|
448
|
+
<div style={{ fontSize: 'var(--lv-text-sm)' }}>{n}</div>
|
|
449
|
+
</Panel>
|
|
450
|
+
))}
|
|
451
|
+
</Grid>
|
|
452
|
+
</Section>
|
|
453
|
+
|
|
454
|
+
<Section eyebrow="Stack / Inline" title="Every step on the scale" flush>
|
|
455
|
+
<Stack gap={3}>
|
|
456
|
+
{([1, 2, 3, 4, 6, 8] as const).map((g) => (
|
|
457
|
+
<Inline key={g} gap={g} align="center">
|
|
458
|
+
<span
|
|
459
|
+
style={{
|
|
460
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
461
|
+
color: 'var(--lv-text-faint)',
|
|
462
|
+
minWidth: '5ch',
|
|
463
|
+
}}
|
|
464
|
+
className="lv-num"
|
|
465
|
+
>
|
|
466
|
+
gap {g}
|
|
467
|
+
</span>
|
|
468
|
+
{[0, 1, 2, 3].map((i) => (
|
|
469
|
+
<span
|
|
470
|
+
key={i}
|
|
471
|
+
style={{
|
|
472
|
+
width: 24,
|
|
473
|
+
height: 24,
|
|
474
|
+
borderRadius: 'var(--lv-radius-sm)',
|
|
475
|
+
background: 'var(--lv-surface-inset)',
|
|
476
|
+
boxShadow: 'var(--lv-elev-inset)',
|
|
477
|
+
}}
|
|
478
|
+
/>
|
|
479
|
+
))}
|
|
480
|
+
</Inline>
|
|
481
|
+
))}
|
|
482
|
+
</Stack>
|
|
483
|
+
</Section>
|
|
484
|
+
</Stack>
|
|
485
|
+
</DocumentSection>
|
|
486
|
+
|
|
487
|
+
{/* ── Actions ───────────────────────────────────────────── */}
|
|
488
|
+
<DocumentSection
|
|
489
|
+
id="actions"
|
|
490
|
+
title="Actions"
|
|
491
|
+
description="Exactly one primary per view. Size is invariant across every state — loading hides the label behind a spinner rather than replacing it."
|
|
492
|
+
>
|
|
493
|
+
<Stack gap={5}>
|
|
494
|
+
<Row label="Variants — default">
|
|
495
|
+
<Action variant="primary">Primary</Action>
|
|
496
|
+
<Action variant="secondary">Secondary</Action>
|
|
497
|
+
<Action variant="ghost">Ghost</Action>
|
|
498
|
+
<Action variant="danger">Delete brief</Action>
|
|
499
|
+
</Row>
|
|
500
|
+
|
|
501
|
+
<Row label="Disabled — paint changes, box does not">
|
|
502
|
+
<Action variant="primary" disabled>
|
|
503
|
+
Primary
|
|
504
|
+
</Action>
|
|
505
|
+
<Action variant="secondary" disabled>
|
|
506
|
+
Secondary
|
|
507
|
+
</Action>
|
|
508
|
+
<Action variant="ghost" disabled>
|
|
509
|
+
Ghost
|
|
510
|
+
</Action>
|
|
511
|
+
<Action variant="danger" disabled>
|
|
512
|
+
Delete brief
|
|
513
|
+
</Action>
|
|
514
|
+
</Row>
|
|
515
|
+
|
|
516
|
+
<Row label="Loading — identical width to the row above">
|
|
517
|
+
<Action variant="primary" loading>
|
|
518
|
+
Primary
|
|
519
|
+
</Action>
|
|
520
|
+
<Action variant="secondary" loading>
|
|
521
|
+
Secondary
|
|
522
|
+
</Action>
|
|
523
|
+
<Action variant="ghost" loading>
|
|
524
|
+
Ghost
|
|
525
|
+
</Action>
|
|
526
|
+
<Action variant="danger" loading>
|
|
527
|
+
Delete brief
|
|
528
|
+
</Action>
|
|
529
|
+
</Row>
|
|
530
|
+
|
|
531
|
+
<Row label="Sizes">
|
|
532
|
+
<Action variant="secondary" size="sm">
|
|
533
|
+
Small
|
|
534
|
+
</Action>
|
|
535
|
+
<Action variant="secondary" size="md">
|
|
536
|
+
Medium
|
|
537
|
+
</Action>
|
|
538
|
+
<Action variant="secondary" size="lg">
|
|
539
|
+
Large
|
|
540
|
+
</Action>
|
|
541
|
+
</Row>
|
|
542
|
+
|
|
543
|
+
<Row label="With icons / icon-only">
|
|
544
|
+
<Action variant="secondary" icon={<IconRetry size={14} />}>
|
|
545
|
+
Retry
|
|
546
|
+
</Action>
|
|
547
|
+
<Action
|
|
548
|
+
variant="secondary"
|
|
549
|
+
trailingIcon={<IconChevronRight size={14} />}
|
|
550
|
+
>
|
|
551
|
+
Continue
|
|
552
|
+
</Action>
|
|
553
|
+
<Action
|
|
554
|
+
variant="ghost"
|
|
555
|
+
aria-label="Retry"
|
|
556
|
+
icon={<IconRetry size={14} />}
|
|
557
|
+
/>
|
|
558
|
+
</Row>
|
|
559
|
+
|
|
560
|
+
<Callout tone="info" title="Focus">
|
|
561
|
+
Tab through this section. Every interactive element gets the same
|
|
562
|
+
2px accent ring at a 2px offset — one focus treatment, everywhere,
|
|
563
|
+
set once at the root.
|
|
564
|
+
</Callout>
|
|
565
|
+
</Stack>
|
|
566
|
+
</DocumentSection>
|
|
567
|
+
|
|
568
|
+
{/* ── Document ──────────────────────────────────────────── */}
|
|
569
|
+
<DocumentSection
|
|
570
|
+
id="document"
|
|
571
|
+
title="Document surfaces"
|
|
572
|
+
description="This whole page is a DocumentShell. The rail on the left is scroll-spied; the header is sticky; the active indicator is always rendered and transparent when inactive, so nothing shifts as you scroll."
|
|
573
|
+
actions={
|
|
574
|
+
<Action variant="ghost" size="sm">
|
|
575
|
+
Edit
|
|
576
|
+
</Action>
|
|
577
|
+
}
|
|
578
|
+
>
|
|
579
|
+
<Section
|
|
580
|
+
eyebrow="Brief"
|
|
581
|
+
title="Positioning statement"
|
|
582
|
+
description="A DocumentSection can hold Sections, and a Section can hold a Panel. That is the ladder doing its job."
|
|
583
|
+
actions={<StatusPill tone="success">Approved</StatusPill>}
|
|
584
|
+
>
|
|
585
|
+
<Stack gap={4}>
|
|
586
|
+
<Prose>
|
|
587
|
+
For mid-market operators who have outgrown spreadsheets but cannot
|
|
588
|
+
justify an enterprise rollout, the product is the only planning
|
|
589
|
+
surface that keeps the model and the narrative in one document —
|
|
590
|
+
so the number in the deck and the number in the sheet cannot drift
|
|
591
|
+
apart.
|
|
592
|
+
</Prose>
|
|
593
|
+
<Panel>
|
|
594
|
+
<Stack gap={2}>
|
|
595
|
+
<div
|
|
596
|
+
style={{
|
|
597
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
598
|
+
letterSpacing: 'var(--lv-tr-2xs)',
|
|
599
|
+
textTransform: 'uppercase',
|
|
600
|
+
fontWeight: 600,
|
|
601
|
+
color: 'var(--lv-text-tertiary)',
|
|
602
|
+
}}
|
|
603
|
+
>
|
|
604
|
+
Extracted claim · needs a citation
|
|
605
|
+
</div>
|
|
606
|
+
<div style={{ fontSize: 'var(--lv-text-sm)' }}>
|
|
607
|
+
“Teams recover four hours a week within the first
|
|
608
|
+
month.”
|
|
609
|
+
</div>
|
|
610
|
+
</Stack>
|
|
611
|
+
</Panel>
|
|
612
|
+
</Stack>
|
|
613
|
+
</Section>
|
|
614
|
+
</DocumentSection>
|
|
615
|
+
|
|
616
|
+
{/* ── Fields ────────────────────────────────────────────── */}
|
|
617
|
+
<DocumentSection
|
|
618
|
+
id="fields"
|
|
619
|
+
title="Fields"
|
|
620
|
+
description="One shared label column across every row, so labels and values line up between unrelated panels. Below 640px it stacks rather than truncating the value."
|
|
621
|
+
>
|
|
622
|
+
<Grid gap={5} minColumnWidth={340}>
|
|
623
|
+
<Section eyebrow="Default" title="No dividers" flush>
|
|
624
|
+
<DefinitionList>
|
|
625
|
+
<FieldRow label="Brief ID" value="BRF-2026-0148" numeric />
|
|
626
|
+
<FieldRow label="Owner" value="Edwin Lovett" />
|
|
627
|
+
<FieldRow
|
|
628
|
+
label="Source documents"
|
|
629
|
+
value="6"
|
|
630
|
+
numeric
|
|
631
|
+
hint="Deduplicated from 11 uploads"
|
|
632
|
+
/>
|
|
633
|
+
<FieldRow label="Last generated" value="2026-08-07 09:22" numeric />
|
|
634
|
+
<FieldRow label="Reviewer" />
|
|
635
|
+
<FieldRow
|
|
636
|
+
label="A deliberately long label that has to wrap somewhere"
|
|
637
|
+
value="Wraps in the label column rather than pushing the value off the grid."
|
|
638
|
+
/>
|
|
639
|
+
<FieldRow
|
|
640
|
+
label="Unbroken value"
|
|
641
|
+
value="urn:brandy:brief:0148:artifact:a7f3c9e1b2d84f60a5c7e2b91d3f8a04"
|
|
642
|
+
/>
|
|
643
|
+
</DefinitionList>
|
|
644
|
+
</Section>
|
|
645
|
+
|
|
646
|
+
<Section eyebrow="Variants" title="Dividers and stacked" flush>
|
|
647
|
+
<Stack gap={5}>
|
|
648
|
+
<DefinitionList dividers>
|
|
649
|
+
<FieldRow label="Status" value="In review" />
|
|
650
|
+
<FieldRow label="Confidence" value="0.82" numeric />
|
|
651
|
+
<FieldRow label="Model" value="claude-opus-5" />
|
|
652
|
+
</DefinitionList>
|
|
653
|
+
<DefinitionList>
|
|
654
|
+
<FieldRow
|
|
655
|
+
layout="stacked"
|
|
656
|
+
label="Summary"
|
|
657
|
+
value="Stacked layout, for values that need the full width."
|
|
658
|
+
/>
|
|
659
|
+
<FieldRow layout="stacked" label="Empty stacked" />
|
|
660
|
+
</DefinitionList>
|
|
661
|
+
</Stack>
|
|
662
|
+
</Section>
|
|
663
|
+
</Grid>
|
|
664
|
+
</DocumentSection>
|
|
665
|
+
|
|
666
|
+
{/* ── Stats ─────────────────────────────────────────────── */}
|
|
667
|
+
<DocumentSection
|
|
668
|
+
id="stats"
|
|
669
|
+
title="Stats"
|
|
670
|
+
description="Tabular figures plus a min-width slot sized in ch for the largest plausible value. Toggle loading — the tiles do not move."
|
|
671
|
+
actions={
|
|
672
|
+
<Action
|
|
673
|
+
variant="secondary"
|
|
674
|
+
size="sm"
|
|
675
|
+
onClick={() => setLoading((l) => !l)}
|
|
676
|
+
>
|
|
677
|
+
{loading ? 'Show values' : 'Show loading'}
|
|
678
|
+
</Action>
|
|
679
|
+
}
|
|
680
|
+
>
|
|
681
|
+
<Stack gap={4}>
|
|
682
|
+
<Grid gap={3} minColumnWidth={190}>
|
|
683
|
+
<StatTile
|
|
684
|
+
label="Impressions"
|
|
685
|
+
value="1,284,930"
|
|
686
|
+
slots={9}
|
|
687
|
+
delta="+12.4%"
|
|
688
|
+
deltaTone="up"
|
|
689
|
+
hint="vs. last 30 days"
|
|
690
|
+
loading={loading}
|
|
691
|
+
/>
|
|
692
|
+
<StatTile
|
|
693
|
+
label="Cost per result"
|
|
694
|
+
value="4.18"
|
|
695
|
+
unit="USD"
|
|
696
|
+
slots={6}
|
|
697
|
+
delta="-2.1%"
|
|
698
|
+
deltaTone="down"
|
|
699
|
+
hint="vs. last 30 days"
|
|
700
|
+
loading={loading}
|
|
701
|
+
/>
|
|
702
|
+
<StatTile
|
|
703
|
+
label="Completion"
|
|
704
|
+
value="99.9"
|
|
705
|
+
unit="%"
|
|
706
|
+
slots={5}
|
|
707
|
+
delta="0.0%"
|
|
708
|
+
deltaTone="flat"
|
|
709
|
+
loading={loading}
|
|
710
|
+
/>
|
|
711
|
+
{/* No delta and no hint — the foot row is still reserved, so
|
|
712
|
+
this tile is exactly as tall as its neighbours. */}
|
|
713
|
+
<StatTile label="Sources" value="6" slots={5} loading={loading} />
|
|
714
|
+
{/* No value at all. */}
|
|
715
|
+
<StatTile label="Awaiting data" slots={5} />
|
|
716
|
+
</Grid>
|
|
717
|
+
|
|
718
|
+
<Section eyebrow="Inset variant" title="Stats inside a card" flush>
|
|
719
|
+
<div
|
|
720
|
+
style={{
|
|
721
|
+
background: 'var(--lv-surface-card)',
|
|
722
|
+
boxShadow: 'var(--lv-elev-2)',
|
|
723
|
+
borderRadius: 'var(--lv-radius-lg)',
|
|
724
|
+
padding: space(4),
|
|
725
|
+
}}
|
|
726
|
+
>
|
|
727
|
+
<Grid gap={3} minColumnWidth={160}>
|
|
728
|
+
<StatTile inset label="Claims" value="28" slots={4} />
|
|
729
|
+
<StatTile inset label="Cited" value="25" slots={4} />
|
|
730
|
+
<StatTile
|
|
731
|
+
inset
|
|
732
|
+
label="Unverified"
|
|
733
|
+
value="3"
|
|
734
|
+
slots={4}
|
|
735
|
+
delta="+3"
|
|
736
|
+
deltaTone="down"
|
|
737
|
+
/>
|
|
738
|
+
</Grid>
|
|
739
|
+
</div>
|
|
740
|
+
</Section>
|
|
741
|
+
</Stack>
|
|
742
|
+
</DocumentSection>
|
|
743
|
+
|
|
744
|
+
{/* ── Callouts ──────────────────────────────────────────── */}
|
|
745
|
+
<DocumentSection
|
|
746
|
+
id="callouts"
|
|
747
|
+
title="Callouts"
|
|
748
|
+
description="Every tone carries a distinct icon silhouette and a screen-reader word. Squint until the colour goes — the tones are still legible."
|
|
749
|
+
>
|
|
750
|
+
<Stack gap={3}>
|
|
751
|
+
{CALLOUT_TONES.map((tone) => (
|
|
752
|
+
<Callout
|
|
753
|
+
key={tone}
|
|
754
|
+
tone={tone}
|
|
755
|
+
title={
|
|
756
|
+
tone === 'info'
|
|
757
|
+
? 'Three claims are missing a primary source'
|
|
758
|
+
: tone === 'success'
|
|
759
|
+
? 'Brief approved and locked'
|
|
760
|
+
: tone === 'warn'
|
|
761
|
+
? 'The comparison period is incomplete'
|
|
762
|
+
: 'Syndication failed for two partners'
|
|
763
|
+
}
|
|
764
|
+
>
|
|
765
|
+
{tone === 'danger'
|
|
766
|
+
? 'Danger is burnt orange rather than red, because the brand accent is red and two reds on one page cannot be told apart. The icon and the verb do the work colour used to.'
|
|
767
|
+
: 'The tone is drawn as a leading rule plus a soft fill — never a full border, which would read as a form field rather than an aside.'}
|
|
768
|
+
</Callout>
|
|
769
|
+
))}
|
|
770
|
+
|
|
771
|
+
<Callout
|
|
772
|
+
tone="warn"
|
|
773
|
+
title="With actions"
|
|
774
|
+
actions={
|
|
775
|
+
<>
|
|
776
|
+
<Action variant="secondary" size="sm">
|
|
777
|
+
Recalculate
|
|
778
|
+
</Action>
|
|
779
|
+
<Action variant="ghost" size="sm">
|
|
780
|
+
Dismiss
|
|
781
|
+
</Action>
|
|
782
|
+
</>
|
|
783
|
+
}
|
|
784
|
+
>
|
|
785
|
+
Actions inside a callout are secondary or ghost. The view's one
|
|
786
|
+
primary action lives in the header.
|
|
787
|
+
</Callout>
|
|
788
|
+
|
|
789
|
+
<Callout tone="info">
|
|
790
|
+
A callout with no title — body only, still on the grid.
|
|
791
|
+
</Callout>
|
|
792
|
+
</Stack>
|
|
793
|
+
</DocumentSection>
|
|
794
|
+
|
|
795
|
+
{/* ── Status pills ──────────────────────────────────────── */}
|
|
796
|
+
<DocumentSection
|
|
797
|
+
id="pills"
|
|
798
|
+
title="Status pills"
|
|
799
|
+
description="The marker shape changes with the tone as well as the colour, so a pill reads correctly in greyscale. Fixed 20px height in every tone."
|
|
800
|
+
>
|
|
801
|
+
<Stack gap={4}>
|
|
802
|
+
<Row label="Tones — default shape per tone">
|
|
803
|
+
{PILL_TONES.map((tone) => (
|
|
804
|
+
<StatusPill key={tone} tone={tone}>
|
|
805
|
+
{tone}
|
|
806
|
+
</StatusPill>
|
|
807
|
+
))}
|
|
808
|
+
</Row>
|
|
809
|
+
<Row label="Explicit shapes">
|
|
810
|
+
<StatusPill tone="neutral" shape="dot">
|
|
811
|
+
dot
|
|
812
|
+
</StatusPill>
|
|
813
|
+
<StatusPill tone="neutral" shape="ring">
|
|
814
|
+
ring
|
|
815
|
+
</StatusPill>
|
|
816
|
+
<StatusPill tone="neutral" shape="square">
|
|
817
|
+
square
|
|
818
|
+
</StatusPill>
|
|
819
|
+
<StatusPill tone="neutral" shape="triangle">
|
|
820
|
+
triangle
|
|
821
|
+
</StatusPill>
|
|
822
|
+
<StatusPill tone="neutral" shape="bar">
|
|
823
|
+
bar
|
|
824
|
+
</StatusPill>
|
|
825
|
+
<StatusPill tone="neutral" hideMark>
|
|
826
|
+
v2.0.1
|
|
827
|
+
</StatusPill>
|
|
828
|
+
</Row>
|
|
829
|
+
<Row label="Overflow — ellipsizes, never wraps">
|
|
830
|
+
<span style={{ maxWidth: 180, display: 'inline-flex', minWidth: 0 }}>
|
|
831
|
+
<StatusPill tone="warning">
|
|
832
|
+
Blocked on a countersignature from the partner legal team
|
|
833
|
+
</StatusPill>
|
|
834
|
+
</span>
|
|
835
|
+
</Row>
|
|
836
|
+
</Stack>
|
|
837
|
+
</DocumentSection>
|
|
838
|
+
|
|
839
|
+
{/* ── Progress ──────────────────────────────────────────── */}
|
|
840
|
+
<DocumentSection
|
|
841
|
+
id="progress"
|
|
842
|
+
title="Progress"
|
|
843
|
+
description="The track is a recessed groove. The readout sits in a 4ch slot, so 9% → 100% cannot nudge the label."
|
|
844
|
+
actions={
|
|
845
|
+
<Inline gap={2}>
|
|
846
|
+
<Action
|
|
847
|
+
variant="ghost"
|
|
848
|
+
size="sm"
|
|
849
|
+
onClick={() => setProgress((p) => Math.max(0, p - 17))}
|
|
850
|
+
>
|
|
851
|
+
−17
|
|
852
|
+
</Action>
|
|
853
|
+
<Action
|
|
854
|
+
variant="ghost"
|
|
855
|
+
size="sm"
|
|
856
|
+
onClick={() => setProgress((p) => Math.min(100, p + 17))}
|
|
857
|
+
>
|
|
858
|
+
+17
|
|
859
|
+
</Action>
|
|
860
|
+
</Inline>
|
|
861
|
+
}
|
|
862
|
+
>
|
|
863
|
+
<Grid gap={5} minColumnWidth={280}>
|
|
864
|
+
<Stack gap={4}>
|
|
865
|
+
<ProgressTrack value={progress} label="Interactive" />
|
|
866
|
+
<ProgressTrack value={0} label="Zero" />
|
|
867
|
+
<ProgressTrack value={100} label="Complete" tone="success" />
|
|
868
|
+
</Stack>
|
|
869
|
+
<Stack gap={4}>
|
|
870
|
+
<ProgressTrack value={62} label="Warning" tone="warning" />
|
|
871
|
+
<ProgressTrack value={19} label="Danger" tone="danger" />
|
|
872
|
+
<ProgressTrack
|
|
873
|
+
indeterminate
|
|
874
|
+
label="Indeterminate"
|
|
875
|
+
aria-label="Generating brief"
|
|
876
|
+
/>
|
|
877
|
+
<ProgressTrack value={41} label="No readout" hideValue />
|
|
878
|
+
</Stack>
|
|
879
|
+
</Grid>
|
|
880
|
+
</DocumentSection>
|
|
881
|
+
|
|
882
|
+
{/* ── Timeline ──────────────────────────────────────────── */}
|
|
883
|
+
<DocumentSection
|
|
884
|
+
id="timeline"
|
|
885
|
+
title="Timeline"
|
|
886
|
+
description="The rail is one of the few places a line is the right answer — it communicates continuity, which spacing alone cannot."
|
|
887
|
+
>
|
|
888
|
+
<Timeline items={TIMELINE_ITEMS} />
|
|
889
|
+
</DocumentSection>
|
|
890
|
+
|
|
891
|
+
{/* ── States ────────────────────────────────────────────── */}
|
|
892
|
+
<DocumentSection
|
|
893
|
+
id="states"
|
|
894
|
+
title="Empty · loading · error"
|
|
895
|
+
description="All three take the same reserve prop. These three boxes are byte-identical in height, which is what stops a region jumping through its lifecycle."
|
|
896
|
+
>
|
|
897
|
+
<Stack gap={5}>
|
|
898
|
+
<Grid gap={4} minColumnWidth={300} align="stretch">
|
|
899
|
+
<EmptyState
|
|
900
|
+
surface="card"
|
|
901
|
+
reserve={240}
|
|
902
|
+
title="No briefs yet"
|
|
903
|
+
description="Briefs appear here once a source document has been ingested and reviewed."
|
|
904
|
+
actions={<Action variant="primary">Create brief</Action>}
|
|
905
|
+
/>
|
|
906
|
+
<div
|
|
907
|
+
style={{
|
|
908
|
+
background: 'var(--lv-surface-card)',
|
|
909
|
+
boxShadow: 'var(--lv-elev-1)',
|
|
910
|
+
borderRadius: 'var(--lv-radius-md)',
|
|
911
|
+
minHeight: 240,
|
|
912
|
+
padding: space(6),
|
|
913
|
+
display: 'flex',
|
|
914
|
+
}}
|
|
915
|
+
>
|
|
916
|
+
<LoadingSkeleton variant="document" lines={5} style={{ flex: 1 }} />
|
|
917
|
+
</div>
|
|
918
|
+
<ErrorState
|
|
919
|
+
surface="card"
|
|
920
|
+
reserve={240}
|
|
921
|
+
title="Could not load briefs"
|
|
922
|
+
description="The workspace responded but the payload failed validation."
|
|
923
|
+
detail={
|
|
924
|
+
'ZodError: expected array, received object\n at parseBriefList (briefs.ts:41)\n at loadWorkspace (workspace.ts:118)'
|
|
925
|
+
}
|
|
926
|
+
actions={
|
|
927
|
+
<Action variant="secondary" icon={<IconRetry size={14} />}>
|
|
928
|
+
Retry
|
|
929
|
+
</Action>
|
|
930
|
+
}
|
|
931
|
+
/>
|
|
932
|
+
</Grid>
|
|
933
|
+
|
|
934
|
+
<Section eyebrow="Skeleton variants" title="Shaped like the real thing" flush>
|
|
935
|
+
<Grid gap={4} minColumnWidth={260}>
|
|
936
|
+
<Panel tone="quiet">
|
|
937
|
+
<Stack gap={3}>
|
|
938
|
+
<div
|
|
939
|
+
style={{
|
|
940
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
941
|
+
color: 'var(--lv-text-faint)',
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
stat
|
|
945
|
+
</div>
|
|
946
|
+
<LoadingSkeleton variant="stat" />
|
|
947
|
+
</Stack>
|
|
948
|
+
</Panel>
|
|
949
|
+
<Panel tone="quiet">
|
|
950
|
+
<Stack gap={3}>
|
|
951
|
+
<div
|
|
952
|
+
style={{
|
|
953
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
954
|
+
color: 'var(--lv-text-faint)',
|
|
955
|
+
}}
|
|
956
|
+
>
|
|
957
|
+
fields
|
|
958
|
+
</div>
|
|
959
|
+
<LoadingSkeleton variant="fields" lines={4} />
|
|
960
|
+
</Stack>
|
|
961
|
+
</Panel>
|
|
962
|
+
<Panel tone="quiet">
|
|
963
|
+
<Stack gap={3}>
|
|
964
|
+
<div
|
|
965
|
+
style={{
|
|
966
|
+
fontSize: 'var(--lv-text-2xs)',
|
|
967
|
+
color: 'var(--lv-text-faint)',
|
|
968
|
+
}}
|
|
969
|
+
>
|
|
970
|
+
text / block
|
|
971
|
+
</div>
|
|
972
|
+
<LoadingSkeleton variant="text" lines={3} />
|
|
973
|
+
<LoadingSkeleton variant="block" height={48} />
|
|
974
|
+
</Stack>
|
|
975
|
+
</Panel>
|
|
976
|
+
</Grid>
|
|
977
|
+
</Section>
|
|
978
|
+
|
|
979
|
+
<Section eyebrow="Bare" title="Without a surface" flush>
|
|
980
|
+
<EmptyState
|
|
981
|
+
title="Nothing matches this filter"
|
|
982
|
+
description="Clear one of the four active filters to widen the result set."
|
|
983
|
+
actions={<Action variant="ghost">Clear filters</Action>}
|
|
984
|
+
/>
|
|
985
|
+
</Section>
|
|
986
|
+
</Stack>
|
|
987
|
+
</DocumentSection>
|
|
988
|
+
|
|
989
|
+
{/* ── Overflow ──────────────────────────────────────────── */}
|
|
990
|
+
<DocumentSection
|
|
991
|
+
id="overflow"
|
|
992
|
+
title="Overflow"
|
|
993
|
+
description="Every place a string can be longer than its box, and what happens there."
|
|
994
|
+
>
|
|
995
|
+
<Grid gap={4} minColumnWidth={300}>
|
|
996
|
+
<Panel tone="quiet">
|
|
997
|
+
<Stack gap={3}>
|
|
998
|
+
<StatTile
|
|
999
|
+
label="A metric label long enough that it has to ellipsize somewhere"
|
|
1000
|
+
value="1,284,930,004"
|
|
1001
|
+
slots={13}
|
|
1002
|
+
delta="+1,204.9%"
|
|
1003
|
+
deltaTone="up"
|
|
1004
|
+
hint="Compared against a hint that is also far too long to fit"
|
|
1005
|
+
/>
|
|
1006
|
+
<ProgressTrack
|
|
1007
|
+
value={73}
|
|
1008
|
+
label="A progress label that has to give way to the readout beside it"
|
|
1009
|
+
/>
|
|
1010
|
+
</Stack>
|
|
1011
|
+
</Panel>
|
|
1012
|
+
<Panel tone="quiet">
|
|
1013
|
+
<DefinitionList>
|
|
1014
|
+
<FieldRow
|
|
1015
|
+
label="Unbreakable token"
|
|
1016
|
+
value="sk_live_51NxAbCdEfGhIjKlMnOpQrStUvWxYz0123456789abcdefghijklmnop"
|
|
1017
|
+
/>
|
|
1018
|
+
<FieldRow
|
|
1019
|
+
label="Long prose value"
|
|
1020
|
+
value="Values wrap and the row grows downward; the label column keeps its width so the grid stays aligned with every other row on the page."
|
|
1021
|
+
/>
|
|
1022
|
+
</DefinitionList>
|
|
1023
|
+
</Panel>
|
|
1024
|
+
<Panel tone="quiet">
|
|
1025
|
+
<Timeline
|
|
1026
|
+
items={[
|
|
1027
|
+
{
|
|
1028
|
+
id: 'long',
|
|
1029
|
+
title:
|
|
1030
|
+
'A timeline entry whose title runs long enough to wrap onto a second and probably a third line',
|
|
1031
|
+
meta: '2026-08-07',
|
|
1032
|
+
state: 'current',
|
|
1033
|
+
description:
|
|
1034
|
+
'The metadata stays on one line at the trailing edge while the title wraps beneath it, because a wrapped timestamp reads as broken.',
|
|
1035
|
+
},
|
|
1036
|
+
{ id: 'short', title: 'Short one', meta: '09:14', state: 'done' },
|
|
1037
|
+
]}
|
|
1038
|
+
/>
|
|
1039
|
+
</Panel>
|
|
1040
|
+
</Grid>
|
|
1041
|
+
</DocumentSection>
|
|
1042
|
+
</DocumentShell>
|
|
1043
|
+
</div>
|
|
1044
|
+
)
|
|
1045
|
+
}
|