@lovett/ui 0.0.9 → 0.0.11

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 (55) hide show
  1. package/dist/index.d.ts +823 -135
  2. package/dist/index.js +2048 -358
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +44 -2
  5. package/dist/theme-v2.css +228 -0
  6. package/dist/tokens.css +123 -8
  7. package/package.json +1 -1
  8. package/src/__tests__/anchor.test.tsx +422 -0
  9. package/src/__tests__/combobox.test.tsx +677 -0
  10. package/src/__tests__/dropdown-menu.test.tsx +418 -0
  11. package/src/__tests__/helpers/geometry.ts +58 -0
  12. package/src/__tests__/layer-stack.test.tsx +228 -0
  13. package/src/__tests__/modal.test.tsx +180 -6
  14. package/src/__tests__/popover.test.tsx +460 -0
  15. package/src/__tests__/select.test.tsx +543 -0
  16. package/src/__tests__/tooltip.test.tsx +355 -0
  17. package/src/calculator-shell-v2.tsx +19 -39
  18. package/src/code-block.tsx +15 -26
  19. package/src/combobox.tsx +796 -0
  20. package/src/dropdown-menu.tsx +142 -152
  21. package/src/icons/brand.tsx +81 -2
  22. package/src/index.ts +111 -0
  23. package/src/lib/anchor.ts +427 -0
  24. package/src/lib/focus.ts +32 -0
  25. package/src/lib/layer-stack.ts +188 -0
  26. package/src/lib/refs.ts +31 -0
  27. package/src/metric-card.tsx +57 -22
  28. package/src/modal.tsx +149 -9
  29. package/src/page-shell.tsx +91 -2
  30. package/src/popover.tsx +407 -0
  31. package/src/segmented-pill.tsx +33 -10
  32. package/src/select.tsx +646 -0
  33. package/src/stat-row.tsx +108 -70
  34. package/src/styles.css +44 -2
  35. package/src/theme-v2.css +7 -245
  36. package/src/tokens.css +123 -8
  37. package/src/tooltip.tsx +297 -0
  38. package/src/react-syntax-highlighter-prism.d.ts +0 -34
  39. package/src/v2/README.md +0 -208
  40. package/src/v2/__demo__/showcase.tsx +0 -1045
  41. package/src/v2/action.tsx +0 -91
  42. package/src/v2/callout.tsx +0 -76
  43. package/src/v2/document-section.tsx +0 -82
  44. package/src/v2/document-shell.tsx +0 -0
  45. package/src/v2/field-row.tsx +0 -113
  46. package/src/v2/icons.tsx +0 -165
  47. package/src/v2/index.ts +0 -147
  48. package/src/v2/layout.tsx +0 -293
  49. package/src/v2/progress-track.tsx +0 -89
  50. package/src/v2/stat-tile.tsx +0 -129
  51. package/src/v2/states.tsx +0 -271
  52. package/src/v2/status-pill.tsx +0 -74
  53. package/src/v2/theme.css +0 -1861
  54. package/src/v2/timeline.tsx +0 -81
  55. package/src/v2/tokens.ts +0 -228
package/dist/styles.css CHANGED
@@ -31,10 +31,24 @@
31
31
  *
32
32
  * Out of scope for Phase 1: .empty, .skel, .field*, .pg-header, .search-bar,
33
33
  * .breadcrumbs, .topbar, .checkbox, .radio, .switch, .reasoning-pulse,
34
- * .chat-md-list, .selection-reply-btn, ::selection. Those land when their
35
- * consumers (Field, EmptyState, PageHeader, etc.) arrive in later phases.
34
+ * .chat-md-list, .selection-reply-btn. Those land when their consumers
35
+ * (Field, EmptyState, PageHeader, etc.) arrive in later phases.
36
+ * `::selection` landed 2026-09-04 (ADR-145 amendment) — see TEXT SELECTION.
36
37
  */
37
38
 
