@lovett/ui 0.0.9 → 0.0.10

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/src/theme-v2.css CHANGED
@@ -1,46 +1,7 @@
1
1
  /*
2
2
  * @lovett/ui — v2 theme, SCOPED.
3
3
  *
4
- * ─────────────────────────────────────────────────────────────────────
5
- * SUPERSEDED FOR NEW WORK by `src/v2/theme.css`.
6
- *
7
- * That file is the OKLCH design layer: a namespaced `--lv-*` token set,
8
- * an explicit surface ladder, elevation as shadow + inner highlight, and
9
- * both appearances driven by `prefers-color-scheme` AND `data-theme`.
10
- * New surfaces should import it and use `@lovett/ui/v2`.
11
- *
12
- * ── THAT DIRECTIVE IS NO LONGER TRUE (2026-08-20) ─────────────────────────
13
- * `@lovett/ui/v2` is NOT EXPORTED any more. The `./v2`, `./v2/theme.css` and
14
- * `./v2/__demo__/showcase` entries were removed from `package.json` under
15
- * ADR-CE-0027 D6 step 5, on the owner's decision, after a search that reaches
16
- * gitignored trees confirmed **zero consumers family-wide**. It cleared four
17
- * name collisions (`EmptyState`, `EmptyStateProps`, `Panel`, `Toolbar`).
18
- *
19
- * NO SOURCE WAS DELETED. `src/v2/` is intact and the removal is three lines of
20
- * `exports` to restore, which is why it was done this way rather than by
21
- * deleting components.
22
- *
23
- * THIS FILE STAYS AND IS STILL LIVE — two ignite worktrees import it as
24
- * `@lovett/ui/theme-v2.css` in their `index.css`. Do not confuse it with
25
- * `src/v2/theme.css`, which is a different, larger file and is the one that
26
- * went unexported.
27
- *
28
- * Consequence to know about: the v2 BRIDGE below (section 3) maps `--lv-*` so
29
- * v2 primitives render in a `data-ds="v2"` subtree. With no public `./v2`
30
- * entry, nothing can import those primitives, so the bridge is inert. It is
31
- * left in place because it is harmless, because removing it is the source
32
- * surgery this change deliberately avoided, and because it becomes live again
33
- * the moment the exports are restored.
34
- *
35
- * This file stays because live consumers depend on it. Everything ABOVE
36
- * the "APPENDED" marker near the bottom is untouched and still generated
37
- * — every existing declaration resolves to exactly the value it always
38
- * did. The appended region is hand-authored and additive: it bridges
39
- * this scope onto the v2 token names and adds the automatic-dark support
40
- * this file never had. If you re-run the generator, re-apply that region.
41
- * ─────────────────────────────────────────────────────────────────────
42
- *
43
- * GENERATED (region above the marker). Regenerate with:
4
+ * GENERATED. Regenerate with:
44
5
  * python3 scripts/figma/emit-theme.py <corpus> --out packages/ui/src/theme-v2.css
45
6
  *
46
7
  * Opt in by putting `data-ds="v2"` on any element. Everything inside it
@@ -193,8 +154,11 @@
193
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);
194
155
  --inset-highlight: 255 255 255 / 0.9;
195
156
  --modal-overlay: 0 0 0 / 0.32;
196
- --ring-focus: 0 0 0 3px rgb(221 40 34 / 0.22);
197
- --ring-error: 0 0 0 3px rgb(151 93 1 / 0.22);
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));
198
162
  }
199
163
 
200
164
  /*
@@ -260,207 +224,5 @@
260
224
  --card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
261
225
  --inset-highlight: 255 255 255 / 0.05;
262
226
  --modal-overlay: 0 0 0 / 0.7;
263
- --ring-focus: 0 0 0 3px rgb(228 70 58 / 0.3);
264
- }
265
-
266
- /* ══════════════════════════════════════════════════════════════════════
267
- ═══════════════════════════ APPENDED ═══════════════════════════════
268
- Hand-authored, additive. Nothing above this line was changed.
269
- Three things this region adds:
270
-
271
- 1. AUTOMATIC DARK. The generated file only goes dark on an explicit
272
- `data-theme="dark"`. A surface that wants to follow the OS opts
273
- in with `data-theme="auto"`; the block below is a verbatim copy
274
- of the explicit-dark map, because a media query and an attribute
275
- selector cannot share a rule. Keep the two in sync.
276
-
277
- 2. THE SURFACE LADDER, as first-class names. The generated file
278
- encodes the ladder implicitly across a dozen aliases
279
- (--page-bg / --bg-surface / --bg-card / --tray-bg ...). These
280
- five names say what the tier IS, which is what a component
281
- should be reading.
282
-
283
- 3. A BRIDGE to the v2 `--lv-*` tokens, so `@lovett/ui/v2`
284
- primitives render correctly inside an existing `data-ds="v2"`
285
- subtree without also loading `src/v2/theme.css`. Migration can
286
- then happen one component at a time.
287
- ══════════════════════════════════════════════════════════════════════ */
288
-
289
- /* ── 1. Automatic dark (opt-in via data-theme="auto") ─────────────── */
290
-
291
- @media (prefers-color-scheme: dark) {
292
- [data-ds='v2'][data-theme='auto'] {
293
- color-scheme: dark;
294
-
295
- --page-bg: 11 16 28;
296
- --background: 11 16 28;
297
- --main-content-bg: 11 16 28;
298
- --bg-surface: 27 35 51;
299
- --surface-frame: 27 35 51;
300
- --card-frame-bg: 27 35 51;
301
- --bg-card: 48 58 80;
302
- --card: 48 58 80;
303
- --tray-bg: 48 58 80;
304
- --bg-elevated: 70 82 109;
305
- --surface-raised: 70 82 109;
306
- --popover: 70 82 109;
307
- --bg-input: 27 35 51;
308
- --bg-card-hover: 70 82 109;
309
- --bg-input-hover: 48 58 80;
310
- --muted: 48 58 80;
311
- --canvas-placement-bg: 27 35 51;
312
- --foreground: 250 251 252;
313
- --card-foreground: 250 251 252;
314
- --popover-foreground: 250 251 252;
315
- --text-secondary: 192 193 195;
316
- --muted-foreground: 192 193 195;
317
- --text-tertiary: 162 163 165;
318
- --text-muted: 133 134 136;
319
- --secondary: 48 58 80;
320
- --secondary-foreground: 250 251 252;
321
- --border: 255 255 255 / 0.08;
322
- --border-strong: 255 255 255 / 0.14;
323
- --border-card: 255 255 255 / 0.08;
324
- --border-input: 255 255 255 / 0.14;
325
- --main-content-border: 255 255 255 / 0.08;
326
- --accent: 228 70 58;
327
- --accent-bright: 251 114 99;
328
- --accent-hover: 251 114 99;
329
- --primary: 228 70 58;
330
- --primary-hover: 251 114 99;
331
- --success: 80 188 126;
332
- --warning: 190 160 64;
333
- --destructive: 218 145 55;
334
- --info: 181 134 254;
335
- --success-bg: 6 160 93 / 0.16;
336
- --warning-bg: 161 131 18 / 0.16;
337
- --destructive-bg: 187 116 2 / 0.16;
338
- --info-bg: 155 97 235 / 0.16;
339
- --shadow-sm: 0 0 0 1px oklch(1 0 0 / 0.08);
340
- --shadow-md: 0 0 0 1px oklch(1 0 0 / 0.08), 0 4px 12px oklch(0 0 0 / 0.4);
341
- --shadow-lg: 0 0 0 1px oklch(1 0 0 / 0.08), 0 8px 24px oklch(0 0 0 / 0.5);
342
- --shadow-xl: 0 0 0 1px oklch(1 0 0 / 0.1), 0 16px 40px oklch(0 0 0 / 0.55);
343
- --shadow-2xl: 0 0 0 1px oklch(1 0 0 / 0.1), 0 24px 60px oklch(0 0 0 / 0.6);
344
- --card-shadow-rest: 0 0 0 1px oklch(1 0 0 / 0.08);
345
- --card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
346
- --inset-highlight: 255 255 255 / 0.05;
347
- --modal-overlay: 0 0 0 / 0.7;
348
- --ring-focus: 0 0 0 3px rgb(228 70 58 / 0.3);
349
- }
350
- }
351
-
352
- /* `data-theme` must win in BOTH directions: forced light under a dark OS
353
- is as much a requirement as forced dark under a light one. The two
354
- explicit values are already unconditional rules, so they beat the
355
- media query above by source order; this only declares the native
356
- `color-scheme` so form controls and scrollbars agree. */
357
- [data-ds='v2'][data-theme='light'] {
358
- color-scheme: light;
359
- }
360
- [data-ds='v2'][data-theme='dark'] {
361
- color-scheme: dark;
362
- }
363
-
364
- /* ── 2. The surface ladder, named ─────────────────────────────────── */
365
- /* page → shell → section → card, each one notch brighter; `inset` goes
366
- the other way, because a recessed panel inside a card is the system's
367
- strongest single move and it only reads as recessed if it is darker.
368
- Aliases, not new values — these resolve to what the tiers already are. */
369
-
370
- [data-ds='v2'] {
371
- --surface-page: rgb(var(--page-bg));
372
- --surface-shell: rgb(var(--bg-surface));
373
- --surface-section: rgb(var(--muted));
374
- --surface-card: rgb(var(--bg-card));
375
- --surface-inset: rgb(var(--canvas-placement-bg));
376
- }
377
-
378
- @media (prefers-color-scheme: dark) {
379
- [data-ds='v2'][data-theme='auto'] {
380
- --surface-section: rgb(27 35 51);
381
- --surface-inset: rgb(27 35 51);
382
- }
383
- }
384
-
385
- [data-ds='v2'][data-theme='dark'] {
386
- --surface-section: rgb(27 35 51);
387
- --surface-inset: rgb(27 35 51);
388
- }
389
-
390
- /* ── 3. Bridge to the v2 `--lv-*` tokens ──────────────────────────── */
391
- /* Lets `@lovett/ui/v2` primitives render inside this scope. Only the
392
- tokens v2 components actually read are mapped; the scales (space,
393
- radius, type, motion) are appearance-independent and are defined
394
- unconditionally in `src/v2/theme.css`, which must still be imported
395
- for those. This block covers colour and depth only — the parts that
396
- have to agree with whichever theme the subtree is on. */
397
-
398
- [data-ds='v2'] {
399
- --lv-surface-page: var(--surface-page);
400
- --lv-surface-shell: var(--surface-shell);
401
- --lv-surface-section: var(--surface-section);
402
- --lv-surface-card: var(--surface-card);
403
- --lv-surface-inset: var(--surface-inset);
404
- --lv-surface-raised: rgb(var(--surface-raised));
405
- --lv-surface-overlay: rgb(var(--modal-overlay));
406
- --lv-surface-hover: rgb(var(--bg-card-hover));
407
- --lv-surface-active: rgb(var(--border-strong));
408
- --lv-surface-selected: rgb(var(--accent-muted));
409
-
410
- --lv-text-primary: rgb(var(--foreground));
411
- --lv-text-secondary: rgb(var(--text-secondary));
412
- --lv-text-tertiary: rgb(var(--text-tertiary));
413
- --lv-text-faint: rgb(var(--text-muted));
414
- --lv-text-disabled: rgb(var(--text-muted));
415
- --lv-text-on-accent: rgb(var(--primary-foreground));
416
-
417
- --lv-hairline: rgb(var(--border));
418
- --lv-hairline-strong: rgb(var(--border-strong));
419
- --lv-border-field: rgb(var(--border-input));
420
-
421
- --lv-accent: rgb(var(--accent));
422
- --lv-accent-hover: rgb(var(--accent-hover));
423
- --lv-accent-active: rgb(var(--accent-hover));
424
- --lv-accent-text: rgb(var(--accent));
425
- --lv-accent-mark: rgb(var(--accent-bright));
426
- --lv-accent-soft: rgb(var(--accent-muted));
427
- --lv-accent-rule: rgb(var(--accent-glow));
428
- --lv-focus: rgb(var(--ring));
429
-
430
- /* The generated palette's semantic reassignment carries straight over:
431
- success green, warning gold, destructive amber, info violet. v2's
432
- own danger hue (48°) was chosen for the same reason this file moved
433
- destructive off red — the brand is red, and two reds on one page
434
- cannot be told apart. */
435
- --lv-success-text: rgb(var(--success));
436
- --lv-success-mark: rgb(var(--success));
437
- --lv-success-soft: rgb(var(--success-bg));
438
- --lv-success-rule: rgb(var(--success));
439
-
440
- --lv-warning-text: rgb(var(--warning));
441
- --lv-warning-mark: rgb(var(--warning));
442
- --lv-warning-soft: rgb(var(--warning-bg));
443
- --lv-warning-rule: rgb(var(--warning));
444
-
445
- --lv-danger-text: rgb(var(--destructive));
446
- --lv-danger-mark: rgb(var(--destructive));
447
- --lv-danger-soft: rgb(var(--destructive-bg));
448
- --lv-danger-rule: rgb(var(--destructive));
449
-
450
- --lv-info-text: rgb(var(--info));
451
- --lv-info-mark: rgb(var(--info));
452
- --lv-info-soft: rgb(var(--info-bg));
453
- --lv-info-rule: rgb(var(--info));
454
-
455
- --lv-highlight: inset 0 1px 0 0 rgb(var(--inset-highlight));
456
- --lv-elev-0: none;
457
- --lv-elev-1: var(--shadow-sm);
458
- --lv-elev-2: var(--shadow-md);
459
- --lv-elev-3: var(--shadow-lg);
460
- --lv-elev-4: var(--shadow-xl);
461
- --lv-elev-inset: inset 0 1px 2px 0 oklch(0 0 0 / 0.07),
462
- inset 0 0 0 1px rgb(var(--border));
463
-
464
- --lv-skeleton-base: rgb(var(--border-strong));
465
- --lv-skeleton-sheen: rgb(var(--border));
227
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
466
228
  }
