@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,466 @@
1
+ /*
2
+ * @lovett/ui — v2 theme, SCOPED.
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:
44
+ * python3 scripts/figma/emit-theme.py <corpus> --out packages/ui/src/theme-v2.css
45
+ *
46
+ * Opt in by putting `data-ds="v2"` on any element. Everything inside it
47
+ * renders on the v2 palette, including existing @lovett/ui primitives —
48
+ * they read the same token names, which now resolve to different values.
49
+ * Nothing outside the scope changes.
50
+ *
51
+ * Derived from a 13-kit Figma corpus; see docs/design-system/figma-corpus/.
52
+ * The direction is Vertex Lab: pure-white trays, near-neutral greys, a
53
+ * navy-tinted ink used as a SURFACE and not only as text.
54
+ *
55
+ * The semantic reassignment is the load-bearing change:
56
+ * --accent red brand identity and the primary action, nothing else
57
+ * --success green save, publish, approve, live
58
+ * --destructive AMBER destructive. No longer red.
59
+ * --warning gold caution
60
+ * --info violet
61
+ *
62
+ * Consequence worth knowing before adopting: amber is a weaker danger
63
+ * signal than red, so destructive actions must carry an icon and an
64
+ * explicit verb. Colour alone no longer does that work.
65
+ */
66
+
67
+ [data-ds="v2"] {
68
+ /* ── Surfaces ─────────────────────────────────────────────── */
69
+ /* The frame/tray step is 2.1 pts — ratified as `medium`. Both being
70
+ white removes the distinction the card pattern is built on. */
71
+ --page-bg: 240 241 242;
72
+ --background: 240 241 242;
73
+ --main-content-bg: 240 241 242;
74
+ --bg-surface: 248 248 249;
75
+ --surface-frame: 248 248 249;
76
+ --card-frame-bg: 248 248 249;
77
+ --bg-card: 255 255 255;
78
+ --card: 255 255 255;
79
+ --tray-bg: 255 255 255;
80
+ --bg-elevated: 255 255 255;
81
+ --surface-raised: 255 255 255;
82
+ --popover: 255 255 255;
83
+ --bg-input: 255 255 255;
84
+ --bg-card-hover: 250 251 252;
85
+ --bg-input-hover: 250 251 252;
86
+ --muted: 246 247 248;
87
+ --canvas-placement-bg: 246 247 248;
88
+
89
+ /* ── Ink ──────────────────────────────────────────────────── */
90
+ /* Navy-tinted, not black. Vertex uses #1c274c 6,522 times — far too
91
+ many for text: it is a panel colour, so it gets a full ramp. */
92
+ --ink-15: 253 253 253;
93
+ --ink-25: 250 251 254;
94
+ --ink-30: 247 248 252;
95
+ --ink-40: 241 245 253;
96
+ --ink-60: 235 241 254;
97
+ --ink-50: 243 247 255;
98
+ --ink-100: 229 237 254;
99
+ --ink-200: 207 217 238;
100
+ --ink-300: 182 193 217;
101
+ --ink-400: 150 163 191;
102
+ --ink-500: 119 134 165;
103
+ --ink-600: 93 108 140;
104
+ --ink-700: 70 82 109;
105
+ --ink-800: 48 58 80;
106
+ --ink-900: 27 35 51;
107
+ --ink-950: 11 16 28;
108
+ --foreground: 11 16 28;
109
+ --card-foreground: 11 16 28;
110
+ --popover-foreground: 11 16 28;
111
+ --text-secondary: 93 108 140;
112
+ --muted-foreground: 93 108 140;
113
+ --text-tertiary: 119 134 165;
114
+ --text-muted: 150 163 191;
115
+ --secondary: 246 247 248;
116
+ --secondary-foreground: 11 16 28;
117
+
118
+ /* ── Borders ──────────────────────────────────────────────── */
119
+ /* Structure only. Depth is shadow — a hard hairline on a depth
120
+ surface reads as a form field. Inputs keep theirs deliberately. */
121
+ --border: 0 0 0 / 0.05;
122
+ --border-strong: 0 0 0 / 0.10;
123
+ --border-card: 0 0 0 / 0.06;
124
+ --border-input: 0 0 0 / 0.12;
125
+ --main-content-border: 0 0 0 / 0.06;
126
+ --input: 246 247 248;
127
+
128
+ /* ── Brand ────────────────────────────────────────────────── */
129
+ /* --accent is the AA-safe action red (4.77:1 with white). The bright
130
+ step is 4.01:1 and cannot carry a white label, so it is exposed
131
+ separately for accents, marks, charts and active fills. */
132
+ --accent: 221 40 34;
133
+ --accent-bright: 228 70 58;
134
+ --accent-hover: 201 17 17;
135
+ --accent-muted: 228 70 58 / 0.10;
136
+ --accent-subtle: 228 70 58 / 0.05;
137
+ --accent-glow: 228 70 58 / 0.22;
138
+ --primary: 221 40 34;
139
+ --primary-hover: 201 17 17;
140
+ --primary-foreground: 255 255 255;
141
+ --ring: 221 40 34;
142
+ --brand-15: 253 253 253;
143
+ --brand-25: 252 251 250;
144
+ --brand-30: 252 247 246;
145
+ --brand-40: 255 242 240;
146
+ --brand-60: 254 237 234;
147
+ --brand-50: 255 244 242;
148
+ --brand-100: 253 231 228;
149
+ --brand-200: 253 204 196;
150
+ --brand-300: 254 166 154;
151
+ --brand-400: 251 114 99;
152
+ --brand-500: 228 70 58;
153
+ --brand-600: 201 17 17;
154
+ --brand-700: 157 8 9;
155
+ --brand-800: 114 4 4;
156
+ --brand-900: 72 2 2;
157
+ --brand-950: 41 0 0;
158
+
159
+ /* ── Semantic — REASSIGNED ────────────────────────────────── */
160
+ --success: 13 128 75;
161
+ --success-bg: 6 160 93 / 0.12;
162
+ --warning: 100 80 1;
163
+ --warning-bg: 161 131 18 / 0.14;
164
+ /* Amber. The collision between a red brand and a red danger state
165
+ cannot be solved by moving the hue — every credible danger red
166
+ sits within a few degrees of the brand. Moving the MEANING can. */
167
+ --destructive: 115 71 9;
168
+ --destructive-bg: 187 116 2 / 0.14;
169
+ --destructive-foreground: 255 255 255;
170
+ --info: 130 62 210;
171
+ --info-bg: 155 97 235 / 0.12;
172
+
173
+ /* ── Categorical series ───────────────────────────────────── */
174
+ /* Constant lightness so no series outranks another; chroma tuned per
175
+ hue so each is equally vivid rather than equally saturated. */
176
+ --series-1: 226 74 62;
177
+ --series-2: 153 100 229;
178
+ --series-3: 9 160 93;
179
+ --series-4: 186 116 4;
180
+ --series-5: 2 137 234;
181
+ --series-6: 14 154 148;
182
+ --series-7: 212 73 150;
183
+ --series-8: 113 149 3;
184
+
185
+ /* ── Elevation ────────────────────────────────────────────── */
186
+ --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);
187
+ --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);
188
+ --shadow-lg: 0 0 0 1px oklch(0 0 0 / 0.06), 0 6px 16px -4px oklch(0 0 0 / 0.12);
189
+ --shadow-xl: 0 0 0 1px oklch(0 0 0 / 0.06), 0 12px 32px -8px oklch(0 0 0 / 0.18);
190
+ --shadow-2xl: 0 0 0 1px oklch(0 0 0 / 0.06), 0 24px 56px -12px oklch(0 0 0 / 0.24);
191
+ --shadow-glow: 0 0 20px oklch(0 0 0 / 0);
192
+ --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);
193
+ --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
+ --inset-highlight: 255 255 255 / 0.9;
195
+ --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);
198
+ }
199
+
200
+ /*
201
+ * Dark. Derived by SELECTING different steps from the same OKLCH ramps,
202
+ * not by inverting the light values — mechanical inversion is what
203
+ * produces muddy dark modes. The ink ramp supplies the surfaces here,
204
+ * which is the same navy that supplies the text in light.
205
+ */
206
+ [data-ds="v2"][data-theme="dark"] {
207
+ --page-bg: 11 16 28;
208
+ --background: 11 16 28;
209
+ --main-content-bg: 11 16 28;
210
+ --bg-surface: 27 35 51;
211
+ --surface-frame: 27 35 51;
212
+ --card-frame-bg: 27 35 51;
213
+ --bg-card: 48 58 80;
214
+ --card: 48 58 80;
215
+ --tray-bg: 48 58 80;
216
+ --bg-elevated: 70 82 109;
217
+ --surface-raised: 70 82 109;
218
+ --popover: 70 82 109;
219
+ --bg-input: 27 35 51;
220
+ --bg-card-hover: 70 82 109;
221
+ --bg-input-hover: 48 58 80;
222
+ --muted: 48 58 80;
223
+ --canvas-placement-bg: 27 35 51;
224
+ --foreground: 250 251 252;
225
+ --card-foreground: 250 251 252;
226
+ --popover-foreground: 250 251 252;
227
+ --text-secondary: 192 193 195;
228
+ --muted-foreground: 192 193 195;
229
+ --text-tertiary: 162 163 165;
230
+ --text-muted: 133 134 136;
231
+ --secondary: 48 58 80;
232
+ --secondary-foreground: 250 251 252;
233
+ --border: 255 255 255 / 0.08;
234
+ --border-strong: 255 255 255 / 0.14;
235
+ --border-card: 255 255 255 / 0.08;
236
+ --border-input: 255 255 255 / 0.14;
237
+ --main-content-border: 255 255 255 / 0.08;
238
+ /* Accents brighten in dark — the light-mode steps go muddy on a dark
239
+ surface because the surrounding lightness has moved. */
240
+ --accent: 228 70 58;
241
+ --accent-bright: 251 114 99;
242
+ --accent-hover: 251 114 99;
243
+ --primary: 228 70 58;
244
+ --primary-hover: 251 114 99;
245
+ --success: 80 188 126;
246
+ --warning: 190 160 64;
247
+ --destructive: 218 145 55;
248
+ --info: 181 134 254;
249
+ --success-bg: 6 160 93 / 0.16;
250
+ --warning-bg: 161 131 18 / 0.16;
251
+ --destructive-bg: 187 116 2 / 0.16;
252
+ --info-bg: 155 97 235 / 0.16;
253
+ /* Layered depth shadows are invisible on dark; a single ring reads. */
254
+ --shadow-sm: 0 0 0 1px oklch(1 0 0 / 0.08);
255
+ --shadow-md: 0 0 0 1px oklch(1 0 0 / 0.08), 0 4px 12px oklch(0 0 0 / 0.4);
256
+ --shadow-lg: 0 0 0 1px oklch(1 0 0 / 0.08), 0 8px 24px oklch(0 0 0 / 0.5);
257
+ --shadow-xl: 0 0 0 1px oklch(1 0 0 / 0.10), 0 16px 40px oklch(0 0 0 / 0.55);
258
+ --shadow-2xl: 0 0 0 1px oklch(1 0 0 / 0.10), 0 24px 60px oklch(0 0 0 / 0.6);
259
+ --card-shadow-rest: 0 0 0 1px oklch(1 0 0 / 0.08);
260
+ --card-shadow-hover: 0 0 0 1px oklch(1 0 0 / 0.14);
261
+ --inset-highlight: 255 255 255 / 0.05;
262
+ --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));
466
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * TokenBadge — tinted, token-driven category badge.
3
+ *
4
+ * Promoted in ADR-123 (Budget Flighting redesign) from the lens-local
5
+ * `TacticBadge`. Distinct from `<Badge>`: Badge carries only the 6 fixed
6
+ * semantic tones (success / warning / …); TokenBadge tints from ANY public
7
+ * token base name, so a categorical palette like `--folder-*` (tactic colors,
8
+ * flight-group colors) works. The lens keeps a thin `TacticBadge` wrapper that
9
+ * passes the matching `--folder-*` token.
10
+ *
11
+ * Token discipline: the only color input is a public token *name*; fill/border/
12
+ * text are derived from it via `rgb(var(--token) / a)`. No literals.
13
+ */
14
+
15
+ import type { ReactNode } from 'react'
16
+
17
+ import { cn } from './lib/utils'
18
+
19
+ export interface TokenBadgeProps {
20
+ /** Public token name to tint from, e.g. `--folder-teal`, `--accent`,
21
+ * `--success`. Must include the leading `--`. */
22
+ colorVar: string
23
+ children: ReactNode
24
+ /** `soft` = tinted fill (default). `outline` = transparent fill + tinted
25
+ * border. */
26
+ variant?: 'soft' | 'outline'
27
+ /** Optional leading status dot in the token color. Ignored when `icon` set. */
28
+ dot?: boolean
29
+ /** Optional leading icon (12–14px expected). */
30
+ icon?: ReactNode
31
+ /** `sm` (10px) · `md` (11px, default) · `lg` (12px, taller). */
32
+ size?: 'sm' | 'md' | 'lg'
33
+ /** Pill (`full`) vs squared category tag (`tag`, default). */
34
+ shape?: 'tag' | 'pill'
35
+ /** Uppercase + wider tracking for short category codes (PRG, YT…). */
36
+ uppercase?: boolean
37
+ className?: string
38
+ }
39
+
40
+ const SIZE_CLASS: Record<NonNullable<TokenBadgeProps['size']>, string> = {
41
+ sm: 'h-[18px] px-1.5 text-[10px]',
42
+ md: 'h-5 px-2 text-[11px]',
43
+ lg: 'h-[22px] px-2.5 text-xs',
44
+ }
45
+
46
+ export function TokenBadge({
47
+ colorVar,
48
+ children,
49
+ variant = 'soft',
50
+ dot = false,
51
+ icon,
52
+ size = 'md',
53
+ shape = 'tag',
54
+ uppercase = false,
55
+ className,
56
+ }: TokenBadgeProps) {
57
+ const solid = `rgb(var(${colorVar}))`
58
+ const style =
59
+ variant === 'outline'
60
+ ? {
61
+ background: 'transparent',
62
+ color: solid,
63
+ boxShadow: `inset 0 0 0 1px rgb(var(${colorVar}) / 0.4)`,
64
+ }
65
+ : { background: `rgb(var(${colorVar}) / 0.12)`, color: solid }
66
+
67
+ return (
68
+ <span
69
+ className={cn(
70
+ 'inline-flex w-fit shrink-0 items-center gap-1 font-bold whitespace-nowrap',
71
+ uppercase && 'uppercase tracking-[0.06em]',
72
+ SIZE_CLASS[size],
73
+ className,
74
+ )}
75
+ style={{
76
+ ...style,
77
+ borderRadius:
78
+ shape === 'pill' ? 'var(--radius-full)' : 'var(--radius-xs)',
79
+ }}
80
+ >
81
+ {dot && !icon ? (
82
+ <span
83
+ aria-hidden
84
+ className="size-1.5 shrink-0 rounded-full"
85
+ style={{ background: 'currentColor' }}
86
+ />
87
+ ) : null}
88
+ {icon && <span className="flex shrink-0 items-center">{icon}</span>}
89
+ {children}
90
+ </span>
91
+ )
92
+ }