39
+ /* ---- TEXT SELECTION ---- */
40
+ /**
41
+ * Explicit, token-backed highlight. Without this, the only `::selection`
42
+ * rule in the bundle came from `@lovett/shell` (`background:
43
+ * var(--selection-bg)`), whose variable is scoped to `.cs-frame` — so at
44
+ * the root it resolved to nothing and highlighted text was invisible.
45
+ * `--selection-bg` is now a public token in tokens.css (light + dark).
46
+ */
47
+ ::selection {
48
+ background: var(--selection-bg);
49
+ color: inherit;
50
+ }
51
+
38
52
  /* ---- PANE MASK ---- */
39
53
  /**
40
54
  * Opaque fill matching the MainLayout content pane, for a sticky element that
@@ -131,6 +145,34 @@
131
145
  box-shadow: var(--ring-focus);
132
146
  }
133
147
 
148
+ /* ─────────────────────────────────────────────────────────────────────────
149
+ * BASELINE FOCUS INDICATOR
150
+ *
151
+ * Until this existed, only elements that opted in — primitives via .btn /
152
+ * .input-shell, and hand-written `focus-visible:` classes — showed OUR focus
153
+ * ring. Everything else fell through to the browser's `outline: auto`, which
154
+ * on macOS paints in the USER'S SYSTEM ACCENT COLOUR. So a keyboard user with
155
+ * an amber system accent saw an amber ring on most controls and our red ring
156
+ * on a few, in the same tab sequence. Measured in the Generate lens: 115
157
+ * buttons, 35 with a ring.
158
+ *
159
+ * `:where()` gives this ZERO specificity, so it is a floor, not an override —
160
+ * .btn, .input-shell, and any component or utility class still win.
161
+ * ───────────────────────────────────────────────────────────────────────── */
162
+ @layer base {
163
+ :where(a[href], button, input, select, textarea, summary, [tabindex]:not([tabindex^='-'])):focus-visible {
164
+ /* OUTLINE, not box-shadow, deliberately. Tailwind's shadow/ring utilities
165
+ compose onto `box-shadow`, so a zero-specificity box-shadow baseline loses
166
+ to any element carrying one (e.g. a `ring-1` selected state) and silently
167
+ renders nothing. `outline` is untouched by that chain, follows
168
+ border-radius in every modern browser, and matches --ring-focus's geometry.
169
+ Elements with their own ring set `outline: none` alongside it, so they
170
+ override this cleanly rather than double-drawing. */
171
+ outline: 2px solid rgb(var(--accent));
172
+ outline-offset: 0;
173
+ }
174
+ }
175
+
134
176
  .btn-primary {
135
177
  background: rgb(var(--accent));
136
178
  color: white;
@@ -0,0 +1,228 @@
1
+ /*
2
+ * @lovett/ui — v2 theme, SCOPED.
3
+ *
4
+ * GENERATED. Regenerate with:
5
+ * python3 scripts/figma/emit-theme.py <corpus> --out packages/ui/src/theme-v2.css
6
+ *
7
+ * Opt in by putting `data-ds="v2"` on any element. Everything inside it
8
+ * renders on the v2 palette, including existing @lovett/ui primitives —
9
+ * they read the same token names, which now resolve to different values.
10
+ * Nothing outside the scope changes.
11
+ *
12
+ * Derived from a 13-kit Figma corpus; see docs/design-system/figma-corpus/.
13
+ * The direction is Vertex Lab: pure-white trays, near-neutral greys, a
14
+ * navy-tinted ink used as a SURFACE and not only as text.
15
+ *
16
+ * The semantic reassignment is the load-bearing change:
17
+ * --accent red brand identity and the primary action, nothing else
18
+ * --success green save, publish, approve, live
19
+ * --destructive AMBER destructive. No longer red.
20
+ * --warning gold caution
21
+ * --info violet
22
+ *
23
+ * Consequence worth knowing before adopting: amber is a weaker danger
24
+ * signal than red, so destructive actions must carry an icon and an
25
+ * explicit verb. Colour alone no longer does that work.
26
+ */
27
+
28
+ [data-ds="v2"] {
29
+ /* ── Surfaces ─────────────────────────────────────────────── */
30
+ /* The frame/tray step is 2.1 pts — ratified as `medium`. Both being
31
+ white removes the distinction the card pattern is built on. */
32
+ --page-bg: 240 241 242;
33
+ --background: 240 241 242;
34
+ --main-content-bg: 240 241 242;
35
+ --bg-surface: 248 248 249;
36
+ --surface-frame: 248 248 249;
37
+ --card-frame-bg: 248 248 249;
38
+ --bg-card: 255 255 255;
39
+ --card: 255 255 255;
40
+ --tray-bg: 255 255 255;
41
+ --bg-elevated: 255 255 255;
42
+ --surface-raised: 255 255 255;
43
+ --popover: 255 255 255;
44
+ --bg-input: 255 255 255;
45
+ --bg-card-hover: 250 251 252;
46
+ --bg-input-hover: 250 251 252;
47
+ --muted: 246 247 248;
48
+ --canvas-placement-bg: 246 247 248;
49
+
50
+ /* ── Ink ──────────────────────────────────────────────────── */
51
+ /* Navy-tinted, not black. Vertex uses #1c274c 6,522 times — far too
52
+ many for text: it is a panel colour, so it gets a full ramp. */
53
+ --ink-15: 253 253 253;
54
+ --ink-25: 250 251 254;
55
+ --ink-30: 247 248 252;
56
+ --ink-40: 241 245 253;
57
+ --ink-60: 235 241 254;
58
+ --ink-50: 243 247 255;
59
+ --ink-100: 229 237 254;
60
+ --ink-200: 207 217 238;
61
+ --ink-300: 182 193 217;
62
+ --ink-400: 150 163 191;
63
+ --ink-500: 119 134 165;
64
+ --ink-600: 93 108 140;
65
+ --ink-700: 70 82 109;
66
+ --ink-800: 48 58 80;
67
+ --ink-900: 27 35 51;
68
+ --ink-950: 11 16 28;
69
+ --foreground: 11 16 28;
70
+ --card-foreground: 11 16 28;
71
+ --popover-foreground: 11 16 28;
72
+ --text-secondary: 93 108 140;
73
+ --muted-foreground: 93 108 140;
74
+ --text-tertiary: 119 134 165;
75
+ --text-muted: 150 163 191;
76
+ --secondary: 246 247 248;
77
+ --secondary-foreground: 11 16 28;
78
+
79
+ /* ── Borders ──────────────────────────────────────────────── */
80
+ /* Structure only. Depth is shadow — a hard hairline on a depth
81
+ surface reads as a form field. Inputs keep theirs deliberately. */
82
+ --border: 0 0 0 / 0.05;
83
+ --border-strong: 0 0 0 / 0.10;
84
+ --border-card: 0 0 0 / 0.06;
85
+ --border-input: 0 0 0 / 0.12;
86
+ --main-content-border: 0 0 0 / 0.06;
87
+ --input: 246 247 248;
88
+
89
+ /* ── Brand ────────────────────────────────────────────────── */
90
+ /* --accent is the AA-safe action red (4.77:1 with white). The bright
91
+ step is 4.01:1 and cannot carry a white label, so it is exposed
92
+ separately for accents, marks, charts and active fills. */
93
+ --accent: 221 40 34;
94
+ --accent-bright: 228 70 58;
95
+ --accent-hover: 201 17 17;
96
+ --accent-muted: 228 70 58 / 0.10;
97
+ --accent-subtle: 228 70 58 / 0.05;
98
+ --accent-glow: 228 70 58 / 0.22;
99
+ --primary: 221 40 34;
100
+ --primary-hover: 201 17 17;
101
+ --primary-foreground: 255 255 255;
102
+ --ring: 221 40 34;
103
+ --brand-15: 253 253 253;
104
+ --brand-25: 252 251 250;
105
+ --brand-30: 252 247 246;
106
+ --brand-40: 255 242 240;
107
+ --brand-60: 254 237 234;
108
+ --brand-50: 255 244 242;
109
+ --brand-100: 253 231 228;
110
+ --brand-200: 253 204 196;
111
+ --brand-300: 254 166 154;
112
+ --brand-400: 251 114 99;
113
+ --brand-500: 228 70 58;
114
+ --brand-600: 201 17 17;
115
+ --brand-700: 157 8 9;
116
+ --brand-800: 114 4 4;
117
+ --brand-900: 72 2 2;
118
+ --brand-950: 41 0 0;
119
+
120
+ /* ── Semantic — REASSIGNED ────────────────────────────────── */
121
+ --success: 13 128 75;
122
+ --success-bg: 6 160 93 / 0.12;
123
+ --warning: 100 80 1;
124
+ --warning-bg: 161 131 18 / 0.14;
125
+ /* Amber. The collision between a red brand and a red danger state
126
+ cannot be solved by moving the hue — every credible danger red
127
+ sits within a few degrees of the brand. Moving the MEANING can. */
128
+ --destructive: 115 71 9;
129
+ --destructive-bg: 187 116 2 / 0.14;
130
+ --destructive-foreground: 255 255 255;
131
+ --info: 130 62 210;
132
+ --info-bg: 155 97 235 / 0.12;
133
+
134
+ /* ── Categorical series ───────────────────────────────────── */
135
+ /* Constant lightness so no series outranks another; chroma tuned per
136
+ hue so each is equally vivid rather than equally saturated. */
137
+ --series-1: 226 74 62;
138
+ --series-2: 153 100 229;
139
+ --series-3: 9 160 93;
140
+ --series-4: 186 116 4;
141
+ --series-5: 2 137 234;
142
+ --series-6: 14 154 148;
143
+ --series-7: 212 73 150;
144
+ --series-8: 113 149 3;
145
+
146
+ /* ── Elevation ────────────────────────────────────────────── */
147
+ --shadow-sm: 0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px -1px oklch(0 0 0 / 0.06), 0 2px 4px 0 oklch(0 0 0 / 0.04);
148
+ --shadow-md: 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 4px -2px oklch(0 0 0 / 0.08), 0 4px 10px -2px oklch(0 0 0 / 0.06);
149
+ --shadow-lg: 0 0 0 1px oklch(0 0 0 / 0.06), 0 6px 16px -4px oklch(0 0 0 / 0.12);
150
+ --shadow-xl: 0 0 0 1px oklch(0 0 0 / 0.06), 0 12px 32px -8px oklch(0 0 0 / 0.18);
151
+ --shadow-2xl: 0 0 0 1px oklch(0 0 0 / 0.06), 0 24px 56px -12px oklch(0 0 0 / 0.24);
152
+ --shadow-glow: 0 0 20px oklch(0 0 0 / 0);
153
+ --card-shadow-rest: 0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px -1px oklch(0 0 0 / 0.06), 0 2px 4px 0 oklch(0 0 0 / 0.04), inset 0 1px 0 0 oklch(1 0 0 / 0.9);
154
+ --card-shadow-hover: 0 0 0 1px oklch(0 0 0 / 0.08), 0 2px 4px -1px oklch(0 0 0 / 0.08), 0 6px 14px -2px oklch(0 0 0 / 0.08), inset 0 1px 0 0 oklch(1 0 0 / 0.9);
155
+ --inset-highlight: 255 255 255 / 0.9;
156
+ --modal-overlay: 0 0 0 / 0.32;
157
+ /* Solid, matching the base theme — a 3px translucent halo measures ~1.4:1
158
+ against its surface, under the 3:1 WCAG 2.2 SC 2.4.11 asks of a focus
159
+ indicator. See the note on --ring-focus in tokens.css. */
160
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
161
+ --ring-error: 0 0 0 2px rgb(var(--destructive));
162
+ }
163
+
164
+ /*
165
+ * Dark. Derived by SELECTING different steps from the same OKLCH ramps,
166
+ * not by inverting the light values — mechanical inversion is what
167
+ * produces muddy dark modes. The ink ramp supplies the surfaces here,
168
+ * which is the same navy that supplies the text in light.
169
+ */
170
+ [data-ds="v2"][data-theme="dark"] {
171
+ --page-bg: 11 16 28;
172
+ --background: 11 16 28;
173
+ --main-content-bg: 11 16 28;
174
+ --bg-surface: 27 35 51;
175
+ --surface-frame: 27 35 51;
176
+ --card-frame-bg: 27 35 51;
177
+ --bg-card: 48 58 80;
178
+ --card: 48 58 80;
179
+ --tray-bg: 48 58 80;
180
+ --bg-elevated: 70 82 109;
181
+ --surface-raised: 70 82 109;
182
+ --popover: 70 82 109;
183
+ --bg-input: 27 35 51;
184
+ --bg-card-hover: 70 82 109;
185
+ --bg-input-hover: 48 58 80;
186
+ --muted: 48 58 80;
187
+ --canvas-placement-bg: 27 35 51;
188
+ --foreground: 250 251 252;
189
+ --card-foreground: 250 251 252;
190
+ --popover-foreground: 250 251 252;
191
+ --text-secondary: 192 193 195;
192
+ --muted-foreground: 192 193 195;
193
+ --text-tertiary: 162 163 165;
194
+ --text-muted: 133 134 136;
195
+ --secondary: 48 58 80;
196
+ --secondary-foreground: 250 251 252;
197
+ --border: 255 255 255 / 0.08;
198
+ --border-strong: 255 255 255 / 0.14;
199
+ --border-card: 255 255 255 / 0.08;
200
+ --border-input: 255 255 255 / 0.14;
201
+ --main-content-border: 255 255 255 / 0.08;
202
+ /* Accents brighten in dark — the light-mode steps go muddy on a dark
203
+ surface because the surrounding lightness has moved. */
204
+ --accent: 228 70 58;
205
+ --accent-bright: 251 114 99;
206
+ --accent-hover: 251 114 99;
207
+ --primary: 228 70 58;
208
+ --primary-hover: 251 114 99;
209
+ --success: 80 188 126;
210
+ --warning: 190 160 64;
211
+ --destructive: 218 145 55;
212
+ --info: 181 134 254;
213
+ --success-bg: 6 160 93 / 0.16;
214
+ --warning-bg: 161 131 18 / 0.16;
215
+ --destructive-bg: 187 116 2 / 0.16;
216
+ --info-bg: 155 97 235 / 0.16;
217
+ /* Layered depth shadows are invisible on dark; a single ring reads. */
218
+ --shadow-sm: 0 0 0 1px oklch(1 0 0 / 0.08);
219
+ --shadow-md: 0 0 0 1px oklch(1 0 0 / 0.08), 0 4px 12px oklch(0 0 0 / 0.4);
220
+ --shadow-lg: 0 0 0 1px oklch(1 0 0 / 0.08), 0 8px 24px oklch(0 0 0 / 0.5);
221
+ --shadow-xl: 0 0 0 1px oklch(1 0 0 / 0.10), 0 16px 40px oklch(0 0 0 / 0.55);
222
+ --shadow-2xl: 0 0 0 1px oklch(1 0 0 / 0.10), 0 24px 60px oklch(0 0 0 / 0.6);
223
+ --card-shadow-rest: 0 0 0 1px oklch(1 0 0 / 0.08);
224
+ --card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
225
+ --inset-highlight: 255 255 255 / 0.05;
226
+ --modal-overlay: 0 0 0 / 0.7;
227
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
228
+ }
package/dist/tokens.css CHANGED
@@ -49,6 +49,9 @@
49
49
  * colored backgrounds — switches,
50
50
  * sliders, badges-on-accent)