package/src/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,41 @@
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
+ /* Ink ON an accent fill. PUBLIC, and theme-independent because --accent is:
99
+ the brand red does not change between light and dark, so its foreground
100
+ must not either. Measures 5.64:1 on --accent.
101
+ Added 2026-08-29 — needs a token ADR (CLAUDE.md §4). It closes a real gap:
102
+ app code had three different answers for "text on accent" (a `text-white`
103
+ literal, the INTERNAL --primary-foreground, and a reference to this very
104
+ name with an inline fallback), and one of them — `rgb(var(--foreground))`
105
+ — inverted to near-black on red at 3.34:1 in light mode. The public
106
+ --accent family had every value EXCEPT its foreground, so lens code had
107
+ nothing correct to reach for. */
108
+ --accent-foreground: 255 255 255;
109
+ /* Accent as TEXT on a normal surface — a different problem from
110
+ --accent-foreground (ink ON an accent fill). PUBLIC, and unlike --accent it
111
+ IS theme-tuned: the brand red measures 5.64:1 on a light card but only
112
+ 3.05:1 on a dark one, so an accent-coloured LABEL is legible in light and
113
+ fails AA in dark. The dark value is the lifted red that clears it (4.57:1).
114
+ Use for selected-state labels, accent links, accent metric values — never
115
+ as a fill. */
116
+ --accent-ink: 239 68 68;
117
+
118
+ /* SEMANTIC INK — PUBLIC. Same problem as --accent-ink, same shape of answer.
119
+ `--success` / `--warning` are sized to work as FILLS and as icon strokes;
120
+ used as TEXT ON THEIR OWN 10-12% TINT they are only legible in one theme.
121
+ Measured over the card: success reads 5.64:1 dark but 3.33:1 light, and
122
+ warning 6.46:1 dark but 2.86:1 light — both fail AA for body text in light
123
+ mode, which is exactly where the generate lens's tip panels and word-count
124
+ chips live. Dark keeps the base colour; light gets a darkened ink that
125
+ clears 4.5:1 while still reading green and amber rather than near-black.
126
+ Text only — never a fill, and never a border. */
127
+ --success-ink: var(--success);
128
+ --warning-ink: var(--warning);
129
+ /* Destructive is the one that fails in BOTH themes as text on its own tint
130
+ (3.53:1 light, 3.67:1 dark), so unlike the other two the dark value is not
131
+ the base — it is lifted 18% toward white to clear 5.11:1 / 4.51:1. */
132
+ --destructive-ink: 242 102 102;
95
133
 
