@lovett/ui 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +279 -115
- package/dist/index.js +479 -66
- package/dist/index.js.map +1 -1
- package/dist/styles.css +74 -0
- package/dist/tokens.css +181 -60
- package/package.json +16 -4
- package/src/__tests__/button.test.tsx +137 -0
- package/src/__tests__/card.test.tsx +103 -0
- package/src/__tests__/dead-render.test.tsx +117 -0
- package/src/__tests__/input.test.tsx +134 -0
- package/src/__tests__/modal.test.tsx +154 -0
- package/src/__tests__/page-shell.test.tsx +128 -0
- package/src/__tests__/setup.ts +43 -0
- package/src/__tests__/token-shape.test.ts +193 -0
- package/src/card.tsx +1 -1
- package/src/collapsible-card.tsx +85 -0
- package/src/data-grid/table-body.tsx +8 -1
- package/src/dropdown-menu.tsx +1 -1
- package/src/floating-status-bar.tsx +1 -1
- package/src/folder-tree-picker.tsx +5 -6
- package/src/frame-stack.tsx +27 -10
- package/src/hero-form-card.tsx +2 -2
- package/src/icons/brand.tsx +187 -0
- package/src/index.ts +32 -0
- package/src/lib/clipboard.ts +14 -0
- package/src/lib/color.ts +111 -0
- package/src/meta-cell.tsx +52 -0
- package/src/meta-previews/MetaFeedCarousel.tsx +1 -1
- package/src/meta-previews/MetaFeedPreview.tsx +1 -1
- package/src/modal.tsx +77 -6
- package/src/pill-button.tsx +23 -5
- package/src/profile-section.tsx +40 -9
- package/src/sortable-table.tsx +5 -1
- package/src/styles.css +74 -0
- package/src/tabs.tsx +4 -0
- package/src/tag-chip-input.tsx +1 -1
- package/src/theme-v2.css +466 -0
- package/src/tokens.css +181 -60
- package/src/v2/README.md +208 -0
- package/src/v2/__demo__/showcase.tsx +1045 -0
- package/src/v2/action.tsx +91 -0
- package/src/v2/callout.tsx +76 -0
- package/src/v2/document-section.tsx +82 -0
- package/src/v2/document-shell.tsx +0 -0
- package/src/v2/field-row.tsx +113 -0
- package/src/v2/icons.tsx +165 -0
- package/src/v2/index.ts +147 -0
- package/src/v2/layout.tsx +293 -0
- package/src/v2/progress-track.tsx +89 -0
- package/src/v2/stat-tile.tsx +129 -0
- package/src/v2/states.tsx +271 -0
- package/src/v2/status-pill.tsx +74 -0
- package/src/v2/theme.css +1861 -0
- package/src/v2/timeline.tsx +81 -0
- package/src/v2/tokens.ts +228 -0
- package/src/value-chip.tsx +76 -0
package/dist/styles.css
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* .has-leading/trailing, .is-error/disabled,
|
|
26
26
|
* .size-sm/lg — Input, Textarea
|
|
27
27
|
* .scrollbar-hide — ChipNav
|
|
28
|
+
* .ds-enter-pop, .ds-enter-rise — DropdownMenu,
|
|
29
|
+
* FloatingStatusBar
|
|
28
30
|
* prefers-reduced-motion block — accessibility
|
|
29
31
|
*
|
|
30
32
|
* Out of scope for Phase 1: .empty, .skel, .field*, .pg-header, .search-bar,
|
|
@@ -33,6 +35,24 @@
|
|
|
33
35
|
* consumers (Field, EmptyState, PageHeader, etc.) arrive in later phases.
|
|
34
36
|
*/
|
|
35
37
|
|
|
38
|
+
/* ---- PANE MASK ---- */
|
|
39
|
+
/**
|
|
40
|
+
* Opaque fill matching the MainLayout content pane, for a sticky element that
|
|
41
|
+
* must hide content scrolling beneath it.
|
|
42
|
+
*
|
|
43
|
+
* Exists because the mask has to equal the pane EXACTLY or content shows
|
|
44
|
+
* through / bands, and the only token that does is `--main-content-bg` —
|
|
45
|
+
* internal, so lens code cannot name it (CLAUDE.md §5). Same shape as
|
|
46
|
+
* `.ds-card-tray` owning `--tray-bg`: the class lives here, consumers use the
|
|
47
|
+
* class. This is NOT a raised surface — do not reach for `--surface-raised`
|
|
48
|
+
* (ADR-063); a sticky mask must match the pane, not sit above it.
|
|
49
|
+
*
|
|
50
|
+
* Consumers: sem-spec `output/index.tsx`, `share-view/ad-preview-section.tsx`.
|
|
51
|
+
*/
|
|
52
|
+
.ds-pane-mask {
|
|
53
|
+
background: rgb(var(--main-content-bg));
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
/* ---- CARD SURFACE ---- */
|
|
37
57
|
.ds-card-surface {
|
|
38
58
|
background: rgb(var(--bg-card));
|
|
@@ -57,6 +77,11 @@
|
|
|
57
77
|
|
|
58
78
|
/* ---- CARD TRAY ---- */
|
|
59
79
|
.ds-card-tray {
|
|
80
|
+
/* 4px, not 10. Concentric radius is outer = inner + padding, so a
|
|
81
|
+
16px card around a 12px tray wants a 4px inset — at 10 the maths
|
|
82
|
+
was 22 and the corners fought each other. It is also what every
|
|
83
|
+
kit in the corpus measures (AI Agent: pad 4, r16 outer / r12
|
|
84
|
+
inner), and it is what reads as thinner and sleeker. */
|
|
60
85
|
margin: var(--tray-inset);
|
|
61
86
|
background: rgb(var(--tray-bg));
|
|
62
87
|
/* Owner ask: visible 1px border on the inner tray so the tray surface
|
|
@@ -534,8 +559,57 @@
|
|
|
534
559
|
transform: scaleY(2);
|
|
535
560
|
}
|
|
536
561
|
|
|
562
|
+
/* ---- ENTRANCE ANIMATIONS ----
|
|
563
|
+
*
|
|
564
|
+
* These exist because `animate-in` / `fade-in-0` / `zoom-in-95` /
|
|
565
|
+
* `slide-in-from-bottom-2` are NOT Tailwind core utilities — they ship in
|
|
566
|
+
* `tailwindcss-animate` / `tw-animate-css`, neither of which is a dependency
|
|
567
|
+
* of this repo. Both of the kit's entrance animations were therefore
|
|
568
|
+
* referencing classes that were never generated and had never run, while
|
|
569
|
+
* typecheck, lint and build stayed green.
|
|
570
|
+
*
|
|
571
|
+
* Adding the dependency would need an ADR (CLAUDE.md §1); two keyframes do
|
|
572
|
+
* not. Durations come from the motion tokens rather than being re-specified
|
|
573
|
+
* per call site.
|
|
574
|
+
*/
|
|
575
|
+
@keyframes ds-enter-pop {
|
|
576
|
+
from {
|
|
577
|
+
opacity: 0;
|
|
578
|
+
transform: scale(0.95);
|
|
579
|
+
}
|
|
580
|
+
to {
|
|
581
|
+
opacity: 1;
|
|
582
|
+
transform: scale(1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
@keyframes ds-enter-rise {
|
|
586
|
+
from {
|
|
587
|
+
opacity: 0;
|
|
588
|
+
transform: translateY(0.5rem);
|
|
589
|
+
}
|
|
590
|
+
to {
|
|
591
|
+
opacity: 1;
|
|
592
|
+
transform: translateY(0);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/* Popovers, menus, dropdowns — anchored to a trigger, so they scale open. */
|
|
596
|
+
.ds-enter-pop {
|
|
597
|
+
animation: ds-enter-pop var(--dur-fast) var(--ease-out);
|
|
598
|
+
}
|
|
599
|
+
/* Bars and toasts entering from a screen edge. Animates `transform`, which
|
|
600
|
+
* composes with (rather than overwrites) a `translate`-based centering
|
|
601
|
+
* utility such as `-translate-x-1/2`. */
|
|
602
|
+
.ds-enter-rise {
|
|
603
|
+
animation: ds-enter-rise var(--dur-base) var(--ease-out);
|
|
604
|
+
}
|
|
605
|
+
|
|
537
606
|
/* ---- REDUCED MOTION ---- */
|
|
538
607
|
@media (prefers-reduced-motion: reduce) {
|
|
608
|
+
/* 0.01ms rather than `none` so animationend still fires for any listener. */
|
|
609
|
+
.ds-enter-pop,
|
|
610
|
+
.ds-enter-rise {
|
|
611
|
+
animation-duration: 0.01ms !important;
|
|
612
|
+
}
|
|
539
613
|
.btn,
|
|
540
614
|
.btn-primary,
|
|
541
615
|
.input-shell {
|
package/dist/tokens.css
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* • Text: --foreground, --text-secondary, --text-tertiary,
|
|
31
31
|
* --text-muted, --muted-foreground
|
|
32
32
|
* • Semantic: --success(-bg), --warning(-bg), --info(-bg),
|
|
33
|
-
* --destructive(-foreground)
|
|
33
|
+
* --destructive(-bg, -foreground)
|
|
34
34
|
* • Folder: --folder-red/amber/emerald/teal/blue/violet/pink/
|
|
35
35
|
* slate/ink (categorical palette for folder accents)
|
|
36
36
|
* • Spacing: --space-1…12 (4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 48)
|
|
@@ -125,32 +125,23 @@
|
|
|
125
125
|
/* ============================================================
|
|
126
126
|
DARK THEME SURFACES — INTERNAL (primitives only)
|
|
127
127
|
============================================================ */
|
|
128
|
-
--background: 9 9 11;
|
|
129
128
|
--foreground: 244 244 245;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
--bg-card-hover: 26 26 29;
|
|
134
|
-
--bg-input: 15 15 17;
|
|
135
|
-
|
|
136
|
-
--card: 20 20 22;
|
|
129
|
+
/* PUBLIC (ADR-063 D1): opaque elevated surface for bespoke lens/app
|
|
130
|
+
floating chrome (pills, capsules, one-off raised panels) that can't be a
|
|
131
|
+
primitive. Same values as the INTERNAL --bg-elevated by design. */
|
|
137
132
|
--card-foreground: 244 244 245;
|
|
138
|
-
--popover: 24 24 27;
|
|
139
133
|
--popover-foreground: 244 244 245;
|
|
140
|
-
|
|
141
|
-
--muted: 39 39 42;
|
|
142
134
|
--muted-foreground: 161 161 170;
|
|
143
135
|
--text-secondary: 161 161 170;
|
|
144
|
-
--text-tertiary
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
--
|
|
136
|
+
/* Lifted 113->132 / 82->106: at the old values --text-tertiary measured
|
|
137
|
+
3.56:1 and --text-muted 2.22:1 on --bg-card, i.e. both under the 3:1
|
|
138
|
+
floor on the surface they are most often used on. */
|
|
139
|
+
--text-tertiary: 132 132 141;
|
|
140
|
+
--text-muted: 106 106 115;
|
|
148
141
|
--secondary-foreground: 244 244 245;
|
|
149
142
|
|
|
150
143
|
--border: 255 255 255 / 0.08;
|
|
151
144
|
--border-strong: 255 255 255 / 0.14;
|
|
152
|
-
--input: 39 39 42;
|
|
153
|
-
|
|
154
145
|
/* Semantic — PUBLIC */
|
|
155
146
|
--success: 16 185 129;
|
|
156
147
|
--success-bg: 16 185 129 / 0.12;
|
|
@@ -159,6 +150,7 @@
|
|
|
159
150
|
--info: 59 130 246;
|
|
160
151
|
--info-bg: 59 130 246 / 0.12;
|
|
161
152
|
--destructive: 239 68 68;
|
|
153
|
+
--destructive-bg: 239 68 68 / 0.12;
|
|
162
154
|
--destructive-foreground: 255 255 255;
|
|
163
155
|
|
|
164
156
|
/* Folder palette — PUBLIC. Categorical accents for Asset folders
|
|
@@ -211,8 +203,6 @@
|
|
|
211
203
|
--bullet-offset-y: 7px;
|
|
212
204
|
|
|
213
205
|
/* Recessed card footer strip — INTERNAL (<Card.Foot>) */
|
|
214
|
-
--card-foot-bg: 255 255 255 / 0.025;
|
|
215
|
-
|
|
216
206
|
/* Card surface — INTERNAL (<Card>) */
|
|
217
207
|
--border-card: 255 255 255 / 0.08;
|
|
218
208
|
--card-shadow-rest:
|
|
@@ -221,13 +211,10 @@
|
|
|
221
211
|
0 4px 12px rgb(0 0 0 / 0.28);
|
|
222
212
|
|
|
223
213
|
/* Card-tray — INTERNAL (<Card.Tray>) */
|
|
224
|
-
--tray-
|
|
225
|
-
--tray-inset: 10px;
|
|
214
|
+
--tray-inset: 4px;
|
|
226
215
|
--radius-tray: var(--radius-md);
|
|
227
216
|
|
|
228
217
|
/* Card frame — INTERNAL (outer .ds-card-surface bg in framed mode) */
|
|
229
|
-
--card-frame-bg: 20 20 22;
|
|
230
|
-
|
|
231
218
|
/* ============================================================
|
|
232
219
|
CALCULATOR SHELL v2 — INTERNAL (ADR-076 D7). Consumed only by
|
|
233
220
|
the v2 primitives (CalculatorShellV2 header, StatRow tiles) and
|
|
@@ -250,7 +237,6 @@
|
|
|
250
237
|
/* ============================================================
|
|
251
238
|
MAIN CONTENT AREA — INTERNAL (layout primitives only)
|
|
252
239
|
============================================================ */
|
|
253
|
-
--main-content-bg: 10 10 11;
|
|
254
240
|
--main-content-border: 255 255 255 / 0.08;
|
|
255
241
|
|
|
256
242
|
/* ============================================================
|
|
@@ -294,9 +280,6 @@
|
|
|
294
280
|
--surface-overlay: 255 255 255 / 0.06;
|
|
295
281
|
--surface-overlay-strong: 255 255 255 / 0.10;
|
|
296
282
|
--surface-overlay-stronger: 255 255 255 / 0.16;
|
|
297
|
-
|
|
298
|
-
--page-bg: 5 5 6;
|
|
299
|
-
--bg-input-hover: 19 19 22;
|
|
300
283
|
--modal-overlay: 0 0 0 / 0.65;
|
|
301
284
|
|
|
302
285
|
/* `--surface-on-accent` — PUBLIC (Phase Sidebar-E MIN-1, ADR-049).
|
|
@@ -349,7 +332,43 @@
|
|
|
349
332
|
--toast-border: 255 255 255 / 0.10;
|
|
350
333
|
|
|
351
334
|
/* Canvas placement bg — INTERNAL (placement primitive) */
|
|
352
|
-
|
|
335
|
+
|
|
336
|
+
/* Surface ladder — dark. Same five roles, same hue family. */
|
|
337
|
+
--surface-page: 11 11 12;
|
|
338
|
+
--surface-frame: 21 21 23;
|
|
339
|
+
--surface-card: 27 27 30;
|
|
340
|
+
--surface-inset: 45 45 48;
|
|
341
|
+
--surface-hover: 55 55 59;
|
|
342
|
+
/* A control you can act on — input, select, option tile, chip-button.
|
|
343
|
+
Sits at CARD level on purpose: in light that is white against the
|
|
344
|
+
frame behind it, which reads "type here"; in dark it is raised above
|
|
345
|
+
the frame. --muted/--surface-inset is the RECESSED role (wells, zebra,
|
|
346
|
+
table headers) and reads as disabled when a control borrows it. The
|
|
347
|
+
lenses were borrowing it because --bg-input is internal and this
|
|
348
|
+
public role did not exist. */
|
|
349
|
+
--surface-control: var(--surface-card);
|
|
350
|
+
--surface-elevated: 37 37 40;
|
|
351
|
+
|
|
352
|
+
--page-bg: var(--surface-page);
|
|
353
|
+
--background: var(--surface-page);
|
|
354
|
+
--main-content-bg: var(--surface-page);
|
|
355
|
+
--bg-surface: var(--surface-frame);
|
|
356
|
+
--bg-card: var(--surface-card);
|
|
357
|
+
--card: var(--surface-card);
|
|
358
|
+
--tray-bg: var(--surface-card);
|
|
359
|
+
--card-frame-bg: var(--surface-frame);
|
|
360
|
+
--card-foot-bg: var(--surface-frame);
|
|
361
|
+
--bg-elevated: var(--surface-elevated);
|
|
362
|
+
--surface-raised: var(--surface-elevated);
|
|
363
|
+
--popover: var(--surface-elevated);
|
|
364
|
+
--bg-input: var(--surface-frame);
|
|
365
|
+
--muted: var(--surface-inset);
|
|
366
|
+
--input: var(--surface-inset);
|
|
367
|
+
--secondary: var(--surface-inset);
|
|
368
|
+
--canvas-placement-bg: var(--surface-frame);
|
|
369
|
+
--bg-card-hover: var(--surface-hover);
|
|
370
|
+
--bg-input-hover: var(--surface-inset);
|
|
371
|
+
|
|
353
372
|
}
|
|
354
373
|
|
|
355
374
|
/* ============================================================
|
|
@@ -382,27 +401,15 @@
|
|
|
382
401
|
* almost entirely on --border-card (0.07). If cards visually
|
|
383
402
|
* disappear, bump --border-card. Hover/muted/input-hover stay
|
|
384
403
|
* darker on purpose so state changes remain visible. */
|
|
385
|
-
--background: 254 254 254;
|
|
386
404
|
--foreground: 17 17 19;
|
|
387
|
-
|
|
388
|
-
--bg-elevated: 255 255 255;
|
|
389
|
-
--bg-card: 255 255 255;
|
|
390
|
-
--bg-card-hover: 244 244 244;
|
|
391
|
-
--bg-input: 255 255 255;
|
|
392
|
-
--bg-input-hover: 253 253 253;
|
|
393
|
-
|
|
394
|
-
--card: 255 255 255;
|
|
405
|
+
/* PUBLIC (ADR-063 D1) — see the dark block note. */
|
|
395
406
|
--card-foreground: 17 17 19;
|
|
396
|
-
--popover: 255 255 255;
|
|
397
407
|
--popover-foreground: 17 17 19;
|
|
398
|
-
|
|
399
|
-
--muted: 245 245 245;
|
|
400
408
|
--muted-foreground: 82 82 91;
|
|
401
409
|
--text-secondary: 82 82 91;
|
|
402
410
|
/* --text-tertiary stays from :root */
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
--secondary: 244 244 245;
|
|
411
|
+
/* Lifted 161->137 — was 2.56:1 on a white card. */
|
|
412
|
+
--text-muted: 137 137 146;
|
|
406
413
|
--secondary-foreground: 17 17 19;
|
|
407
414
|
|
|
408
415
|
--border: 0 0 0 / 0.12;
|
|
@@ -412,8 +419,6 @@
|
|
|
412
419
|
* to delineate. The 0.16 value worked when --bg-input was slate-tinted; on
|
|
413
420
|
* white-on-white the prior border was too subtle. */
|
|
414
421
|
--border-input: 0 0 0 / 0.22;
|
|
415
|
-
--input: 244 244 245;
|
|
416
|
-
|
|
417
422
|
--success: 5 150 105;
|
|
418
423
|
--success-bg: 5 150 105 / 0.10;
|
|
419
424
|
/* `--warning` and the folder palette are intentionally split out
|
|
@@ -423,13 +428,12 @@
|
|
|
423
428
|
--info: 37 99 235;
|
|
424
429
|
--info-bg: 37 99 235 / 0.10;
|
|
425
430
|
--destructive: 220 38 38;
|
|
431
|
+
--destructive-bg: 220 38 38 / 0.10;
|
|
426
432
|
|
|
427
433
|
--surface-overlay-soft: 0 0 0 / 0.03;
|
|
428
434
|
--surface-overlay: 0 0 0 / 0.05;
|
|
429
435
|
--surface-overlay-strong: 0 0 0 / 0.08;
|
|
430
436
|
--surface-overlay-stronger: 0 0 0 / 0.12;
|
|
431
|
-
|
|
432
|
-
--page-bg: 254 254 254;
|
|
433
437
|
--modal-overlay: 0 0 0 / 0.40;
|
|
434
438
|
/* `--surface-on-accent` stays white in light mode too — see :root
|
|
435
439
|
* comment. Declared here for symmetry / future-proofing. */
|
|
@@ -449,17 +453,7 @@
|
|
|
449
453
|
* what was actually painting "the steely background" the owner
|
|
450
454
|
* flagged; --page-bg is the body, but MainLayout's content area
|
|
451
455
|
* paints with this token. Both must move together. */
|
|
452
|
-
--main-content-bg: 254 254 254;
|
|
453
456
|
--main-content-border: 0 0 0 / 0.10;
|
|
454
|
-
|
|
455
|
-
--canvas-placement-bg: 241 245 249;
|
|
456
|
-
|
|
457
|
-
--card-foot-bg: 248 250 252;
|
|
458
|
-
|
|
459
|
-
--tray-bg: 255 255 255;
|
|
460
|
-
|
|
461
|
-
--card-frame-bg: 248 250 252;
|
|
462
|
-
|
|
463
457
|
/* Calculator Shell v2 — light overrides (ADR-076 D7). */
|
|
464
458
|
--brand-ink: 20 20 23;
|
|
465
459
|
--shell-shadow:
|
|
@@ -483,6 +477,68 @@
|
|
|
483
477
|
0 2px 4px rgb(0 0 0 / 0.07),
|
|
484
478
|
0 8px 24px rgb(0 0 0 / 0.09);
|
|
485
479
|
|
|
480
|
+
/* ============================================================
|
|
481
|
+
SURFACE LADDER — the canonical set (2026-08-04)
|
|
482
|
+
------------------------------------------------------------
|
|
483
|
+
Before this, 19 surface tokens carried 4 distinct values, in
|
|
484
|
+
THREE different grey families: --card-frame-bg / --card-foot-bg
|
|
485
|
+
at hue 248 (slate), --muted / --bg-card-hover at 90
|
|
486
|
+
(achromatic), --input / --secondary at 286 (zinc). Mixing grey
|
|
487
|
+
families is what reads as "inconsistent" long before anyone can
|
|
488
|
+
say which value is wrong.
|
|
489
|
+
|
|
490
|
+
Everything now sits on hue 286 — the family the TEXT ramp
|
|
491
|
+
already uses (--foreground and every --text-* measure 285.8-286.4),
|
|
492
|
+
so greys and type finally agree.
|
|
493
|
+
|
|
494
|
+
FIVE roles. Every legacy name below is an alias onto one of them,
|
|
495
|
+
so no component changes and nothing is orphaned:
|
|
496
|
+
|
|
497
|
+
page the document. PURE WHITE — no grey backdrop.
|
|
498
|
+
card cards, trays, inputs, popovers. Also white; separation
|
|
499
|
+
is border + shadow, not a fill step.
|
|
500
|
+
frame the CONTAINED BACKGROUND for a group of sections —
|
|
501
|
+
<FrameStack> wrapping several <SectionCard>. The only
|
|
502
|
+
place a grey surface is correct.
|
|
503
|
+
inset recessed wells, table header strips, zebra rows.
|
|
504
|
+
hover the deepest step; hover on an inset.
|
|
505
|
+
============================================================ */
|
|
506
|
+
--surface-page: 255 255 255;
|
|
507
|
+
--surface-card: 255 255 255;
|
|
508
|
+
--surface-frame: 247 247 249;
|
|
509
|
+
--surface-inset: 242 242 244;
|
|
510
|
+
--surface-hover: 238 238 240;
|
|
511
|
+
/* A control you can act on — input, select, option tile, chip-button.
|
|
512
|
+
Sits at CARD level on purpose: in light that is white against the
|
|
513
|
+
frame behind it, which reads "type here"; in dark it is raised above
|
|
514
|
+
the frame. --muted/--surface-inset is the RECESSED role (wells, zebra,
|
|
515
|
+
table headers) and reads as disabled when a control borrows it. The
|
|
516
|
+
lenses were borrowing it because --bg-input is internal and this
|
|
517
|
+
public role did not exist. */
|
|
518
|
+
--surface-control: var(--surface-card);
|
|
519
|
+
|
|
520
|
+
/* Legacy names — aliases onto the five above. Kept so the ~1,300
|
|
521
|
+
existing call sites keep working; prefer the canonical names. */
|
|
522
|
+
--page-bg: var(--surface-page);
|
|
523
|
+
--background: var(--surface-page);
|
|
524
|
+
--bg-surface: var(--surface-page);
|
|
525
|
+
--main-content-bg: var(--surface-page);
|
|
526
|
+
--bg-card: var(--surface-card);
|
|
527
|
+
--card: var(--surface-card);
|
|
528
|
+
--tray-bg: var(--surface-card);
|
|
529
|
+
--bg-elevated: var(--surface-card);
|
|
530
|
+
--surface-raised: var(--surface-card);
|
|
531
|
+
--popover: var(--surface-card);
|
|
532
|
+
--bg-input: var(--surface-card);
|
|
533
|
+
--card-frame-bg: var(--surface-frame);
|
|
534
|
+
--card-foot-bg: var(--surface-frame);
|
|
535
|
+
--muted: var(--surface-inset);
|
|
536
|
+
--input: var(--surface-inset);
|
|
537
|
+
--secondary: var(--surface-inset);
|
|
538
|
+
--canvas-placement-bg: var(--surface-inset);
|
|
539
|
+
--bg-card-hover: var(--surface-hover);
|
|
540
|
+
--bg-input-hover: var(--surface-frame);
|
|
541
|
+
|
|
486
542
|
}
|
|
487
543
|
|
|
488
544
|
/* ============================================================
|
|
@@ -510,7 +566,12 @@
|
|
|
510
566
|
dark even though the rest of the shell is light.
|
|
511
567
|
============================================================ */
|
|
512
568
|
[data-theme="light"] {
|
|
513
|
-
|
|
569
|
+
/* Flat white — this token is the app FRAME (main-layout.tsx paints the
|
|
570
|
+
whole shell with it), so a grey here tints every light page behind the
|
|
571
|
+
floating content card. The card's border + shadow carry the separation.
|
|
572
|
+
Matches meta-ads-audit-dashboard, which consumes the same tokens and
|
|
573
|
+
sets its rail to --page-bg. */
|
|
574
|
+
--sidebar-bg: 255 255 255;
|
|
514
575
|
--sidebar-surface: 0 0 0 / 0.04;
|
|
515
576
|
--sidebar-surface-hover: 0 0 0 / 0.08;
|
|
516
577
|
--sidebar-surface-active: 207 14 15 / 0.10;
|
|
@@ -571,9 +632,69 @@
|
|
|
571
632
|
so the hex literals are permitted here and nowhere in apps/.
|
|
572
633
|
============================================================ */
|
|
573
634
|
:root {
|
|
574
|
-
--color-meta-blue:
|
|
635
|
+
--color-meta-blue: rgb(var(--platform-facebook));
|
|
575
636
|
--color-meta-ink: #050505;
|
|
576
637
|
--color-surface-50: #fafafa;
|
|
577
638
|
--color-surface-100: #f4f4f5;
|
|
578
639
|
--color-surface-200: #e4e4e7;
|
|
579
640
|
}
|
|
641
|
+
|
|
642
|
+
/* ============================================================
|
|
643
|
+
PLATFORM BRAND + MEDIA-OVERLAY TOKENS (theme-INDEPENDENT)
|
|
644
|
+
------------------------------------------------------------
|
|
645
|
+
Third-party brand colours. Facebook is #1877F2 whether the
|
|
646
|
+
workspace is light or dark, so these deliberately sit OUTSIDE
|
|
647
|
+
the theme blocks — putting them in the theme would make a
|
|
648
|
+
platform badge change colour with the app, which is wrong.
|
|
649
|
+
|
|
650
|
+
This block exists because `swipes.css` and `collections.css`
|
|
651
|
+
each said, in their own header: "when a second consumer of
|
|
652
|
+
these exact tokens lands, promote to packages/ui/src/tokens.css".
|
|
653
|
+
Three landed (collections, ad-intel, the social previews), and
|
|
654
|
+
the same Facebook blue ended up defined in seven places under
|
|
655
|
+
seven names — plus an eighth, `--adstage-fb-like`, at a DIFFERENT
|
|
656
|
+
and wrong value (#2078f4). This is that promotion.
|
|
657
|
+
|
|
658
|
+
BARE TRIPLES, not complete colours: the same brand colour is
|
|
659
|
+
used at different opacities per surface (swipes/collections
|
|
660
|
+
badges at 0.78, ad-intel at 0.85), so the alpha belongs at the
|
|
661
|
+
call site — `rgb(var(--platform-facebook) / 0.78)`. Baking an
|
|
662
|
+
alpha into the token is what makes it un-shareable.
|
|
663
|
+
|
|
664
|
+
The `--media-*` overlays DO carry their alpha, because there the
|
|
665
|
+
opacity ramp IS the shared definition: swipes and collections
|
|
666
|
+
used byte-identical values.
|
|
667
|
+
|
|
668
|
+
Platform-chrome and exempt substrate (packages/ui), so the
|
|
669
|
+
literals are permitted here and nowhere in apps/.
|
|
670
|
+
============================================================ */
|
|
671
|
+
:root {
|
|
672
|
+
/* Platform brand colours. */
|
|
673
|
+
--platform-facebook: 24 119 242;
|
|
674
|
+
--platform-instagram: 225 48 108;
|
|
675
|
+
--platform-linkedin: 10 102 194;
|
|
676
|
+
--platform-google: 66 133 244;
|
|
677
|
+
/* Google's logo green — the discovery report's tiles use it where the
|
|
678
|
+
* badge surfaces use the logo blue above. Both are real, both are used. */
|
|
679
|
+
--platform-google-green: 52 168 83;
|
|
680
|
+
--platform-youtube: 255 0 0;
|
|
681
|
+
/* TikTok's brand is black, which only works as a BADGE FILL (white text
|
|
682
|
+
* on it). As an icon colour on a dark card it measures 1.22:1, so the
|
|
683
|
+
* discovery tiles use theme-aware --folder-slate instead. Same reason
|
|
684
|
+
* there is no --platform-x: X is brand-black too, and its one consumer
|
|
685
|
+
* uses --folder-ink, which is byte-identical in light mode. */
|
|
686
|
+
--platform-tiktok: 0 0 0;
|
|
687
|
+
--platform-yelp: 211 35 35;
|
|
688
|
+
--platform-email: 245 158 11;
|
|
689
|
+
--platform-website: 255 255 255;
|
|
690
|
+
/* Near-white reads on every brand fill above. */
|
|
691
|
+
--platform-on-color: 255 255 255;
|
|
692
|
+
|
|
693
|
+
/* Overlays on creative media. Theme-independent because the thing
|
|
694
|
+
* underneath is an image, not a themed surface. */
|
|
695
|
+
--media-scrim-strong: 0 0 0 / 0.78;
|
|
696
|
+
--media-scrim-mid: 0 0 0 / 0.2;
|
|
697
|
+
--media-scrim-label: 0 0 0 / 0.55;
|
|
698
|
+
--media-on-scrim: 255 255 255 / 0.88;
|
|
699
|
+
--media-overlay-btn: 255 255 255 / 0.14;
|
|
700
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lovett/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Design system primitives, tokens, layouts, and patterns for the Lovett portfolio.",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"import": "./dist/index.js"
|
|
16
16
|
},
|
|
17
17
|
"./tokens.css": "./dist/tokens.css",
|
|
18
|
-
"./styles.css": "./dist/styles.css"
|
|
18
|
+
"./styles.css": "./dist/styles.css",
|
|
19
|
+
"./theme-v2.css": "./dist/theme-v2.css"
|
|
19
20
|
},
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "restricted"
|
|
@@ -32,14 +33,23 @@
|
|
|
32
33
|
"react-router-dom": "^7.1.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
36
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
37
|
+
"@testing-library/react": "^16.3.2",
|
|
38
|
+
"@testing-library/user-event": "^14.6.1",
|
|
39
|
+
"@types/node": "^22",
|
|
35
40
|
"@types/react": "^19.0.0",
|
|
36
41
|
"@types/react-dom": "^19.0.0",
|
|
37
42
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
43
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
38
44
|
"eslint": "^9.17.0",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
39
46
|
"lucide-react": "^0.469.0",
|
|
47
|
+
"react": "^19.0.0",
|
|
48
|
+
"react-dom": "^19.0.0",
|
|
40
49
|
"react-router-dom": "^7.1.0",
|
|
41
50
|
"tsup": "^8.5.1",
|
|
42
|
-
"typescript": "^5.7.2"
|
|
51
|
+
"typescript": "^5.7.2",
|
|
52
|
+
"vitest": "^4.1.6"
|
|
43
53
|
},
|
|
44
54
|
"dependencies": {
|
|
45
55
|
"@dnd-kit/core": "^6.3.1",
|
|
@@ -55,7 +65,9 @@
|
|
|
55
65
|
},
|
|
56
66
|
"scripts": {
|
|
57
67
|
"lint": "eslint .",
|
|
58
|
-
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
68
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:ci": "vitest run --no-file-parallelism",
|
|
59
71
|
"build": "tsup",
|
|
60
72
|
"clean": "rm -rf dist"
|
|
61
73
|
},
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Button — 254 consuming files, previously zero tests.
|
|
3
|
+
*
|
|
4
|
+
* These assert the contract every consumer relies on, not appearance:
|
|
5
|
+
* the base class survives a caller `className`, the ref reaches the
|
|
6
|
+
* element, rest props land on the DOM node, and loading implies disabled.
|
|
7
|
+
*/
|
|
8
|
+
import { createRef } from 'react'
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
10
|
+
import { render, screen } from '@testing-library/react'
|
|
11
|
+
import userEvent from '@testing-library/user-event'
|
|
12
|
+
import { Button } from '../button'
|
|
13
|
+
|
|
14
|
+
describe('Button', () => {
|
|
15
|
+
it('renders its children in a <button>', () => {
|
|
16
|
+
render(<Button>Save</Button>)
|
|
17
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('defaults to type="button" so it never submits a form by accident', () => {
|
|
21
|
+
render(<Button>Save</Button>)
|
|
22
|
+
expect(screen.getByRole('button')).toHaveAttribute('type', 'button')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('honours an explicit type', () => {
|
|
26
|
+
render(<Button type="submit">Save</Button>)
|
|
27
|
+
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('defaults to the primary variant at md', () => {
|
|
31
|
+
render(<Button>Save</Button>)
|
|
32
|
+
const btn = screen.getByRole('button')
|
|
33
|
+
expect(btn).toHaveClass('btn', 'btn-primary')
|
|
34
|
+
// md is the default and maps to no size modifier.
|
|
35
|
+
expect(btn.className).not.toMatch(/size-(sm|lg)/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it.each([
|
|
39
|
+
['primary', 'btn-primary'],
|
|
40
|
+
['secondary', 'btn-secondary'],
|
|
41
|
+
['outline', 'btn-outline'],
|
|
42
|
+
['ghost', 'btn-ghost'],
|
|
43
|
+
['destructive', 'btn-destructive'],
|
|
44
|
+
['destructive-soft', 'btn-destructive-soft'],
|
|
45
|
+
['icon', 'btn-icon'],
|
|
46
|
+
['toolbar', 'btn-toolbar'],
|
|
47
|
+
] as const)('maps variant=%s to .%s', (variant, expected) => {
|
|
48
|
+
render(<Button variant={variant}>x</Button>)
|
|
49
|
+
expect(screen.getByRole('button')).toHaveClass('btn', expected)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it.each([
|
|
53
|
+
['sm', 'size-sm'],
|
|
54
|
+
['lg', 'size-lg'],
|
|
55
|
+
] as const)('maps size=%s to .%s', (size, expected) => {
|
|
56
|
+
render(<Button size={size}>x</Button>)
|
|
57
|
+
expect(screen.getByRole('button')).toHaveClass(expected)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('applies is-circle only when shape=circle AND variant=icon', () => {
|
|
61
|
+
const { rerender } = render(
|
|
62
|
+
<Button shape="circle" variant="icon">
|
|
63
|
+
x
|
|
64
|
+
</Button>,
|
|
65
|
+
)
|
|
66
|
+
expect(screen.getByRole('button')).toHaveClass('is-circle')
|
|
67
|
+
|
|
68
|
+
rerender(
|
|
69
|
+
<Button shape="circle" variant="primary">
|
|
70
|
+
x
|
|
71
|
+
</Button>,
|
|
72
|
+
)
|
|
73
|
+
expect(screen.getByRole('button')).not.toHaveClass('is-circle')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('MERGES a caller className rather than replacing the base classes', () => {
|
|
77
|
+
// Five other primitives use `className ??` replace semantics, which
|
|
78
|
+
// silently deletes their base layout. Button must not join them.
|
|
79
|
+
render(<Button className="w-full">x</Button>)
|
|
80
|
+
expect(screen.getByRole('button')).toHaveClass('btn', 'btn-primary', 'w-full')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('disables itself while loading and flags .is-loading', () => {
|
|
84
|
+
render(<Button loading>Saving</Button>)
|
|
85
|
+
const btn = screen.getByRole('button')
|
|
86
|
+
expect(btn).toBeDisabled()
|
|
87
|
+
expect(btn).toHaveClass('is-loading')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('does not fire onClick while loading', async () => {
|
|
91
|
+
const onClick = vi.fn()
|
|
92
|
+
render(
|
|
93
|
+
<Button loading onClick={onClick}>
|
|
94
|
+
Saving
|
|
95
|
+
</Button>,
|
|
96
|
+
)
|
|
97
|
+
await userEvent.click(screen.getByRole('button'))
|
|
98
|
+
expect(onClick).not.toHaveBeenCalled()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('fires onClick when enabled', async () => {
|
|
102
|
+
const onClick = vi.fn()
|
|
103
|
+
render(<Button onClick={onClick}>Save</Button>)
|
|
104
|
+
await userEvent.click(screen.getByRole('button'))
|
|
105
|
+
expect(onClick).toHaveBeenCalledTimes(1)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('forwards a ref to the underlying <button>', () => {
|
|
109
|
+
const ref = createRef<HTMLButtonElement>()
|
|
110
|
+
render(<Button ref={ref}>x</Button>)
|
|
111
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('spreads rest props onto the DOM node', () => {
|
|
115
|
+
render(
|
|
116
|
+
<Button aria-describedby="hint" data-testid="cta">
|
|
117
|
+
x
|
|
118
|
+
</Button>,
|
|
119
|
+
)
|
|
120
|
+
const btn = screen.getByTestId('cta')
|
|
121
|
+
expect(btn).toHaveAttribute('aria-describedby', 'hint')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('renders leading and trailing icons around the label', () => {
|
|
125
|
+
render(
|
|
126
|
+
<Button
|
|
127
|
+
leadingIcon={<span data-testid="lead" />}
|
|
128
|
+
trailingIcon={<span data-testid="trail" />}
|
|
129
|
+
>
|
|
130
|
+
Label
|
|
131
|
+
</Button>,
|
|
132
|
+
)
|
|
133
|
+
expect(screen.getByTestId('lead')).toBeInTheDocument()
|
|
134
|
+
expect(screen.getByTestId('trail')).toBeInTheDocument()
|
|
135
|
+
expect(screen.getByRole('button')).toHaveTextContent('Label')
|
|
136
|
+
})
|
|
137
|
+
})
|