51
51
  * --muted (muted fill for ghost UI)
52
+ * --surface-subtle (lightest structural tint — header bands
53
+ * and zebra on an already-bordered
54
+ * surface; 1.04:1, matched across themes)
52
55
  * --header-height (sticky AppHeader height — referenced
53
56
  * by sticky chip bars; same value lives
54
57
  * in src/lib/layout.ts for JS callsites)
@@ -92,6 +95,49 @@
92
95
  --accent-muted: 207 14 15 / 0.12;
93
96
  --accent-subtle: 207 14 15 / 0.06;
94
97
  --accent-glow: 207 14 15 / 0.25;
98
+ /* Text-selection highlight. PUBLIC, and a full colour (not a triple) so
99
+ `::selection { background: var(--selection-bg) }` is valid at :root.
100
+ `@lovett/shell` ships that exact global rule while defining the
101
+ variable only inside `.cs-frame`; unset at the root, `background`
102
+ computed to transparent and every selection in the app was invisible
103
+ (ADR-145 amendment, 2026-09-04). Neutral blue like a native selection,
104
+ not the accent — the accent earns impact from scarcity. */
105
+ --selection-bg: rgb(var(--info) / 0.34);
106
+ /* Ink ON an accent fill. PUBLIC, and theme-independent because --accent is:
107
+ the brand red does not change between light and dark, so its foreground
108
+ must not either. Measures 5.64:1 on --accent.
109
+ Added 2026-08-29 — needs a token ADR (CLAUDE.md §4). It closes a real gap:
110
+ app code had three different answers for "text on accent" (a `text-white`
111
+ literal, the INTERNAL --primary-foreground, and a reference to this very
112
+ name with an inline fallback), and one of them — `rgb(var(--foreground))`
113
+ — inverted to near-black on red at 3.34:1 in light mode. The public
114
+ --accent family had every value EXCEPT its foreground, so lens code had
115
+ nothing correct to reach for. */
116
+ --accent-foreground: 255 255 255;
117
+ /* Accent as TEXT on a normal surface — a different problem from
118
+ --accent-foreground (ink ON an accent fill). PUBLIC, and unlike --accent it
119
+ IS theme-tuned: the brand red measures 5.64:1 on a light card but only
120
+ 3.05:1 on a dark one, so an accent-coloured LABEL is legible in light and
121
+ fails AA in dark. The dark value is the lifted red that clears it (4.57:1).
122
+ Use for selected-state labels, accent links, accent metric values — never
123
+ as a fill. */
124
+ --accent-ink: 239 68 68;
125
+
126
+ /* SEMANTIC INK — PUBLIC. Same problem as --accent-ink, same shape of answer.
127
+ `--success` / `--warning` are sized to work as FILLS and as icon strokes;
128
+ used as TEXT ON THEIR OWN 10-12% TINT they are only legible in one theme.
129
+ Measured over the card: success reads 5.64:1 dark but 3.33:1 light, and
130
+ warning 6.46:1 dark but 2.86:1 light — both fail AA for body text in light
131
+ mode, which is exactly where the generate lens's tip panels and word-count
132
+ chips live. Dark keeps the base colour; light gets a darkened ink that
133
+ clears 4.5:1 while still reading green and amber rather than near-black.
134
+ Text only — never a fill, and never a border. */
135
+ --success-ink: var(--success);
136
+ --warning-ink: var(--warning);
137
+ /* Destructive is the one that fails in BOTH themes as text on its own tint
138
+ (3.53:1 light, 3.67:1 dark), so unlike the other two the dark value is not
139
+ the base — it is lifted 18% toward white to clear 5.11:1 / 4.51:1. */
140
+ --destructive-ink: 242 102 102;
95
141
 