96
134
  /* ============================================================
97
135
  HIERARCHY MAP PALETTE — PUBLIC, theme-INDEPENDENT.
@@ -167,6 +205,17 @@
167
205
  --folder-slate: 148 163 184;
168
206
  --folder-ink: 203 213 225;
169
207
 
208
+ /* OPAQUE categorical tints — the chip/pill FILL for a --folder-* hue.
209
+ Pre-composited (hue at 0.12 over --surface-card) so a chip needs no
210
+ alpha and therefore does not change appearance when the surface
211
+ under it changes. Only the two hues with consumers are defined;
212
+ extend per hue as consumers land rather than all nine up front.
213
+ NOTE: the hue itself is a FILL/GRAPHIC value, not an ink — e.g.
214
+ --folder-amber on its own tint measures 2.86:1 in light. Chip TEXT
215
+ is --foreground; the tint + border carry the category. */
216
+ --folder-amber-tint: 53 43 28;
217
+ --folder-blue-tint: 31 39 56;
218
+
170
219
  /* ============================================================
171
220
  SIDEBAR — INTERNAL (only <Sidebar/> reads these)
172
221
  ============================================================ */
@@ -298,8 +347,18 @@
298
347
  --shadow-xl: 0 16px 40px rgb(0 0 0 / 0.55);
