@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.
Files changed (59) hide show
  1. package/dist/index.d.ts +2659 -0
  2. package/dist/index.js +14451 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/styles.css +636 -0
  5. package/dist/tokens.css +700 -0
  6. package/package.json +44 -17
  7. package/src/__tests__/button.test.tsx +137 -0
  8. package/src/__tests__/card.test.tsx +103 -0
  9. package/src/__tests__/dead-render.test.tsx +117 -0
  10. package/src/__tests__/input.test.tsx +134 -0
  11. package/src/__tests__/modal.test.tsx +154 -0
  12. package/src/__tests__/page-shell.test.tsx +128 -0
  13. package/src/__tests__/setup.ts +43 -0
  14. package/src/__tests__/token-shape.test.ts +193 -0
  15. package/src/allocation-sparkbar.tsx +90 -0
  16. package/src/card.tsx +1 -1
  17. package/src/collapsible-card.tsx +85 -0
  18. package/src/data-grid/table-body.tsx +8 -1
  19. package/src/dropdown-menu.tsx +1 -1
  20. package/src/floating-status-bar.tsx +112 -0
  21. package/src/folder-tree-picker.tsx +5 -6
  22. package/src/frame-stack.tsx +27 -10
  23. package/src/hero-form-card.tsx +2 -2
  24. package/src/icons/brand.tsx +187 -0
  25. package/src/index.ts +43 -0
  26. package/src/lib/clipboard.ts +14 -0
  27. package/src/lib/color.ts +111 -0
  28. package/src/meta-cell.tsx +52 -0
  29. package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
  30. package/src/meta-previews/MetaFeedPreview.tsx +1 -1
  31. package/src/microsoft-logo.tsx +33 -0
  32. package/src/modal.tsx +77 -6
  33. package/src/pill-button.tsx +23 -5
  34. package/src/profile-section.tsx +40 -9
  35. package/src/sortable-table.tsx +5 -1
  36. package/src/styles.css +74 -0
  37. package/src/tabs.tsx +4 -0
  38. package/src/tag-chip-input.tsx +1 -1
  39. package/src/theme-v2.css +466 -0
  40. package/src/token-badge.tsx +92 -0
  41. package/src/tokens.css +181 -60
  42. package/src/v2/README.md +208 -0
  43. package/src/v2/__demo__/showcase.tsx +1045 -0
  44. package/src/v2/action.tsx +91 -0
  45. package/src/v2/callout.tsx +76 -0
  46. package/src/v2/document-section.tsx +82 -0
  47. package/src/v2/document-shell.tsx +0 -0
  48. package/src/v2/field-row.tsx +113 -0
  49. package/src/v2/icons.tsx +165 -0
  50. package/src/v2/index.ts +147 -0
  51. package/src/v2/layout.tsx +293 -0
  52. package/src/v2/progress-track.tsx +89 -0
  53. package/src/v2/stat-tile.tsx +129 -0
  54. package/src/v2/states.tsx +271 -0
  55. package/src/v2/status-pill.tsx +74 -0
  56. package/src/v2/theme.css +1861 -0
  57. package/src/v2/timeline.tsx +81 -0
  58. package/src/v2/tokens.ts +228 -0
  59. 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
+ )
@@ -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
+ )