96
142
  /* ============================================================
97
143
  HIERARCHY MAP PALETTE — PUBLIC, theme-INDEPENDENT.
@@ -167,6 +213,17 @@
167
213
  --folder-slate: 148 163 184;
168
214
  --folder-ink: 203 213 225;
169
215
 
216
+ /* OPAQUE categorical tints — the chip/pill FILL for a --folder-* hue.
217
+ Pre-composited (hue at 0.12 over --surface-card) so a chip needs no
218
+ alpha and therefore does not change appearance when the surface
219
+ under it changes. Only the two hues with consumers are defined;
220
+ extend per hue as consumers land rather than all nine up front.
221
+ NOTE: the hue itself is a FILL/GRAPHIC value, not an ink — e.g.
222
+ --folder-amber on its own tint measures 2.86:1 in light. Chip TEXT
223
+ is --foreground; the tint + border carry the category. */
224
+ --folder-amber-tint: 53 43 28;
225
+ --folder-blue-tint: 31 39 56;
226
+
170
227
  /* ============================================================
171
228
  SIDEBAR — INTERNAL (only <Sidebar/> reads these)
172
229
  ============================================================ */
@@ -298,8 +355,18 @@
298
355
  --shadow-xl: 0 16px 40px rgb(0 0 0 / 0.55);