299
348
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.60);
300
349
 
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);
350
+ /* Thin and SOLID, not a soft halo and this is a legibility fix as much as
351
+ a look. The old 3px @ 0.25/0.20 composited to rgb(245 207 207) on a light
352
+ card and rgb(63 24 27) on a dark one: 1.43:1 and 1.11:1 against the surface
353
+ it sits on, against the 3:1 that WCAG 2.2 SC 2.4.11 (Focus Appearance)
354
+ asks of a focus indicator. Alpha was measured across the range and NOTHING
355
+ below fully-opaque clears 3:1 in dark mode, so the ring is solid: 5.64:1
356
+ light, 3.05:1 dark. 2px keeps the >=2px perimeter the same SC requires
357
+ while reading crisper than the 3px glow it replaces.
358
+ Both resolve through the theme-aware tokens, so the light block needs no
359
+ override — var() resolves at use time. */
360
+ --ring-focus: 0 0 0 2px rgb(var(--accent));
361
+ --ring-error: 0 0 0 2px rgb(var(--destructive));
303
362
 
304
363
  --radius-xs: 6px;
305
364
 
@@ -339,6 +398,18 @@
339
398
  --surface-card: 27 27 30;
340
399
  --surface-inset: 45 45 48;
341
400
  --surface-hover: 55 55 59;
