@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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lovett/ui v2 — Timeline
|
|
3
|
+
*
|
|
4
|
+
* An ordered sequence of events with a vertical rail. The rail is one of
|
|
5
|
+
* the few places a line is the right answer: it communicates continuity
|
|
6
|
+
* between items, which spacing alone cannot do.
|
|
7
|
+
*
|
|
8
|
+
* State is carried by the marker's glyph (tick / dot / bar) as well as
|
|
9
|
+
* its colour, and the state word is exposed to screen readers. A "done"
|
|
10
|
+
* step and a "blocked" step are distinguishable with the colour removed.
|
|
11
|
+
*
|
|
12
|
+
* Marker rings are `inset` box-shadows rather than borders, so the
|
|
13
|
+
* markers stay the same 20px whatever their state — a border would grow
|
|
14
|
+
* the box and shift the rail.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react'
|
|
18
|
+
import { cn } from '../lib/utils'
|
|
19
|
+
import { IconCheck, IconDanger, IconDot } from './icons'
|
|
20
|
+
|
|
21
|
+
export type TimelineState = 'done' | 'current' | 'todo' | 'blocked'
|
|
22
|
+
|
|
23
|
+
export interface TimelineItem {
|
|
24
|
+
id: string
|
|
25
|
+
title: ReactNode
|
|
26
|
+
/** Trailing metadata — a timestamp, an actor. Tabular and nowrap. */
|
|
27
|
+
meta?: ReactNode
|
|
28
|
+
description?: ReactNode
|
|
29
|
+
state?: TimelineState
|
|
30
|
+
/** Replace the default state glyph. */
|
|
31
|
+
icon?: ReactNode
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const STATE_ICON: Readonly<Record<TimelineState, ReactNode>> = {
|
|
35
|
+
done: <IconCheck size={12} />,
|
|
36
|
+
current: <IconDot size={12} />,
|
|
37
|
+
todo: <IconDot size={10} />,
|
|
38
|
+
blocked: <IconDanger size={12} />,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STATE_WORD: Readonly<Record<TimelineState, string>> = {
|
|
42
|
+
done: 'Completed',
|
|
43
|
+
current: 'In progress',
|
|
44
|
+
todo: 'Not started',
|
|
45
|
+
blocked: 'Blocked',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TimelineProps extends HTMLAttributes<HTMLOListElement> {
|
|
49
|
+
items: readonly TimelineItem[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const Timeline = forwardRef<HTMLOListElement, TimelineProps>(
|
|
53
|
+
function Timeline({ items, className, ...rest }, ref) {
|
|
54
|
+
return (
|
|
55
|
+
<ol ref={ref} className={cn('lv-timeline', className)} {...rest}>
|
|
56
|
+
{items.map((item) => {
|
|
57
|
+
const state = item.state ?? 'todo'
|
|
58
|
+
return (
|
|
59
|
+
<li key={item.id} className="lv-timeline-item" data-state={state}>
|
|
60
|
+
<span className="lv-timeline-marker">
|
|
61
|
+
{item.icon ?? STATE_ICON[state]}
|
|
62
|
+
</span>
|
|
63
|
+
<div className="lv-timeline-content">
|
|
64
|
+
<div className="lv-timeline-titlerow">
|
|
65
|
+
<span className="lv-timeline-title">
|
|
66
|
+
<span className="lv-sr-only">{STATE_WORD[state]}: </span>
|
|
67
|
+
{item.title}
|
|
68
|
+
</span>
|
|
69
|
+
{item.meta && <span className="lv-timeline-meta">{item.meta}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
{item.description && (
|
|
72
|
+
<div className="lv-timeline-desc">{item.description}</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</li>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</ol>
|
|
79
|
+
)
|
|
80
|
+
},
|
|
81
|
+
)
|
package/src/v2/tokens.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lovett/ui v2 — tokens.ts
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript mirror of `theme.css`. Two jobs:
|
|
5
|
+
*
|
|
6
|
+
* 1. Give components a *typed* vocabulary for the scales, so a spacing
|
|
7
|
+
* prop can only ever be a step on the 4px grid — arbitrary values are
|
|
8
|
+
* not expressible, which is how a spacing scale survives contact with
|
|
9
|
+
* a deadline.
|
|
10
|
+
* 2. Carry the handful of numbers that JS genuinely needs (sticky
|
|
11
|
+
* offsets, IntersectionObserver rootMargin) — CSS custom properties
|
|
12
|
+
* cannot be read by an observer.
|
|
13
|
+
*
|
|
14
|
+
* If you change a value here, change it in `theme.css` too. The CSS is
|
|
15
|
+
* the source of truth for anything a browser paints; this file is the
|
|
16
|
+
* source of truth for anything TypeScript checks.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
20
|
+
Spacing — one 4px scale
|
|
21
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
22
|
+
|
|
23
|
+
/** Every legal spacing step. Off-scale values are not representable. */
|
|
24
|
+
export const SPACE_STEPS = [
|
|
25
|
+
0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24,
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
export type SpaceStep = (typeof SPACE_STEPS)[number]
|
|
29
|
+
|
|
30
|
+
/** `space(4)` → `'var(--lv-space-4)'`. */
|
|
31
|
+
export function space(step: SpaceStep): string {
|
|
32
|
+
return `var(--lv-space-${step})`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Pixel value of a step, for the rare case JS needs the number. */
|
|
36
|
+
export const SPACE_PX: Readonly<Record<SpaceStep, number>> = {
|
|
37
|
+
0: 0,
|
|
38
|
+
1: 4,
|
|
39
|
+
2: 8,
|
|
40
|
+
3: 12,
|
|
41
|
+
4: 16,
|
|
42
|
+
5: 20,
|
|
43
|
+
6: 24,
|
|
44
|
+
7: 28,
|
|
45
|
+
8: 32,
|
|
46
|
+
10: 40,
|
|
47
|
+
12: 48,
|
|
48
|
+
14: 56,
|
|
49
|
+
16: 64,
|
|
50
|
+
20: 80,
|
|
51
|
+
24: 96,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
55
|
+
Radius, type, motion
|
|
56
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
57
|
+
|
|
58
|
+
export type RadiusStep = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
59
|
+
|
|
60
|
+
export function radius(step: RadiusStep): string {
|
|
61
|
+
return `var(--lv-radius-${step})`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TextRole =
|
|
65
|
+
| '2xs'
|
|
66
|
+
| 'xs'
|
|
67
|
+
| 'sm'
|
|
68
|
+
| 'base'
|
|
69
|
+
| 'md'
|
|
70
|
+
| 'lg'
|
|
71
|
+
| 'xl'
|
|
72
|
+
| '2xl'
|
|
73
|
+
| '3xl'
|
|
74
|
+
| 'stat'
|
|
75
|
+
|
|
76
|
+
export type ElevationStep = 0 | 1 | 2 | 3 | 4
|
|
77
|
+
|
|
78
|
+
export function elevation(step: ElevationStep): string {
|
|
79
|
+
return `var(--lv-elev-${step})`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The recessed-panel shadow. Distinct from the elevation ladder — it
|
|
83
|
+
* digs down rather than lifting up. */
|
|
84
|
+
export const ELEVATION_INSET = 'var(--lv-elev-inset)'
|
|
85
|
+
|
|
86
|
+
export type Duration = 'instant' | 'fast' | 'base' | 'slow'
|
|
87
|
+
|
|
88
|
+
export function duration(name: Duration): string {
|
|
89
|
+
return `var(--lv-dur-${name})`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `spring` is for small elements only — buttons, toggles, badges.
|
|
93
|
+
* Never a panel, never a data value: overshoot on a number reads as
|
|
94
|
+
* imprecise. */
|
|
95
|
+
export type Easing = 'out' | 'in-out' | 'spring'
|
|
96
|
+
|
|
97
|
+
export function easing(name: Easing): string {
|
|
98
|
+
return `var(--lv-ease-${name})`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
102
|
+
Surfaces — the ladder
|
|
103
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* page → shell → section → card, each one notch brighter than the tier
|
|
107
|
+
* behind it. `inset` goes the other way on purpose: a recessed panel
|
|
108
|
+
* inside a card is the system's strongest single move, and it only
|
|
109
|
+
* reads as recessed if it is actually darker.
|
|
110
|
+
*/
|
|
111
|
+
export type SurfaceTier =
|
|
112
|
+
| 'page'
|
|
113
|
+
| 'shell'
|
|
114
|
+
| 'section'
|
|
115
|
+
| 'card'
|
|
116
|
+
| 'inset'
|
|
117
|
+
| 'raised'
|
|
118
|
+
|
|
119
|
+
export function surface(tier: SurfaceTier): string {
|
|
120
|
+
return `var(--lv-surface-${tier})`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type TextTone =
|
|
124
|
+
| 'primary'
|
|
125
|
+
| 'secondary'
|
|
126
|
+
| 'tertiary'
|
|
127
|
+
| 'faint'
|
|
128
|
+
| 'disabled'
|
|
129
|
+
| 'on-accent'
|
|
130
|
+
|
|
131
|
+
export function textColor(tone: TextTone): string {
|
|
132
|
+
return `var(--lv-text-${tone})`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
136
|
+
Semantic tones
|
|
137
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Note the hue assignment: danger is burnt orange (48°), NOT red. The
|
|
141
|
+
* brand accent is red, and a red brand beside a red danger state is not
|
|
142
|
+
* distinguishable on a page that shows both. Every component below pairs
|
|
143
|
+
* its tone with an icon and a word, so the hue is never the only signal.
|
|
144
|
+
*/
|
|
145
|
+
export type SemanticTone = 'success' | 'warning' | 'danger' | 'info'
|
|
146
|
+
|
|
147
|
+
export type Tone = SemanticTone | 'neutral' | 'accent'
|
|
148
|
+
|
|
149
|
+
export function toneText(tone: SemanticTone): string {
|
|
150
|
+
return `var(--lv-${tone}-text)`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function toneMark(tone: SemanticTone): string {
|
|
154
|
+
return `var(--lv-${tone}-mark)`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function toneSoft(tone: SemanticTone): string {
|
|
158
|
+
return `var(--lv-${tone}-soft)`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Chart series. Constant lightness so no series outranks another. */
|
|
162
|
+
export const SERIES = [
|
|
163
|
+
'var(--lv-series-1)',
|
|
164
|
+
'var(--lv-series-2)',
|
|
165
|
+
'var(--lv-series-3)',
|
|
166
|
+
'var(--lv-series-4)',
|
|
167
|
+
'var(--lv-series-5)',
|
|
168
|
+
'var(--lv-series-6)',
|
|
169
|
+
'var(--lv-series-7)',
|
|
170
|
+
'var(--lv-series-8)',
|
|
171
|
+
] as const
|
|
172
|
+
|
|
173
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
174
|
+
Layout constants — mirrored from theme.css because JS needs numbers
|
|
175
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
176
|
+
|
|
177
|
+
/** Height of the sticky DocumentShell header. Mirrors `--lv-doc-header-h`. */
|
|
178
|
+
export const DOC_HEADER_HEIGHT = 56
|
|
179
|
+
|
|
180
|
+
/** Width of the DocumentShell nav rail. Mirrors `--lv-doc-nav-w`. */
|
|
181
|
+
export const DOC_NAV_WIDTH = 224
|
|
182
|
+
|
|
183
|
+
/** Anchor scroll offset (header + breathing room). Mirrors `--lv-scroll-offset`. */
|
|
184
|
+
export const SCROLL_OFFSET = 88
|
|
185
|
+
|
|
186
|
+
/** Width the two-column document layout collapses at. Mirrors the
|
|
187
|
+
* `@media (min-width: 900px)` breakpoint — chosen because that is where
|
|
188
|
+
* a 224px rail plus a readable measure stops fitting, not because it is
|
|
189
|
+
* a device size. */
|
|
190
|
+
export const DOC_TWO_COLUMN_MIN_WIDTH = 900
|
|
191
|
+
|
|
192
|
+
/* ══════════════════════════════════════════════════════════════════════
|
|
193
|
+
Shared prop shapes
|
|
194
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
195
|
+
|
|
196
|
+
export type AlignItems = 'start' | 'center' | 'end' | 'stretch' | 'baseline'
|
|
197
|
+
export type JustifyContent =
|
|
198
|
+
| 'start'
|
|
199
|
+
| 'center'
|
|
200
|
+
| 'end'
|
|
201
|
+
| 'between'
|
|
202
|
+
| 'around'
|
|
203
|
+
| 'evenly'
|
|
204
|
+
|
|
205
|
+
const ALIGN_MAP: Readonly<Record<AlignItems, string>> = {
|
|
206
|
+
start: 'flex-start',
|
|
207
|
+
center: 'center',
|
|
208
|
+
end: 'flex-end',
|
|
209
|
+
stretch: 'stretch',
|
|
210
|
+
baseline: 'baseline',
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const JUSTIFY_MAP: Readonly<Record<JustifyContent, string>> = {
|
|
214
|
+
start: 'flex-start',
|
|
215
|
+
center: 'center',
|
|
216
|
+
end: 'flex-end',
|
|
217
|
+
between: 'space-between',
|
|
218
|
+
around: 'space-around',
|
|
219
|
+
evenly: 'space-evenly',
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function alignValue(align: AlignItems): string {
|
|
223
|
+
return ALIGN_MAP[align]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function justifyValue(justify: JustifyContent): string {
|
|
227
|
+
return JUSTIFY_MAP[justify]
|
|
228
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValueChip — a monospace value sitting in a recessed well.
|
|
3
|
+
*
|
|
4
|
+
* The smallest unit of the tightened v2 language: any literal value the
|
|
5
|
+
* user might want to read precisely or copy (a hex, an rgb triple, a
|
|
6
|
+
* dimension, an id, a duration) renders the same way everywhere —
|
|
7
|
+
* `--surface-inset` fill, hairline border, mono + tabular figures.
|
|
8
|
+
*
|
|
9
|
+
* Lifted from the extension's Colors/Fonts tabs, which had built this
|
|
10
|
+
* three times locally as `.panel-inset` chips.
|
|
11
|
+
*
|
|
12
|
+
* Pass `copyValue` to make it a button that copies and toasts. Without
|
|
13
|
+
* it the chip is inert text and renders as a <span>, so a read-only
|
|
14
|
+
* value never advertises an interaction it doesn't have.
|
|
15
|
+
*/
|
|
16
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
|
|
17
|
+
import { cn } from './lib/utils'
|
|
18
|
+
import { copyText } from './lib/clipboard'
|
|
19
|
+
|
|
20
|
+
export interface ValueChipProps
|
|
21
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
|
|
22
|
+
children: ReactNode
|
|
23
|
+
/** Makes the chip copyable. The toast reads "Copied <copyLabel>". */
|
|
24
|
+
copyValue?: string
|
|
25
|
+
copyLabel?: string
|
|
26
|
+
/** `sm` for dense rows (10px), `md` default (11px). */
|
|
27
|
+
size?: 'sm' | 'md'
|
|
28
|
+
/** Stretch to fill a flex row — the equal-thirds hex/rgb/hsl layout. */
|
|
29
|
+
fill?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ValueChip = forwardRef<HTMLButtonElement, ValueChipProps>(
|
|
33
|
+
function ValueChip(
|
|
34
|
+
{ children, copyValue, copyLabel, size = 'md', fill, className, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
37
|
+
const shared = cn(
|
|
38
|
+
'inline-flex items-center justify-center px-2 py-1 font-mono tabular-nums truncate',
|
|
39
|
+
size === 'sm' ? 'text-[10px]' : 'text-[11px]',
|
|
40
|
+
fill && 'flex-1 min-w-0',
|
|
41
|
+
className,
|
|
42
|
+
)
|
|
43
|
+
const surface = {
|
|
44
|
+
background: 'rgb(var(--surface-inset))',
|
|
45
|
+
border: '1px solid rgb(var(--border))',
|
|
46
|
+
borderRadius: 'var(--radius-sm)',
|
|
47
|
+
color: 'rgb(var(--text-secondary))',
|
|
48
|
+
} as const
|
|
49
|
+
|
|
50
|
+
if (!copyValue) {
|
|
51
|
+
return (
|
|
52
|
+
<span className={shared} style={surface}>
|
|
53
|
+
{children}
|
|
54
|
+
</span>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<button
|
|
60
|
+
ref={ref}
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => void copyText(copyValue, copyLabel)}
|
|
63
|
+
className={cn(
|
|
64
|
+
shared,
|
|
65
|
+
'cursor-pointer transition-colors',
|
|
66
|
+
'hover:border-[rgb(var(--border-strong))] hover:text-[rgb(var(--foreground))]',
|
|
67
|
+
'focus-visible:outline-none focus-visible:[box-shadow:var(--ring-focus)]',
|
|
68
|
+
)}
|
|
69
|
+
style={surface}
|
|
70
|
+
{...rest}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</button>
|
|
74
|
+
)
|
|
75
|
+
},
|
|
76
|
+
)
|