299
356
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.60);
300
357
 
301
- --ring-focus: 0 0 0 3px rgb(207 14 15 / 0.25);
302
- --ring-error: 0 0 0 3px rgb(239 68 68 / 0.25);
358
+ /* Thin and SOLID, not a soft halo and this is a legibility fix as much as
359
+ a look. The old 3px @ 0.25/0.20 composited to rgb(245 207 207) on a light
360
+ card and rgb(63 24 27) on a dark one: 1.43:1 and 1.11:1 against the surface
361
+ it sits on, against the 3:1 that WCAG 2.2 SC 2.4.11 (Focus Appearance)
362
+ asks of a focus indicator. Alpha was measured across the range and NOTHING
363
+ below fully-opaque clears 3:1 in dark mode, so the ring is solid: 5.64:1
364
+ light, 3.05:1 dark. 2px keeps the >=2px perimeter the same SC requires
365
+ while reading crisper than the 3px glow it replaces.
366
+ Both resolve through the theme-aware tokens, so the light block needs no
367
+ override — var() resolves at use time. */
368
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
369
+ --ring-error: 0 0 0 2px rgb(var(--destructive));
303
370
 
304
371
  --radius-xs: 6px;
305
372
 
@@ -339,6 +406,18 @@
339
406
  --surface-card: 27 27 30;