401
+ /* THE LIGHTEST STRUCTURAL TINT — PUBLIC. A header band or zebra stripe on a
402
+ surface that ALREADY HAS A BORDER, where the fill only has to hint that a
403
+ row is chrome rather than content.
404
+ Measured: the border alone is 1.25:1 in dark and 1.32:1 in light, stronger
405
+ than any fill step in the neutral ramp — so the fill is decoration on top
406
+ of structure that already reads, and it can go very light indeed.
407
+ Sized by OKLCH lightness so the two themes are the SAME design: ΔL 0.017
408
+ dark, 0.014 light (1.045:1 / 1.042:1). That symmetry is the point. Using
409
+ --muted for this job gave ΔL 0.075 in dark against 0.038 in light — twice
410
+ the step in one theme, which is why a header that looked right in dark read
411
+ as a grey slab in light. */
412
+ --surface-subtle: 31 31 34;
342
413
  /* A control you can act on — input, select, option tile, chip-button.
343
414
  Sits at CARD level on purpose: in light that is white against the
344
415
  frame behind it, which reads "type here"; in dark it is raised above
@@ -389,6 +460,20 @@
389
460
  [data-theme="light"], [data-theme="combo"] {
390
461
  /* Brand accent — base stays, hover darkens, glow softens */
391
462
  --accent-hover: 178 8 9;
463
+ /* Brand red passes on a light card (5.64:1) — see the :root note. */
464
+ --accent-ink: var(--accent);
465
+ /* Solved against the DARKEST tint these inks land on, not the lightest.
466
+ The lens mixes success at both 10% (tip panels) and 20% (score chips); an
467
+ ink tuned only to the 10% tint cleared 4.61:1 there and still failed at
468
+ 4.09:1 on the 20% chip. This value clears BOTH — 5.12:1 and 4.54:1 — and
469
+ at 77% of --success it is still unmistakably green. */
470
+ --success-ink: 4 116 81;
471
+ /* Same reasoning as --success-ink: solved against the 20% tint as well as
472
+ the 10% one (5.03:1 / 4.52:1, up from 2.86:1 / 4.05:1). 72% of --warning,
473
+ still unmistakably amber. */
474
+ --warning-ink: 156 86 4;
475
+ /* 5.28:1 / 4.51:1 on the 10% and 20% tints, up from 3.53:1. */
476
+ --destructive-ink: 189 33 33;
392
477
  --accent-muted: 207 14 15 / 0.10;
393
478
  --accent-subtle: 207 14 15 / 0.05;
394
479
  --accent-glow: 207 14 15 / 0.20;
@@ -406,9 +491,28 @@
406
491
  --card-foreground: 17 17 19;
407
492
  --popover-foreground: 17 17 19;
408
493
  --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. */
494
+ /* Byte-identical to --muted-foreground in both themes since they were
495
+ introduced. Aliased rather than duplicated so the two names cannot
496
+ drift apart; prefer --muted-foreground in new code. */
497
+ --text-secondary: var(--muted-foreground);
498
+ /* Was "stays from :root" — but :root IS the dark theme, so light
499
+ inherited the dark-tuned 132 132 141, which measures 3.71:1 on a
500
+ 255 card and fails AA for the 13px body text that consumes it.
501
+ Re-picked by measurement, not by eye: 110 110 119 is the first rung
502
+ with headroom on every light surface it can land on —
503
+ 5.05:1 on --surface-card/page (255), 4.72:1 on --surface-frame (247),
504
+ 4.52:1 on --surface-inset (242). Keeps the ramp's 286° cool cast.
505
+ Side effect: it also repairs the ink ramp's shape. The tiers were
506
+ L 0.442 / 0.616 / 0.633 — a wide gap then a collision, tertiary and
507
+ muted less than half a surface-step apart. They are now
508
+ 0.442 / 0.541 / 0.633: three evenly-spread rungs. */
509
+ --text-tertiary: 110 110 119;
510
+ /* Lifted 161->137 — was 2.56:1 on a white card. Still only 3.47:1 on
511
+ 255, i.e. it clears the 3:1 non-text floor and NOT the 4.5:1 text
512
+ floor. That is deliberate: --text-muted is the quietest rung and is
513
+ for NON-TEXT and DISABLED chrome only — quiet icons, chevrons,
514
+ disabled labels. Real text one tier up (--text-tertiary) or higher.
515
+ Lifting this to pass 4.5:1 would collapse it into --text-tertiary. */
412
516
  --text-muted: 137 137 146;