340
407
  --surface-inset: 45 45 48;
341
408
  --surface-hover: 55 55 59;
409
+ /* THE LIGHTEST STRUCTURAL TINT — PUBLIC. A header band or zebra stripe on a
410
+ surface that ALREADY HAS A BORDER, where the fill only has to hint that a
411
+ row is chrome rather than content.
412
+ Measured: the border alone is 1.25:1 in dark and 1.32:1 in light, stronger
413
+ than any fill step in the neutral ramp — so the fill is decoration on top
414
+ of structure that already reads, and it can go very light indeed.
415
+ Sized by OKLCH lightness so the two themes are the SAME design: ΔL 0.017
416
+ dark, 0.014 light (1.045:1 / 1.042:1). That symmetry is the point. Using
417
+ --muted for this job gave ΔL 0.075 in dark against 0.038 in light — twice
418
+ the step in one theme, which is why a header that looked right in dark read
419
+ as a grey slab in light. */
420
+ --surface-subtle: 31 31 34;
342
421
  /* A control you can act on — input, select, option tile, chip-button.
343
422
  Sits at CARD level on purpose: in light that is white against the
344
423
  frame behind it, which reads "type here"; in dark it is raised above
@@ -389,6 +468,21 @@
389
468
  [data-theme="light"], [data-theme="combo"] {
390
469
  /* Brand accent — base stays, hover darkens, glow softens */
391
470
  --accent-hover: 178 8 9;
471
+ --selection-bg: rgb(var(--info) / 0.24);
472
+ /* Brand red passes on a light card (5.64:1) — see the :root note. */
473
+ --accent-ink: var(--accent);
474
+ /* Solved against the DARKEST tint these inks land on, not the lightest.
475
+ The lens mixes success at both 10% (tip panels) and 20% (score chips); an
476
+ ink tuned only to the 10% tint cleared 4.61:1 there and still failed at
477
+ 4.09:1 on the 20% chip. This value clears BOTH — 5.12:1 and 4.54:1 — and
478
+ at 77% of --success it is still unmistakably green. */
479
+ --success-ink: 4 116 81;
480
+ /* Same reasoning as --success-ink: solved against the 20% tint as well as
481
+ the 10% one (5.03:1 / 4.52:1, up from 2.86:1 / 4.05:1). 72% of --warning,
482
+ still unmistakably amber. */
483
+ --warning-ink: 156 86 4;
484
+ /* 5.28:1 / 4.51:1 on the 10% and 20% tints, up from 3.53:1. */
485
+ --destructive-ink: 189 33 33;
392
486
  --accent-muted: 207 14 15 / 0.10;
393
487
  --accent-subtle: 207 14 15 / 0.05;