413
517
  --secondary-foreground: 17 17 19;
414
518
 
@@ -446,9 +550,6 @@
446
550
  --shadow-2xl: 0 24px 60px rgb(0 0 0 / 0.16);
447
551
  --shadow-glow: 0 0 20px rgb(207 14 15 / 0.20);
448
552
 
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
553
  /* Match --page-bg (254/254/254). Was 241/245/249 (slate) — that's
453
554
  * what was actually painting "the steely background" the owner
454
555
  * flagged; --page-bg is the body, but MainLayout's content area
@@ -508,6 +609,8 @@
508
609
  --surface-frame: 247 247 249;
509
610
  --surface-inset: 242 242 244;
510
611
  --surface-hover: 238 238 240;
612
+ /* 1.042:1 against the card — a band, not a slab. See the :root note. */
613
+ --surface-subtle: 250 250 252;
511
614
  /* A control you can act on — input, select, option tile, chip-button.
512
615
  Sits at CARD level on purpose: in light that is white against the
513
616
  frame behind it, which reads "type here"; in dark it is raised above
@@ -606,6 +709,9 @@
606
709
  --folder-pink: 219 39 119;
607
710
  --folder-slate: 71 85 105;
608
711
  --folder-ink: 15 23 42;
712
+ /* Opaque categorical tints — hue at 0.10 over the 255 light card. */
713
+ --folder-amber-tint: 251 241 230;
714
+ --folder-blue-tint: 232 237 251;
609
715
  }
610
716
 
611
717
  /* ============================================================
@@ -1,34 +0,0 @@
1
- // Bridges a conflict between two resolvers, neither of which is wrong.
2
- //
3
- // `@types/react-syntax-highlighter` declares the DIRECTORY module
4
- // `react-syntax-highlighter/dist/esm/styles/prism`. Node's ESM resolver refuses
5
- // a directory import and needs the explicit `.../prism/index.js`. So the
6
- // specifier TypeScript can type is the one Node rejects, and the specifier Node
7
- // accepts is the one TypeScript cannot find — see the note in `code-block.tsx`
8
- // for what that cost.
9
- //
10
- // This declares the file path by RE-EXPORTING the real types from the directory
11
- // module, so the values stay fully typed. The alternative — a `@ts-expect-error`
12
- // or an `any` import — would silence the error by discarding `oneLight` and
13
- // `vscDarkPlus`'s types, which is trading a build failure for a typing hole at
14
- // the one site that already proved it can ship broken.
15
- //
16
- // ── AND THE SAME APPLIES TO THE 13 LANGUAGE IMPORTS ─────────────────────────
17
- // Fixing only the styles directory shipped `0.0.8`, which failed on
18
- // `.../languages/prism/bash` — a FILE imported without its extension, which
19
- // Node's ESM resolver also refuses. The directory import was merely the FIRST
20
- // failure, not the defect. Caught by loading the published artifact under Node
21
- // rather than trusting the local build, which is the whole of PN-015.
22
- //
23
- // Removable when upstream's types declare the file paths directly.
24
- declare module 'react-syntax-highlighter/dist/esm/styles/prism/index.js' {
25
- export * from 'react-syntax-highlighter/dist/esm/styles/prism';
26
- }
27
-
28
- declare module 'react-syntax-highlighter/dist/esm/languages/prism/*.js' {
29
- // Upstream types these as `any`-shaped syntax definitions passed straight to
30
- // `registerLanguage`; the wildcard keeps that contract rather than inventing
31
- // a stricter one this package cannot verify.
32
- const language: unknown;
33
- export default language;
34
- }