394
488
  --accent-glow: 207 14 15 / 0.20;
@@ -406,9 +500,28 @@
406
500
  --card-foreground: 17 17 19;
407
501
  --popover-foreground: 17 17 19;
408
502
  --muted-foreground: 82 82 91;
409
- --text-secondary: 82 82 91;
410
- /* --text-tertiary stays from :root */
411
- /* Lifted 161->137 was 2.56:1 on a white card. */
503
+ /* Byte-identical to --muted-foreground in both themes since they were
504
+ introduced. Aliased rather than duplicated so the two names cannot
505
+ drift apart; prefer --muted-foreground in new code. */
506
+ --text-secondary: var(--muted-foreground);
507
+ /* Was "stays from :root" — but :root IS the dark theme, so light
508
+ inherited the dark-tuned 132 132 141, which measures 3.71:1 on a
509
+ 255 card and fails AA for the 13px body text that consumes it.
510
+ Re-picked by measurement, not by eye: 110 110 119 is the first rung
511
+ with headroom on every light surface it can land on —
512
+ 5.05:1 on --surface-card/page (255), 4.72:1 on --surface-frame (247),
513
+ 4.52:1 on --surface-inset (242). Keeps the ramp's 286° cool cast.
514
+ Side effect: it also repairs the ink ramp's shape. The tiers were
515
+ L 0.442 / 0.616 / 0.633 — a wide gap then a collision, tertiary and
516
+ muted less than half a surface-step apart. They are now
517
+ 0.442 / 0.541 / 0.633: three evenly-spread rungs. */
518
+ --text-tertiary: 110 110 119;
519
+ /* Lifted 161->137 — was 2.56:1 on a white card. Still only 3.47:1 on
520
+ 255, i.e. it clears the 3:1 non-text floor and NOT the 4.5:1 text
521
+ floor. That is deliberate: --text-muted is the quietest rung and is
522
+ for NON-TEXT and DISABLED chrome only — quiet icons, chevrons,
523
+ disabled labels. Real text one tier up (--text-tertiary) or higher.
524
+ Lifting this to pass 4.5:1 would collapse it into --text-tertiary. */
412
525
  --text-muted: 137 137 146;
413
526
  --secondary-foreground: 17 17 19;
414
527
 
@@ -446,9 +559,6 @@
446
559
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.16);
447
560
  --shadow-glow: 0 0 20px rgb(207 14 15 / 0.20);
448
561
 
449
- --ring-focus: 0 0 0 3px rgb(207 14 15 / 0.20);
450
- --ring-error: 0 0 0 3px rgb(220 38 38 / 0.18);
451
-
452
562
  /* Match --page-bg (254/254/254). Was 241/245/249 (slate) — that's
453
563
  * what was actually painting "the steely background" the owner
454
564
  * flagged; --page-bg is the body, but MainLayout's content area
@@ -508,6 +618,8 @@
508
618
  --surface-frame: 247 247 249;
509
619
  --surface-inset: 242 242 244;
510
620
  --surface-hover: 238 238 240;
621
+ /* 1.042:1 against the card — a band, not a slab. See the :root note. */
622
+ --surface-subtle: 250 250 252;
511
623
  /* A control you can act on — input, select, option tile, chip-button.
512
624
  Sits at CARD level on purpose: in light that is white against the
513
625
  frame behind it, which reads "type here"; in dark it is raised above
@@ -606,6 +718,9 @@
606
718
  --folder-pink: 219 39 119;
607
719
  --folder-slate: 71 85 105;
608
720
  --folder-ink: 15 23 42;
721
+ /* Opaque categorical tints — hue at 0.10 over the 255 light card. */
722
+ --folder-amber-tint: 251 241 230;
723
+ --folder-blue-tint: 232 237 251;
609
724
  }
610
725
 
611
726
  /* ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovett/ui",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Design system primitives, tokens, layouts, and patterns for the Lovett portfolio.",