@tangle-network/brand 1.28.0 → 1.30.0
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/CHANGELOG.md +43 -0
- package/README.md +3 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/src/palette.generated.ts +4 -4
- package/src/styles/legacy-light.css +60 -45
- package/src/styles/named-themes.css +12 -12
- package/src/styles/named-themes.test.ts +1 -1
- package/src/styles/surface-ladder.test.ts +104 -37
- package/src/styles/system.css +23 -15
- package/src/styles/theme.css +4 -6
- package/src/styles/tokens.css +96 -91
- package/src/styles/tokens.test.ts +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.30.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 29e2a49: One loop period, one shadow colour and one radius ramp.
|
|
8
|
+
|
|
9
|
+
- `--duration-ambient` (2000ms) and `--motion-ambient` (ease-in-out) are the only loop period on a page; the shared `tangle-breathe` keyframe breathes a running mark on them, opacity only. Reduced motion keeps that period, so the essential "still working" signal survives.
|
|
10
|
+
- `--shadow-color` is the one shadow colour, as HSL channels: black in dark, `220 40% 8%` in light. Every spine shadow, the agents and hospitality themes and system.css draw in it; hospitality's forest-tinted shadow becomes the light shadow colour. The dark `--shadow-overlay` no longer inverts into a white halo.
|
|
11
|
+
- Radius roles are one ramp: `--radius-tag` 4, `--radius-control` 8, `--radius-card` 12, `--radius-container` 16, `--radius-pill`. `--radius-chip`, `--radius-field` and `--radius-panel` keep their values as aliases of control, card and container.
|
|
12
|
+
|
|
13
|
+
## 1.29.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 4678ab2: The canonical spine (`:root`, `.dark`, `.light`, `data-theme` `dark`, `light`
|
|
18
|
+
and `tangle-dark`) has a real surface ladder. `--hsl-raised` is now the spine's
|
|
19
|
+
L3 plane and `--hsl-raised-2` its L4 plane, instead of both repeating the card,
|
|
20
|
+
and every adjacent plane and well is at least 4.4 OKLCH L apart (gated in
|
|
21
|
+
`surface-ladder.test.ts`):
|
|
22
|
+
|
|
23
|
+
- Dark: page `#0a0a14`, card `#191826`, raised `#252237`, raised-2 and popover
|
|
24
|
+
`#312e47`. A field in a raised card takes the card colour instead of the page's
|
|
25
|
+
near-black well, and dark cards and menus carry the 1px top highlight
|
|
26
|
+
(`--edge-highlight`).
|
|
27
|
+
- Light: page `#eceef3`, card white, raised `#edeff4`, raised-2 white. A field in
|
|
28
|
+
a raised card is white.
|
|
29
|
+
|
|
30
|
+
`--hsl-secondary` and `--hsl-popover` moved with the ladder (dark `#221f33` to
|
|
31
|
+
`#252237`, `#2c2942` to `#312e47`; light `#f2f3f8` to `#edeff4`). Named themes
|
|
32
|
+
other than Hospitality keep their flat ladder.
|
|
33
|
+
|
|
34
|
+
One hairline: `--border-subtle` and `--border-default` now alias `--hsl-border`
|
|
35
|
+
in the spine, so every divider, card edge and table rule renders one colour. The
|
|
36
|
+
dark hairline is `#37364a` (1.5:1 on the card, as documented; it measured 1.2:1).
|
|
37
|
+
`--glass-border-hover` now follows `--border-hover`.
|
|
38
|
+
|
|
39
|
+
The `--font-size-xs`, `-sm`, `-base` and `-lg` tokens now equal Tailwind's
|
|
40
|
+
`text-xs`, `text-sm`, `text-base` and `text-lg` (12, 14, 16 and 18px; they were
|
|
41
|
+
11, 12, 15 and 16px), so `sm` means 14px in CSS and in a class. `text-eyebrow`,
|
|
42
|
+
the `Heading` eyebrow and subsection variants and `.tangle-api-meta` keep their
|
|
43
|
+
rendered sizes on the new steps. Code that relied on the old values reads
|
|
44
|
+
`--font-size-xs` for 12px captions and `--font-size-base` for 16px emphasis.
|
|
45
|
+
|
|
3
46
|
## 1.28.0
|
|
4
47
|
|
|
5
48
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ The Agent App light-default contract is an explicit exception to Brand's default
|
|
|
82
82
|
/* Import globals.css separately only when the host already intends those globals. */
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
Choose this **instead of** `styles`, `tokens.css` and `named-themes.css`; do not stack the two default contracts. The projection contains canonical declarations with a zero-specificity light root default, keeps all explicit mode selectors, and omits `--radius`, which the legacy consumer deliberately leaves to its host. It includes no global component rules
|
|
85
|
+
Choose this **instead of** `styles`, `tokens.css` and `named-themes.css`; do not stack the two default contracts. The projection contains canonical declarations with a zero-specificity light root default, keeps all explicit mode selectors, and omits `--radius`, which the legacy consumer deliberately leaves to its host. It includes no global component rules or automatic ladder/system imports; its one keyframe, `tangle-breathe`, runs only where a component names it.
|
|
86
86
|
|
|
87
87
|
This export is the shared migration boundary, **not a claim that Agent App has already migrated** and not a replacement for that package's entire stylesheet. Its component-specific variables, neutral-ramp API, keyframes and structural rules remain consumer-owned until a separate, explicit migration reconciles them. Remove duplicate shared palette declarations in that migration rather than placing competing global token systems on the same page.
|
|
88
88
|
|
|
@@ -123,9 +123,9 @@ Renderers that cannot read CSS variables (PDF, canvas, WebGL, email, the `theme-
|
|
|
123
123
|
|
|
124
124
|
The base surface ladder is the one GTM proved out: dark is indigo-lifted `#0a0a14` / `#191826` / `#221f33` / `#2c2942`; light is a cool `#eceef3` canvas, white paper cards and overlays, and `#f1f2f7` nested wells. The cast stays below 0.1 chroma and ink stays achromatic, so indigo still reads as the interaction accent, not the base field. Existing named products may supply deliberate surface overrides.
|
|
125
125
|
|
|
126
|
-
Chrome radii are 6/8/10/12px in both base modes; the composer has its own 26px role. Inter (variable, then static) leads body and display stacks, with Geist fallbacks; Geist Mono leads code with JetBrains Mono and system fallbacks. Runtime font/radius registrations retain their public override names; tests keep Tailwind registration defaults aligned with the canonical declarations.
|
|
126
|
+
Chrome radii are 6/8/10/12px in both base modes; the composer has its own 26px role. Role radii are one ramp: `--radius-tag` 4, `--radius-control` 8, `--radius-card` 12, `--radius-container` 16, and `--radius-pill` for every chip; `--radius-chip`, `--radius-field` and `--radius-panel` are older names of control, card and container. Every shadow is drawn in `--shadow-color`, HSL channels that are black in dark and `220 40% 8%` in light; a named theme retints its shadows by setting that one token. Inter (variable, then static) leads body and display stacks, with Geist fallbacks; Geist Mono leads code with JetBrains Mono and system fallbacks. Runtime font/radius registrations retain their public override names; tests keep Tailwind registration defaults aligned with the canonical declarations.
|
|
127
127
|
|
|
128
|
-
Existing `--transition-*` values are preserved. The shared `--duration-*`, `--ease-*` and `--motion-*` vocabulary comes from the existing Agent App contract. It does not apply animations automatically. Reduced motion zeroes those durations and re-resolves composites at theme boundaries. The adjacent `--interaction-*` roles are elapsed acknowledgment/loading thresholds for existing product instrumentation, not animation durations; reduced motion does not change them.
|
|
128
|
+
Existing `--transition-*` values are preserved. The shared `--duration-*`, `--ease-*` and `--motion-*` vocabulary comes from the existing Agent App contract. It does not apply animations automatically. Reduced motion zeroes those durations and re-resolves composites at theme boundaries. `--duration-ambient` (2000 ms; `--motion-ambient` adds ease-in-out) is the only loop period on a page: a running mark breathes on it with the `tangle-breathe` keyframe, opacity only. Reduced motion keeps that one, because it is the essential "still working" signal and moves nothing. The adjacent `--interaction-*` roles are elapsed acknowledgment/loading thresholds for existing product instrumentation, not animation durations; reduced motion does not change them.
|
|
129
129
|
|
|
130
130
|
Focus uses `--focus-border`, `--focus-halo`, their danger variants, and `--border-strong`, derived from the local ring/border palette. UI's `focusField` / `focusRing` consume them. `globals.css` supplies the existing fallback for controls without their own focus treatment.
|
|
131
131
|
|
package/dist/index.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export declare const palettes: {
|
|
|
54
54
|
readonly canvas: "#eceef3";
|
|
55
55
|
readonly card: "#ffffff";
|
|
56
56
|
readonly well: "#f5f6fa";
|
|
57
|
-
readonly nested: "#
|
|
57
|
+
readonly nested: "#edeff4";
|
|
58
58
|
readonly overlay: "#ffffff";
|
|
59
59
|
readonly deepest: "#ffffff";
|
|
60
60
|
readonly hairline: "#c8c7d6";
|
|
@@ -87,10 +87,10 @@ export declare const palettes: {
|
|
|
87
87
|
readonly canvas: "#0a0a14";
|
|
88
88
|
readonly card: "#191826";
|
|
89
89
|
readonly well: "#11101e";
|
|
90
|
-
readonly nested: "#
|
|
91
|
-
readonly overlay: "#
|
|
90
|
+
readonly nested: "#252237";
|
|
91
|
+
readonly overlay: "#312e47";
|
|
92
92
|
readonly deepest: "#040408";
|
|
93
|
-
readonly hairline: "#
|
|
93
|
+
readonly hairline: "#37364a";
|
|
94
94
|
readonly ink: "#e6e6e6";
|
|
95
95
|
readonly inkSecondary: "#c7c7c7";
|
|
96
96
|
readonly inkMuted: "#b5b5b5";
|
package/dist/index.js
CHANGED
|
@@ -95,7 +95,7 @@ const palettes = {
|
|
|
95
95
|
canvas: "#eceef3",
|
|
96
96
|
card: "#ffffff",
|
|
97
97
|
well: "#f5f6fa",
|
|
98
|
-
nested: "#
|
|
98
|
+
nested: "#edeff4",
|
|
99
99
|
overlay: "#ffffff",
|
|
100
100
|
deepest: "#ffffff",
|
|
101
101
|
hairline: "#c8c7d6",
|
|
@@ -128,10 +128,10 @@ const palettes = {
|
|
|
128
128
|
canvas: "#0a0a14",
|
|
129
129
|
card: "#191826",
|
|
130
130
|
well: "#11101e",
|
|
131
|
-
nested: "#
|
|
132
|
-
overlay: "#
|
|
131
|
+
nested: "#252237",
|
|
132
|
+
overlay: "#312e47",
|
|
133
133
|
deepest: "#040408",
|
|
134
|
-
hairline: "#
|
|
134
|
+
hairline: "#37364a",
|
|
135
135
|
ink: "#e6e6e6",
|
|
136
136
|
inkSecondary: "#c7c7c7",
|
|
137
137
|
inkMuted: "#b5b5b5",
|
package/package.json
CHANGED
package/src/palette.generated.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const palettes = {
|
|
|
43
43
|
canvas: "#eceef3",
|
|
44
44
|
card: "#ffffff",
|
|
45
45
|
well: "#f5f6fa",
|
|
46
|
-
nested: "#
|
|
46
|
+
nested: "#edeff4",
|
|
47
47
|
overlay: "#ffffff",
|
|
48
48
|
deepest: "#ffffff",
|
|
49
49
|
hairline: "#c8c7d6",
|
|
@@ -76,10 +76,10 @@ export const palettes = {
|
|
|
76
76
|
canvas: "#0a0a14",
|
|
77
77
|
card: "#191826",
|
|
78
78
|
well: "#11101e",
|
|
79
|
-
nested: "#
|
|
80
|
-
overlay: "#
|
|
79
|
+
nested: "#252237",
|
|
80
|
+
overlay: "#312e47",
|
|
81
81
|
deepest: "#040408",
|
|
82
|
-
hairline: "#
|
|
82
|
+
hairline: "#37364a",
|
|
83
83
|
ink: "#e6e6e6",
|
|
84
84
|
inkSecondary: "#c7c7c7",
|
|
85
85
|
inkMuted: "#b5b5b5",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* GENERATED by scripts/gen-compat.mjs. Do not edit.
|
|
2
2
|
* Source: tokens.css + named-themes.css
|
|
3
|
-
* SHA-256:
|
|
3
|
+
* SHA-256: 83dde77644ce502513ced146ea7ed7dad93fc736195530c0c2bef82849bf1f21
|
|
4
4
|
* Light-default compatibility; all explicit modes remain canonical.
|
|
5
5
|
*/
|
|
6
6
|
:where(:root),
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
--hsl-background: 240 33% 6%;
|
|
28
28
|
--hsl-input: 244 29% 9%;
|
|
29
29
|
--hsl-card: 244 23% 12%;
|
|
30
|
-
--hsl-secondary: 249 24%
|
|
31
|
-
--hsl-popover: 247
|
|
30
|
+
--hsl-secondary: 249 24% 17.5%;
|
|
31
|
+
--hsl-popover: 247 21% 23%;
|
|
32
32
|
--hsl-muted: var(--hsl-secondary);
|
|
33
33
|
--hsl-accent: var(--hsl-secondary);
|
|
34
34
|
--md3-surface-dim: #040408;
|
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
--md3-on-surface: var(--text-primary);
|
|
70
70
|
--md3-on-surface-variant: var(--text-muted);
|
|
71
71
|
--md3-outline: #8e8e8e;
|
|
72
|
-
--hsl-border:
|
|
72
|
+
--hsl-border: 244 16% 25%;
|
|
73
73
|
--md3-outline-variant: hsl(var(--hsl-border));
|
|
74
74
|
--sidebar-border: var(--hsl-border);
|
|
75
|
-
--border-subtle:
|
|
76
|
-
--border-default:
|
|
75
|
+
--border-subtle: hsl(var(--hsl-border));
|
|
76
|
+
--border-default: hsl(var(--hsl-border));
|
|
77
77
|
--border-hover: rgba(255, 255, 255, 0.28);
|
|
78
78
|
--hsl-primary: 246 54% 51%;
|
|
79
79
|
--hsl-primary-foreground: 0 0% 100%;
|
|
@@ -250,7 +250,7 @@
|
|
|
250
250
|
--glass-bg: var(--depth-2);
|
|
251
251
|
--glass-bg-strong: var(--depth-3);
|
|
252
252
|
--glass-border: var(--border-subtle);
|
|
253
|
-
--glass-border-hover: var(--border-
|
|
253
|
+
--glass-border-hover: var(--border-hover);
|
|
254
254
|
--glass-blur: 0px;
|
|
255
255
|
--mesh-teal: transparent;
|
|
256
256
|
--mesh-violet: transparent;
|
|
@@ -272,12 +272,12 @@
|
|
|
272
272
|
--code-success: var(--surface-success-text);
|
|
273
273
|
--code-comment: var(--syntax-comment);
|
|
274
274
|
--code-error: var(--syntax-error);
|
|
275
|
-
--shadow-
|
|
276
|
-
--shadow-
|
|
275
|
+
--shadow-color: 0 0% 0%;
|
|
276
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.3);
|
|
277
|
+
--shadow-dropdown: 0 8px 30px hsl(var(--shadow-color) / 0.45);
|
|
277
278
|
--shadow-glow: none;
|
|
278
|
-
--shadow-accent:
|
|
279
|
-
--shadow-overlay: 0 2px 6px hsl(var(--hsl-
|
|
280
|
-
0 16px 40px hsl(var(--hsl-foreground) / 0.16);
|
|
279
|
+
--shadow-accent: var(--shadow-card);
|
|
280
|
+
--shadow-overlay: 0 2px 6px hsl(var(--shadow-color) / 0.3), 0 16px 40px hsl(var(--shadow-color) / 0.5);
|
|
281
281
|
--scrim: rgb(0 0 0 / 0.4);
|
|
282
282
|
--scrim-strong: rgb(0 0 0 / 0.72);
|
|
283
283
|
--on-media: #ffffff;
|
|
@@ -292,9 +292,12 @@
|
|
|
292
292
|
--radius-full: 999px;
|
|
293
293
|
--radius-composer: 26px;
|
|
294
294
|
--radius-tag: 4px;
|
|
295
|
-
--radius-
|
|
296
|
-
--radius-
|
|
297
|
-
--radius-
|
|
295
|
+
--radius-control: var(--radius-md);
|
|
296
|
+
--radius-card: var(--radius-xl);
|
|
297
|
+
--radius-container: 16px;
|
|
298
|
+
--radius-chip: var(--radius-control);
|
|
299
|
+
--radius-field: var(--radius-card);
|
|
300
|
+
--radius-panel: var(--radius-container);
|
|
298
301
|
--radius-cover: 20px;
|
|
299
302
|
--radius-sheet: 24px;
|
|
300
303
|
--control-height: 2.25rem;
|
|
@@ -311,10 +314,10 @@
|
|
|
311
314
|
--shell-inset-gutter: 0.5rem;
|
|
312
315
|
--transcript-font-size: 1rem;
|
|
313
316
|
--transcript-line-height: var(--line-height-base);
|
|
314
|
-
--font-size-xs: 0.
|
|
315
|
-
--font-size-sm: 0.
|
|
316
|
-
--font-size-base:
|
|
317
|
-
--font-size-lg:
|
|
317
|
+
--font-size-xs: 0.75rem;
|
|
318
|
+
--font-size-sm: 0.875rem;
|
|
319
|
+
--font-size-base: 1rem;
|
|
320
|
+
--font-size-lg: 1.125rem;
|
|
318
321
|
--font-size-xl: 1.25rem;
|
|
319
322
|
--font-size-2xl: 1.5rem;
|
|
320
323
|
--font-size-3xl: 1.875rem;
|
|
@@ -377,6 +380,8 @@
|
|
|
377
380
|
--motion-dismiss: var(--duration-instant) var(--ease-exit);
|
|
378
381
|
--motion-stream: var(--duration-stream) var(--ease-expo);
|
|
379
382
|
--motion-arrive: var(--duration-arrive) var(--ease-expo);
|
|
383
|
+
--duration-ambient: 2000ms;
|
|
384
|
+
--motion-ambient: var(--duration-ambient) ease-in-out;
|
|
380
385
|
--interaction-feedback-budget: 100ms;
|
|
381
386
|
--interaction-pending-delay: 200ms;
|
|
382
387
|
--interaction-long-work-delay: 1000ms;
|
|
@@ -408,7 +413,7 @@
|
|
|
408
413
|
--hsl-background: 223 23% 94%;
|
|
409
414
|
--hsl-input: 225 29% 97%;
|
|
410
415
|
--hsl-card: 0 0% 100%;
|
|
411
|
-
--hsl-secondary:
|
|
416
|
+
--hsl-secondary: 228 25% 94.5%;
|
|
412
417
|
--hsl-popover: var(--hsl-card);
|
|
413
418
|
--md3-surface-dim: #dcdee6;
|
|
414
419
|
--md3-surface-container-lowest: hsl(var(--hsl-card));
|
|
@@ -423,8 +428,6 @@
|
|
|
423
428
|
--text-dim: #686868;
|
|
424
429
|
--md3-outline: #787878;
|
|
425
430
|
--hsl-border: 244 16% 81%;
|
|
426
|
-
--border-subtle: rgba(0, 0, 0, 0.10);
|
|
427
|
-
--border-default: rgba(0, 0, 0, 0.14);
|
|
428
431
|
--border-hover: rgba(0, 0, 0, 0.24);
|
|
429
432
|
--hsl-ring: var(--hsl-primary);
|
|
430
433
|
--brand-primary: #6366F1;
|
|
@@ -535,10 +538,10 @@
|
|
|
535
538
|
--run-mix-cancelled: #515367;
|
|
536
539
|
--glass-bg-strong: var(--depth-1);
|
|
537
540
|
--code-header-bg: color-mix(in srgb, hsl(var(--hsl-muted)) 50%, transparent);
|
|
538
|
-
--shadow-
|
|
539
|
-
--shadow-
|
|
540
|
-
--shadow-
|
|
541
|
-
--shadow-
|
|
541
|
+
--shadow-color: 220 40% 8%;
|
|
542
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.06);
|
|
543
|
+
--shadow-dropdown: 0 8px 30px hsl(var(--shadow-color) / 0.1);
|
|
544
|
+
--shadow-overlay: 0 2px 6px hsl(var(--shadow-color) / 0.1), 0 16px 40px hsl(var(--shadow-color) / 0.16);
|
|
542
545
|
}
|
|
543
546
|
:where(:root),
|
|
544
547
|
.dark,
|
|
@@ -558,17 +561,17 @@
|
|
|
558
561
|
[data-sandbox-ui],
|
|
559
562
|
[data-sandbox-theme],
|
|
560
563
|
[data-theme] {
|
|
561
|
-
--hsl-raised: var(--hsl-
|
|
562
|
-
--hsl-raised-2: var(--hsl-
|
|
564
|
+
--hsl-raised: var(--hsl-secondary);
|
|
565
|
+
--hsl-raised-2: var(--hsl-popover);
|
|
563
566
|
--well-page: color-mix(in oklab, hsl(var(--hsl-background)) 70%, black);
|
|
564
567
|
--well-card: var(--well-page);
|
|
565
|
-
--well-raised: var(--
|
|
566
|
-
--well-raised-2: var(--well-
|
|
568
|
+
--well-raised: hsl(var(--hsl-card));
|
|
569
|
+
--well-raised-2: var(--well-raised);
|
|
567
570
|
--well: var(--well-page);
|
|
568
571
|
--field-border: color-mix(in oklab, hsl(var(--hsl-border)), hsl(var(--hsl-foreground)) 45%);
|
|
569
572
|
--shadow-raised: var(--shadow-card);
|
|
570
573
|
--shadow-well: 0 0 0 0 transparent;
|
|
571
|
-
--edge-highlight:
|
|
574
|
+
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
572
575
|
--ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
|
|
573
576
|
--ix-hover-border: hsl(var(--hsl-ring) / 0.7);
|
|
574
577
|
--ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
|
|
@@ -589,8 +592,15 @@
|
|
|
589
592
|
--ix-hover-bg: hsl(var(--hsl-ring) / 0.07);
|
|
590
593
|
--well-page: hsl(var(--hsl-card));
|
|
591
594
|
--well-card: hsl(var(--hsl-background));
|
|
592
|
-
--well-raised:
|
|
593
|
-
--
|
|
595
|
+
--well-raised-2: var(--well-card);
|
|
596
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
597
|
+
}
|
|
598
|
+
[data-theme="aubergine"], [data-theme="aubergine-light"], [data-theme="arena"], [data-theme="arena-light"], [data-theme="super"], [data-theme="super-light"], [data-theme="tangle-light"], [data-theme="agents"], [data-theme="agents-light"], [data-theme="website"], [data-theme="intelligence"] {
|
|
599
|
+
--hsl-raised: var(--hsl-card);
|
|
600
|
+
--hsl-raised-2: var(--hsl-card);
|
|
601
|
+
--well-raised: var(--well-card);
|
|
602
|
+
--well-raised-2: var(--well-card);
|
|
603
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
594
604
|
}
|
|
595
605
|
[data-slot="card"] :where(*) {
|
|
596
606
|
--card: var(--hsl-raised);
|
|
@@ -635,6 +645,11 @@
|
|
|
635
645
|
--chart-tier-a: #5b5bd6;
|
|
636
646
|
--chart-tier-s: #272962;
|
|
637
647
|
}
|
|
648
|
+
@keyframes tangle-breathe {
|
|
649
|
+
50% {
|
|
650
|
+
opacity: 0.45;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
638
653
|
@media (prefers-reduced-motion: reduce) {
|
|
639
654
|
:where(:root),
|
|
640
655
|
.dark,
|
|
@@ -746,7 +761,7 @@
|
|
|
746
761
|
--text-hero: var(--font-size-hero);
|
|
747
762
|
--text-page: var(--font-size-3xl);
|
|
748
763
|
--text-section: var(--font-size-xl);
|
|
749
|
-
--text-eyebrow: var(--font-size-
|
|
764
|
+
--text-eyebrow: var(--font-size-xs);
|
|
750
765
|
--text-control-sm: var(--control-text-sm);
|
|
751
766
|
--text-control-md: var(--control-text-md);
|
|
752
767
|
--text-control-lg: var(--control-text-lg);
|
|
@@ -1257,9 +1272,9 @@
|
|
|
1257
1272
|
--field-border: #798479;
|
|
1258
1273
|
--border-accent: rgba(40, 88, 66, 0.20);
|
|
1259
1274
|
--border-accent-hover: rgba(40, 88, 66, 0.36);
|
|
1260
|
-
--shadow-card: 0 1px 2px
|
|
1261
|
-
--shadow-raised: 0 1px 1px
|
|
1262
|
-
--shadow-well: inset 0 1px 2px
|
|
1275
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.07), 0 4px 14px -4px hsl(var(--shadow-color) / 0.1);
|
|
1276
|
+
--shadow-raised: 0 1px 1px hsl(var(--shadow-color) / 0.05), 0 2px 6px -2px hsl(var(--shadow-color) / 0.07);
|
|
1277
|
+
--shadow-well: inset 0 1px 2px hsl(var(--shadow-color) / 0.06);
|
|
1263
1278
|
--edge-highlight: 0 0 0 0 transparent;
|
|
1264
1279
|
--accent-text: #285842;
|
|
1265
1280
|
--accent-surface-soft: rgba(40, 88, 66, 0.05);
|
|
@@ -1359,9 +1374,9 @@
|
|
|
1359
1374
|
--field-border: #939c94;
|
|
1360
1375
|
--border-accent: rgba(118, 183, 145, 0.35);
|
|
1361
1376
|
--border-accent-hover: rgba(118, 183, 145, 0.50);
|
|
1362
|
-
--shadow-card: 0 1px 2px
|
|
1363
|
-
--shadow-raised: 0 1px 2px
|
|
1364
|
-
--shadow-well: inset 0 1px 2px
|
|
1377
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.45), 0 10px 24px -14px hsl(var(--shadow-color) / 0.6);
|
|
1378
|
+
--shadow-raised: 0 1px 2px hsl(var(--shadow-color) / 0.35);
|
|
1379
|
+
--shadow-well: inset 0 1px 2px hsl(var(--shadow-color) / 0.5);
|
|
1365
1380
|
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
1366
1381
|
--accent-text: #b2dbc2;
|
|
1367
1382
|
--accent-surface-soft: rgba(118, 183, 145, 0.07);
|
|
@@ -1443,8 +1458,8 @@
|
|
|
1443
1458
|
--border-default: #49474e;
|
|
1444
1459
|
--border-hover: #625f69;
|
|
1445
1460
|
--shadow-card: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
|
1446
|
-
--shadow-dropdown: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px
|
|
1447
|
-
--shadow-overlay: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px
|
|
1461
|
+
--shadow-dropdown: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px hsl(var(--shadow-color) / 0.5);
|
|
1462
|
+
--shadow-overlay: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px hsl(var(--shadow-color) / 0.6);
|
|
1448
1463
|
}
|
|
1449
1464
|
[data-theme="agents-light"] {
|
|
1450
1465
|
--bg-card: #ffffff;
|
|
@@ -1497,9 +1512,9 @@
|
|
|
1497
1512
|
--border-subtle: #dbd8e0;
|
|
1498
1513
|
--border-default: #d0cdd7;
|
|
1499
1514
|
--border-hover: #bcbac7;
|
|
1500
|
-
--shadow-card: 0 0 0 1px
|
|
1501
|
-
--shadow-dropdown: 0 0 0 1px
|
|
1502
|
-
--shadow-overlay: 0 8px 28px
|
|
1515
|
+
--shadow-card: 0 0 0 1px hsl(var(--shadow-color) / 0.03), 0 2px 6px hsl(var(--shadow-color) / 0.05), 0 4px 8px hsl(var(--shadow-color) / 0.08);
|
|
1516
|
+
--shadow-dropdown: 0 0 0 1px hsl(var(--shadow-color) / 0.04), 0 6px 16px hsl(var(--shadow-color) / 0.12);
|
|
1517
|
+
--shadow-overlay: 0 8px 28px hsl(var(--shadow-color) / 0.28);
|
|
1503
1518
|
}
|
|
1504
1519
|
[data-theme="website"]:where(:not(.dark)) {
|
|
1505
1520
|
--hsl-background: 240 42.9% 97.3%;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
--text-hero: var(--font-size-hero);
|
|
19
19
|
--text-page: var(--font-size-3xl);
|
|
20
20
|
--text-section: var(--font-size-xl);
|
|
21
|
-
--text-eyebrow: var(--font-size-
|
|
21
|
+
--text-eyebrow: var(--font-size-xs);
|
|
22
22
|
--text-control-sm: var(--control-text-sm);
|
|
23
23
|
--text-control-md: var(--control-text-md);
|
|
24
24
|
--text-control-lg: var(--control-text-lg);
|
|
@@ -393,9 +393,9 @@
|
|
|
393
393
|
--border-hover: rgba(23, 38, 29, 0.28); --border-strong: #798479;
|
|
394
394
|
--field-border: #798479;
|
|
395
395
|
--border-accent: rgba(40, 88, 66, 0.20); --border-accent-hover: rgba(40, 88, 66, 0.36);
|
|
396
|
-
--shadow-card: 0 1px 2px
|
|
397
|
-
--shadow-raised: 0 1px 1px
|
|
398
|
-
--shadow-well: inset 0 1px 2px
|
|
396
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.07), 0 4px 14px -4px hsl(var(--shadow-color) / 0.1);
|
|
397
|
+
--shadow-raised: 0 1px 1px hsl(var(--shadow-color) / 0.05), 0 2px 6px -2px hsl(var(--shadow-color) / 0.07);
|
|
398
|
+
--shadow-well: inset 0 1px 2px hsl(var(--shadow-color) / 0.06);
|
|
399
399
|
--edge-highlight: 0 0 0 0 transparent;
|
|
400
400
|
--accent-text: #285842;
|
|
401
401
|
--accent-surface-soft: rgba(40, 88, 66, 0.05); --accent-surface-strong: rgba(40, 88, 66, 0.13);
|
|
@@ -443,9 +443,9 @@
|
|
|
443
443
|
--border-hover: rgba(227, 233, 229, 0.28); --border-strong: #939c94;
|
|
444
444
|
--field-border: #939c94;
|
|
445
445
|
--border-accent: rgba(118, 183, 145, 0.35); --border-accent-hover: rgba(118, 183, 145, 0.50);
|
|
446
|
-
--shadow-card: 0 1px 2px
|
|
447
|
-
--shadow-raised: 0 1px 2px
|
|
448
|
-
--shadow-well: inset 0 1px 2px
|
|
446
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.45), 0 10px 24px -14px hsl(var(--shadow-color) / 0.6);
|
|
447
|
+
--shadow-raised: 0 1px 2px hsl(var(--shadow-color) / 0.35);
|
|
448
|
+
--shadow-well: inset 0 1px 2px hsl(var(--shadow-color) / 0.5);
|
|
449
449
|
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
450
450
|
/* Lifted from #99caad so selected text on a primary tint still clears 4.5:1
|
|
451
451
|
* over the lighter raised plane (L 37.9). */
|
|
@@ -517,8 +517,8 @@
|
|
|
517
517
|
--border-default: #49474e;
|
|
518
518
|
--border-hover: #625f69;
|
|
519
519
|
--shadow-card: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
|
520
|
-
--shadow-dropdown: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px
|
|
521
|
-
--shadow-overlay: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px
|
|
520
|
+
--shadow-dropdown: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px hsl(var(--shadow-color) / 0.5);
|
|
521
|
+
--shadow-overlay: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px hsl(var(--shadow-color) / 0.6);
|
|
522
522
|
}
|
|
523
523
|
[data-theme="agents-light"] {
|
|
524
524
|
--bg-card: #ffffff;
|
|
@@ -571,9 +571,9 @@
|
|
|
571
571
|
--border-subtle: #dbd8e0;
|
|
572
572
|
--border-default: #d0cdd7;
|
|
573
573
|
--border-hover: #bcbac7;
|
|
574
|
-
--shadow-card: 0 0 0 1px
|
|
575
|
-
--shadow-dropdown: 0 0 0 1px
|
|
576
|
-
--shadow-overlay: 0 8px 28px
|
|
574
|
+
--shadow-card: 0 0 0 1px hsl(var(--shadow-color) / 0.03), 0 2px 6px hsl(var(--shadow-color) / 0.05), 0 4px 8px hsl(var(--shadow-color) / 0.08);
|
|
575
|
+
--shadow-dropdown: 0 0 0 1px hsl(var(--shadow-color) / 0.04), 0 6px 16px hsl(var(--shadow-color) / 0.12);
|
|
576
|
+
--shadow-overlay: 0 8px 28px hsl(var(--shadow-color) / 0.28);
|
|
577
577
|
}
|
|
578
578
|
/* agents:end */
|
|
579
579
|
|
|
@@ -301,7 +301,7 @@ for (const [name, mode] of [
|
|
|
301
301
|
it("is a white page whose cards separate by shadow", () => {
|
|
302
302
|
expect(declIn(css, "hsl-background")).toBe("0 0% 100%");
|
|
303
303
|
expect(declIn(css, "hsl-card")).toBe("0 0% 100%");
|
|
304
|
-
expect(declIn(css, "shadow-card")).toMatch(/
|
|
304
|
+
expect(declIn(css, "shadow-card")).toMatch(/hsl\(var\(--shadow-color\)/);
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
307
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { blockIn, blocksIn, contrastRatio, declIn, hexIn, hslIn, hslToRgb, oklchLightness } from "./css-test-utils";
|
|
4
|
+
import { blockIn, blocksIn, contrastRatio, declIn, hexIn, hslIn, hslToRgb, oklchLightness, resolveIn } from "./css-test-utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The surface ladder and the interaction palette.
|
|
@@ -171,14 +171,13 @@ describe("shared surface context and interaction tokens (tokens.css)", () => {
|
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it("
|
|
175
|
-
expect(declIn(body, "hsl-raised")).toBe("var(--hsl-
|
|
176
|
-
expect(declIn(body, "hsl-raised-2")).toBe("var(--hsl-
|
|
174
|
+
it("takes the raised planes from the spine's own L3 and L4 (values: the canonical suite below)", () => {
|
|
175
|
+
expect(declIn(body, "hsl-raised")).toBe("var(--hsl-secondary)");
|
|
176
|
+
expect(declIn(body, "hsl-raised-2")).toBe("var(--hsl-popover)");
|
|
177
177
|
expect(declIn(body, "well")).toBe("var(--well-page)");
|
|
178
178
|
expect(declIn(body, "shadow-raised")).toBe("var(--shadow-card)");
|
|
179
179
|
// A shadow token joins a comma list; "none" would void the list.
|
|
180
180
|
expect(declIn(body, "shadow-well")).toBe("0 0 0 0 transparent");
|
|
181
|
-
expect(declIn(body, "edge-highlight")).toBe("0 0 0 0 transparent");
|
|
182
181
|
});
|
|
183
182
|
|
|
184
183
|
it("derives the four interaction states from the focus ring at the specified alphas", () => {
|
|
@@ -223,19 +222,20 @@ describe("shared surface context and interaction tokens (tokens.css)", () => {
|
|
|
223
222
|
});
|
|
224
223
|
});
|
|
225
224
|
|
|
226
|
-
describe("Canonical surface ladder (
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
const
|
|
225
|
+
describe("Canonical surface ladder (the spine: :root, .dark, .light, dark, light, tangle-dark)", () => {
|
|
226
|
+
// Drew, 2026-10-26, on a Discovery run page built from the spine: "no color
|
|
227
|
+
// depth ladder". Raised levels painted the card and every well was one value,
|
|
228
|
+
// so a card in a card, and a field in it, read as the plane they sat on.
|
|
229
|
+
const css = rules(tokens);
|
|
230
|
+
const ladderRule = (selector: string) => {
|
|
231
231
|
const block = blocksIn(tokens, selector).find((b) => /--well-page:/.test(b));
|
|
232
|
-
if (!block) throw new Error(`no
|
|
232
|
+
if (!block) throw new Error(`no ladder rule under ${selector}`);
|
|
233
233
|
return block;
|
|
234
234
|
};
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const DARK = blockIn(tokens, ".dark");
|
|
238
|
-
const LIGHT = blockIn(tokens, ".light");
|
|
235
|
+
const shared = ladderRule("[data-theme]");
|
|
236
|
+
const lightRule = ladderRule('[data-theme$="-light"]');
|
|
237
|
+
const DARK = [shared, blockIn(tokens, ".dark")];
|
|
238
|
+
const LIGHT = [lightRule, shared, blockIn(tokens, ".light")];
|
|
239
239
|
|
|
240
240
|
type Rgb = [number, number, number];
|
|
241
241
|
const toLab = (rgb: Rgb) => {
|
|
@@ -255,37 +255,104 @@ describe("Canonical surface ladder (every theme without its own ladder)", () =>
|
|
|
255
255
|
};
|
|
256
256
|
/** `color-mix(in oklab, a, b <t>)`. */
|
|
257
257
|
const mix = (a: Rgb, b: Rgb, t: number) => fromLab(toLab(a).map((v, i) => v * (1 - t) + toLab(b)[i] * t));
|
|
258
|
-
|
|
258
|
+
/** A token as the browser paints it, including the `page + 30% black` well. */
|
|
259
|
+
const rgb = (sources: string[], token: string): Rgb => {
|
|
260
|
+
const recess = /^color-mix\(in oklab, hsl\(var\(--([a-z0-9-]+)\)\) (\d+)%, black\)$/.exec(resolveIn(sources, token));
|
|
261
|
+
if (recess) return mix(rgb(sources, recess[1]!), [0, 0, 0], 1 - Number(recess[2]) / 100);
|
|
262
|
+
return hexIn(sources, token);
|
|
263
|
+
};
|
|
264
|
+
const Lof = (sources: string[], token: string) => oklchLightness(rgb(sources, token));
|
|
265
|
+
|
|
266
|
+
const ladder: Array<[role: string, token: string, light: number, dark: number]> = [
|
|
267
|
+
["page", "hsl-background", 94.9, 15.0],
|
|
268
|
+
["card", "hsl-card", 100, 21.7],
|
|
269
|
+
["raised", "hsl-raised", 95.2, 26.6],
|
|
270
|
+
["raised-2", "hsl-raised-2", 100, 31.6],
|
|
271
|
+
["popover", "hsl-popover", 100, 31.6],
|
|
272
|
+
["well on page", "well-page", 100, 10.2],
|
|
273
|
+
["well in card", "well-card", 94.9, 10.2],
|
|
274
|
+
["well in raised", "well-raised", 100, 21.7],
|
|
275
|
+
["well in raised-2", "well-raised-2", 94.9, 21.7],
|
|
276
|
+
["hairline", "hsl-border", 83.5, 34.1],
|
|
277
|
+
];
|
|
278
|
+
for (const [role, token, lightL, darkL] of ladder) {
|
|
279
|
+
it(`${role} (--${token}) lands at L ${lightL} light / ${darkL} dark`, () => {
|
|
280
|
+
expect(Lof(LIGHT, token)).toBeCloseTo(lightL, 0);
|
|
281
|
+
expect(Lof(DARK, token)).toBeCloseTo(darkL, 0);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
259
284
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
const steps: Array<[child: string, parent: string]> = [
|
|
286
|
+
["hsl-card", "hsl-background"],
|
|
287
|
+
["hsl-raised", "hsl-card"],
|
|
288
|
+
["hsl-raised-2", "hsl-raised"],
|
|
289
|
+
["well-page", "hsl-background"],
|
|
290
|
+
["well-card", "hsl-card"],
|
|
291
|
+
["well-raised", "hsl-raised"],
|
|
292
|
+
["well-raised-2", "hsl-raised-2"],
|
|
293
|
+
];
|
|
294
|
+
it.each([
|
|
295
|
+
["light", LIGHT],
|
|
296
|
+
["dark", DARK],
|
|
297
|
+
])("steps every plane and well at least 4.4 L off its parent in %s", (_mode, sources) => {
|
|
298
|
+
for (const [child, parent] of steps) {
|
|
299
|
+
expect(Math.abs(Lof(sources, child) - Lof(sources, parent)), `${child} on ${parent}`).toBeGreaterThanOrEqual(4.4);
|
|
267
300
|
}
|
|
268
301
|
});
|
|
269
302
|
|
|
270
|
-
it("
|
|
271
|
-
const well
|
|
272
|
-
|
|
273
|
-
|
|
303
|
+
it("sinks every dark well below the plane it sits on, and lifts dark planes with a top highlight", () => {
|
|
304
|
+
for (const [well, parent] of steps.filter(([child]) => child.startsWith("well"))) {
|
|
305
|
+
expect(Lof(DARK, well), `${well} under ${parent}`).toBeLessThan(Lof(DARK, parent));
|
|
306
|
+
}
|
|
307
|
+
expect(declIn(shared, "edge-highlight")).toBe("inset 0 1px 0 rgba(255, 255, 255, 0.06)");
|
|
308
|
+
expect(declIn(lightRule, "edge-highlight")).toBe("0 0 0 0 transparent");
|
|
274
309
|
});
|
|
275
310
|
|
|
276
|
-
it("
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
311
|
+
it("draws every line with one hairline: 1.5:1 on the dark card and still seen on the raised plane", () => {
|
|
312
|
+
for (const sources of [LIGHT, DARK]) {
|
|
313
|
+
for (const alias of ["border-subtle", "border-default", "md3-outline-variant"]) {
|
|
314
|
+
expect(hexIn(sources, alias), `--${alias}`).toEqual(hexIn(sources, "hsl-border"));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
expect(contrastRatio(rgb(DARK, "hsl-border"), rgb(DARK, "hsl-card"))).toBeGreaterThanOrEqual(1.45);
|
|
318
|
+
expect(contrastRatio(rgb(DARK, "hsl-border"), rgb(DARK, "hsl-raised"))).toBeGreaterThanOrEqual(1.25);
|
|
319
|
+
expect(contrastRatio(rgb(LIGHT, "hsl-border"), rgb(LIGHT, "hsl-background"))).toBeGreaterThanOrEqual(1.4);
|
|
280
320
|
});
|
|
281
321
|
|
|
282
322
|
it.each([
|
|
283
|
-
["dark",
|
|
284
|
-
["light",
|
|
285
|
-
])("draws a field border that clears 3:1 on the card and
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
323
|
+
["dark", DARK],
|
|
324
|
+
["light", LIGHT],
|
|
325
|
+
])("draws a field border that clears 3:1 on the page, card and raised plane in %s (WCAG 1.4.11)", (_mode, sources) => {
|
|
326
|
+
expect(declIn(shared, "field-border")).toBe("color-mix(in oklab, hsl(var(--hsl-border)), hsl(var(--hsl-foreground)) 45%)");
|
|
327
|
+
const border = mix(rgb(sources, "hsl-border"), rgb(sources, "hsl-foreground"), 0.45);
|
|
328
|
+
for (const plane of ["hsl-background", "hsl-card", "hsl-raised"]) {
|
|
329
|
+
expect(contrastRatio(border, rgb(sources, plane)), `on --${plane}`).toBeGreaterThanOrEqual(3);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("keeps every named theme with its own planes on the flat ladder until it is tuned", () => {
|
|
334
|
+
// The canonical ladder is the spine's indigo planes; a named theme that
|
|
335
|
+
// inherited it would nest an indigo card inside its own card.
|
|
336
|
+
const flat = css.match(/([^{}]*)\{\s*--hsl-raised:\s*var\(--hsl-card\);[^}]*\}/);
|
|
337
|
+
expect(flat, "the named-theme flat rule").not.toBeNull();
|
|
338
|
+
const flatBody = flat![0];
|
|
339
|
+
for (const [token, value] of [
|
|
340
|
+
["hsl-raised", "var(--hsl-card)"],
|
|
341
|
+
["hsl-raised-2", "var(--hsl-card)"],
|
|
342
|
+
["well-raised", "var(--well-card)"],
|
|
343
|
+
["well-raised-2", "var(--well-card)"],
|
|
344
|
+
["edge-highlight", "0 0 0 0 transparent"],
|
|
345
|
+
]) {
|
|
346
|
+
expect(declIn(flatBody, token!), `--${token}`).toBe(value);
|
|
347
|
+
}
|
|
348
|
+
const names = (selectors: string) => [...selectors.matchAll(/data-theme="([^"]+)"/g)].map((m) => m[1]);
|
|
349
|
+
const ownPlanes = new Set(
|
|
350
|
+
[...rules(themes).matchAll(/([^{}]*)\{([^{}]*)\}/g)]
|
|
351
|
+
.filter((m) => m[2]!.includes("--hsl-background:"))
|
|
352
|
+
.flatMap((m) => names(m[1]!)),
|
|
353
|
+
);
|
|
354
|
+
const tuned = new Set(["hospitality"]);
|
|
355
|
+
expect(new Set(names(flat![1]!))).toEqual(new Set([...ownPlanes].filter((name) => !tuned.has(name!))));
|
|
289
356
|
});
|
|
290
357
|
});
|
|
291
358
|
|
package/src/styles/system.css
CHANGED
|
@@ -140,12 +140,15 @@
|
|
|
140
140
|
* action. White text passes on every stop (5.37, 5.39 and 5.18:1). */
|
|
141
141
|
--cta: linear-gradient(90deg, #5b5bd6 0%, #6e56cf 55%, #8e4ec6 100%);
|
|
142
142
|
--cta-hover: linear-gradient(90deg, #5151cd 0%, #654dc4 55%, #8347b9 100%);
|
|
143
|
-
/* Radius by role.
|
|
144
|
-
*
|
|
143
|
+
/* Radius by role, the tokens.css ramp: tag 4, control 8, card 12,
|
|
144
|
+
* container 16, pill. Chip, field and panel are the older names. */
|
|
145
145
|
--radius-tag: 4px;
|
|
146
|
-
--radius-
|
|
147
|
-
--radius-
|
|
148
|
-
--radius-
|
|
146
|
+
--radius-control: 8px;
|
|
147
|
+
--radius-card: 12px;
|
|
148
|
+
--radius-container: 16px;
|
|
149
|
+
--radius-chip: var(--radius-control);
|
|
150
|
+
--radius-field: var(--radius-card);
|
|
151
|
+
--radius-panel: var(--radius-container);
|
|
149
152
|
--radius-cover: 20px;
|
|
150
153
|
--radius-sheet: 24px;
|
|
151
154
|
--radius-pill: 999px;
|
|
@@ -191,10 +194,11 @@
|
|
|
191
194
|
--text-figure--line-height: 2.75rem;
|
|
192
195
|
--text-figure--letter-spacing: -0.02em;
|
|
193
196
|
--text-figure--font-weight: 600;
|
|
197
|
+
--shadow-color: 0 0% 0%;
|
|
194
198
|
/* Elevation. In dark, a white hairline states the edge; only floating layers cast a shadow. */
|
|
195
199
|
--shadow-1: inset 0 0 0 1px rgb(255 255 255 / 0.06);
|
|
196
|
-
--shadow-2: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px
|
|
197
|
-
--shadow-3: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px
|
|
200
|
+
--shadow-2: inset 0 0 0 1px rgb(255 255 255 / 0.08), 0 8px 24px hsl(var(--shadow-color) / 0.5);
|
|
201
|
+
--shadow-3: inset 0 0 0 1px rgb(255 255 255 / 0.1), 0 16px 48px hsl(var(--shadow-color) / 0.6);
|
|
198
202
|
--shadow-card: var(--shadow-1);
|
|
199
203
|
--shadow-dropdown: var(--shadow-2);
|
|
200
204
|
--shadow-overlay: var(--shadow-3);
|
|
@@ -332,12 +336,15 @@
|
|
|
332
336
|
* action. White text passes on every stop (5.37, 5.39 and 5.18:1). */
|
|
333
337
|
--cta: linear-gradient(90deg, #5b5bd6 0%, #6e56cf 55%, #8e4ec6 100%);
|
|
334
338
|
--cta-hover: linear-gradient(90deg, #5151cd 0%, #654dc4 55%, #8347b9 100%);
|
|
335
|
-
/* Radius by role.
|
|
336
|
-
*
|
|
339
|
+
/* Radius by role, the tokens.css ramp: tag 4, control 8, card 12,
|
|
340
|
+
* container 16, pill. Chip, field and panel are the older names. */
|
|
337
341
|
--radius-tag: 4px;
|
|
338
|
-
--radius-
|
|
339
|
-
--radius-
|
|
340
|
-
--radius-
|
|
342
|
+
--radius-control: 8px;
|
|
343
|
+
--radius-card: 12px;
|
|
344
|
+
--radius-container: 16px;
|
|
345
|
+
--radius-chip: var(--radius-control);
|
|
346
|
+
--radius-field: var(--radius-card);
|
|
347
|
+
--radius-panel: var(--radius-container);
|
|
341
348
|
--radius-cover: 20px;
|
|
342
349
|
--radius-sheet: 24px;
|
|
343
350
|
--radius-pill: 999px;
|
|
@@ -383,10 +390,11 @@
|
|
|
383
390
|
--text-figure--line-height: 2.75rem;
|
|
384
391
|
--text-figure--letter-spacing: -0.02em;
|
|
385
392
|
--text-figure--font-weight: 600;
|
|
393
|
+
--shadow-color: 220 40% 8%;
|
|
386
394
|
/* Elevation. In light, shadow separates a card from the white page. */
|
|
387
|
-
--shadow-1: 0 0 0 1px
|
|
388
|
-
--shadow-2: 0 0 0 1px
|
|
389
|
-
--shadow-3: 0 8px 28px
|
|
395
|
+
--shadow-1: 0 0 0 1px hsl(var(--shadow-color) / 0.03), 0 2px 6px hsl(var(--shadow-color) / 0.05), 0 4px 8px hsl(var(--shadow-color) / 0.08);
|
|
396
|
+
--shadow-2: 0 0 0 1px hsl(var(--shadow-color) / 0.04), 0 6px 16px hsl(var(--shadow-color) / 0.12);
|
|
397
|
+
--shadow-3: 0 8px 28px hsl(var(--shadow-color) / 0.28);
|
|
390
398
|
--shadow-card: var(--shadow-1);
|
|
391
399
|
--shadow-dropdown: var(--shadow-2);
|
|
392
400
|
--shadow-overlay: var(--shadow-3);
|
package/src/styles/theme.css
CHANGED
|
@@ -36,12 +36,10 @@
|
|
|
36
36
|
--radius-xl: 12px;
|
|
37
37
|
--radius-full: 999px;
|
|
38
38
|
|
|
39
|
-
--shadow-card: 0 1px 2px
|
|
40
|
-
--shadow-dropdown: 0 8px 30px
|
|
39
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.3);
|
|
40
|
+
--shadow-dropdown: 0 8px 30px hsl(var(--shadow-color) / 0.45);
|
|
41
41
|
--shadow-glow: none;
|
|
42
|
-
--shadow-overlay:
|
|
43
|
-
0 2px 6px hsl(var(--hsl-foreground) / 0.1),
|
|
44
|
-
0 16px 40px hsl(var(--hsl-foreground) / 0.16);
|
|
42
|
+
--shadow-overlay: 0 2px 6px hsl(var(--shadow-color) / 0.3), 0 16px 40px hsl(var(--shadow-color) / 0.5);
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/* Inline variable-dependent utilities so their source is resolved on the
|
|
@@ -66,7 +64,7 @@
|
|
|
66
64
|
--text-hero: var(--font-size-hero);
|
|
67
65
|
--text-page: var(--font-size-3xl);
|
|
68
66
|
--text-section: var(--font-size-xl);
|
|
69
|
-
--text-eyebrow: var(--font-size-
|
|
67
|
+
--text-eyebrow: var(--font-size-xs);
|
|
70
68
|
|
|
71
69
|
/* Control and form type: text-control-sm/md/lg, text-label, text-help. */
|
|
72
70
|
--text-control-sm: var(--control-text-sm);
|
package/src/styles/tokens.css
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/* Tangle design tokens — the single palette every Tangle surface renders from.
|
|
2
2
|
*
|
|
3
|
-
* The surface ladder is
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The surface ladder is near-neutral grey with a faint indigo cast; the indigo
|
|
4
|
+
* accent is trim, never field. Each plane steps at least 4.4 OKLCH L off the
|
|
5
|
+
* plane it sits on (surface-ladder.test.ts), so a card, a card in a card and a
|
|
6
|
+
* field each read by their own fill. Dark adds a 1.5:1 hairline and a top edge
|
|
7
|
+
* highlight, because a black shadow on a dark canvas renders as nothing; light
|
|
8
|
+
* is white paper on a grey canvas, separated by shadow.
|
|
8
9
|
*
|
|
9
10
|
* Each colour is declared ONCE, as a role (a plane, an ink tier, one of four
|
|
10
11
|
* indigos, one ink per status hue), and every other name of that colour is an
|
|
@@ -15,26 +16,9 @@
|
|
|
15
16
|
* add a named theme in named-themes.css (`data-theme="<name>"`) — do NOT re-declare
|
|
16
17
|
* these tokens in the app, or its surfaces silently stop tracking the brand. */
|
|
17
18
|
|
|
18
|
-
/* MD3 surface hierarchy */
|
|
19
19
|
/* Dark is the default theme (`:root`/`[data-sandbox-ui]`). It also responds to
|
|
20
20
|
* the standard `[data-theme="dark"]` / `.dark` switch so any consumer can pin or
|
|
21
21
|
* toggle a theme without inventing an ad-hoc `data-sandbox-theme` name. */
|
|
22
|
-
/*
|
|
23
|
-
* Surfaces are NEUTRAL GREY and the indigo is trim.
|
|
24
|
-
*
|
|
25
|
-
* Colour in the surface competes with colour in the accent: on an indigo-cast
|
|
26
|
-
* ladder the indigo nav pill, link and focus ring are one more indigo thing on
|
|
27
|
-
* an indigo page, and the eye stops finding them. Neutral planes give the brand
|
|
28
|
-
* a field to sit on, so a single accent does all the signalling.
|
|
29
|
-
*
|
|
30
|
-
* The canvas stays above pure black (#0a0a14, lifted toward indigo). Near-white ink on it is the
|
|
31
|
-
* glare end of the range, so its luminance leaves room for distinct text tiers.
|
|
32
|
-
*
|
|
33
|
-
* Elevation is carried by surface lightness plus a hairline that can actually be
|
|
34
|
-
* seen — a black shadow on a dark canvas renders as nothing, so shadow does no
|
|
35
|
-
* work here. The hairline is about 1.5:1 against the resting card — quiet by design,
|
|
36
|
-
* enough to state an edge without drawing a box around every panel.
|
|
37
|
-
*/
|
|
38
22
|
/* Seed a complete baseline on every supported boundary BEFORE mode overrides.
|
|
39
23
|
* Inherited var() aliases are already substituted at their parent; declaring
|
|
40
24
|
* them here lets a nested light/dark/named scope resolve its own palette.
|
|
@@ -65,13 +49,14 @@
|
|
|
65
49
|
/* ── Planes ─────────────────────────────────────────────────────────────
|
|
66
50
|
* One ladder, authored once as HSL channels because the shadcn layer reads
|
|
67
51
|
* hsl(var(--hsl-*)); every hex plane below is derived from it, so a step
|
|
68
|
-
* moves in one place.
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
--hsl-
|
|
72
|
-
--hsl-
|
|
73
|
-
--hsl-
|
|
74
|
-
--hsl-
|
|
52
|
+
* moves in one place. OKLCH L x100: page 15.0, card 21.7, raised 26.6,
|
|
53
|
+
* raised-2 and overlay 31.6, each at least 4.4 over the plane it sits on.
|
|
54
|
+
* The page is lifted off black so near-white ink keeps room for its tiers. */
|
|
55
|
+
--hsl-background: 240 33% 6%; /* L0 page #0a0a14 */
|
|
56
|
+
--hsl-input: 244 29% 9%; /* L1 chrome (the sidebar) and the legacy field fill #11101e */
|
|
57
|
+
--hsl-card: 244 23% 12%; /* L2 card #191826 */
|
|
58
|
+
--hsl-secondary: 249 24% 17.5%; /* L3 raised: a card in a card, and the hover well #252237 */
|
|
59
|
+
--hsl-popover: 247 21% 23%; /* L4 raised-2 and overlay #312e47: a popover carries body text, so it sits on the top plane the ink ramp reaches */
|
|
75
60
|
--hsl-muted: var(--hsl-secondary);
|
|
76
61
|
--hsl-accent: var(--hsl-secondary); /* neutral hover; indigo lives in --accent-surface-strong */
|
|
77
62
|
/* The floor of the ladder: the recessed page and the terminal well share it
|
|
@@ -125,15 +110,16 @@
|
|
|
125
110
|
--md3-on-surface-variant: var(--text-muted);
|
|
126
111
|
--md3-outline: #8e8e8e;
|
|
127
112
|
|
|
128
|
-
/* ──
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
|
|
113
|
+
/* ── Hairline ───────────────────────────────────────────────────────────
|
|
114
|
+
* ONE role for every divider, card edge and table rule, so lines never
|
|
115
|
+
* disagree. 1.5:1 against the card and 1.3:1 on the raised plane: quiet by
|
|
116
|
+
* design, it states where a plane ends without boxing every panel.
|
|
117
|
+
* --border-subtle and --border-default are its older names. */
|
|
118
|
+
--hsl-border: 244 16% 25%; /* #37364a */
|
|
133
119
|
--md3-outline-variant: hsl(var(--hsl-border));
|
|
134
120
|
--sidebar-border: var(--hsl-border);
|
|
135
|
-
--border-subtle:
|
|
136
|
-
--border-default:
|
|
121
|
+
--border-subtle: hsl(var(--hsl-border));
|
|
122
|
+
--border-default: hsl(var(--hsl-border));
|
|
137
123
|
--border-hover: rgba(255, 255, 255, 0.28);
|
|
138
124
|
|
|
139
125
|
/* ── Accent: four indigos, each with one job ────────────────────────────
|
|
@@ -357,7 +343,7 @@
|
|
|
357
343
|
--glass-bg: var(--depth-2);
|
|
358
344
|
--glass-bg-strong: var(--depth-3);
|
|
359
345
|
--glass-border: var(--border-subtle);
|
|
360
|
-
--glass-border-hover: var(--border-
|
|
346
|
+
--glass-border-hover: var(--border-hover);
|
|
361
347
|
--glass-blur: 0px;
|
|
362
348
|
|
|
363
349
|
/* Mesh glows — off; depth comes from the ladder, not from glow */
|
|
@@ -388,23 +374,18 @@
|
|
|
388
374
|
--code-comment: var(--syntax-comment);
|
|
389
375
|
--code-error: var(--syntax-error);
|
|
390
376
|
|
|
391
|
-
/* Shadows — flat, no inner ring, no
|
|
392
|
-
|
|
393
|
-
|
|
377
|
+
/* Shadows — flat, no inner ring, no glow. Every shadow is one colour,
|
|
378
|
+
--shadow-color, as HSL channels so each sets its own alpha: black here, a
|
|
379
|
+
deep cool ink in light. A shadow never inverts into a light halo; dark
|
|
380
|
+
elevation is the plane step plus --edge-highlight. --shadow-overlay is the
|
|
381
|
+
lift above --shadow-dropdown, for a surface that covers the page (a
|
|
382
|
+
drawer, a modal, a floating dock): a tight contact layer plus a wide one. */
|
|
383
|
+
--shadow-color: 0 0% 0%;
|
|
384
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.3);
|
|
385
|
+
--shadow-dropdown: 0 8px 30px hsl(var(--shadow-color) / 0.45);
|
|
394
386
|
--shadow-glow: none;
|
|
395
|
-
--shadow-accent:
|
|
396
|
-
|
|
397
|
-
rather than sitting beside it: a drawer, a modal, a floating dock.
|
|
398
|
-
Two layers — a tight contact shadow plus a wide ambient one.
|
|
399
|
-
Alone among the shadows this is derived from --hsl-foreground instead of
|
|
400
|
-
a fixed rgba, so it INVERTS with the theme. That is what makes it work in
|
|
401
|
-
dark: a black shadow on a dark canvas renders as nothing (see the ladder
|
|
402
|
-
note at the top of this file), while the foreground is near-white there,
|
|
403
|
-
so the same declaration reads as a soft halo. One definition therefore
|
|
404
|
-
serves both themes and the light block does not restate it. */
|
|
405
|
-
--shadow-overlay:
|
|
406
|
-
0 2px 6px hsl(var(--hsl-foreground) / 0.1),
|
|
407
|
-
0 16px 40px hsl(var(--hsl-foreground) / 0.16);
|
|
387
|
+
--shadow-accent: var(--shadow-card);
|
|
388
|
+
--shadow-overlay: 0 2px 6px hsl(var(--shadow-color) / 0.3), 0 16px 40px hsl(var(--shadow-color) / 0.5);
|
|
408
389
|
/* Scrim and media ink. A scrim dims the page behind a modal or sheet, or
|
|
409
390
|
darkens a photo under its caption. It is black in every theme: a dimming
|
|
410
391
|
layer that inverted in dark mode would brighten the page it covers.
|
|
@@ -424,12 +405,18 @@
|
|
|
424
405
|
--radius-pill: 999px;
|
|
425
406
|
--radius-full: 999px;
|
|
426
407
|
--radius-composer: 26px; /* centered composer pill */
|
|
427
|
-
/* Radius by role
|
|
428
|
-
|
|
408
|
+
/* Radius by role, one ramp: tag 4, control 8, card 12, container 16, and
|
|
409
|
+
--radius-pill for every chip and status pill. Chip, field and panel are
|
|
410
|
+
the older names of control, card and container. Cover and sheet are the
|
|
411
|
+
media and bottom-sheet radii. system.css repeats these for pages that load
|
|
412
|
+
it on its own ladders. */
|
|
429
413
|
--radius-tag: 4px;
|
|
430
|
-
--radius-
|
|
431
|
-
--radius-
|
|
432
|
-
--radius-
|
|
414
|
+
--radius-control: var(--radius-md);
|
|
415
|
+
--radius-card: var(--radius-xl);
|
|
416
|
+
--radius-container: 16px;
|
|
417
|
+
--radius-chip: var(--radius-control);
|
|
418
|
+
--radius-field: var(--radius-card);
|
|
419
|
+
--radius-panel: var(--radius-container);
|
|
433
420
|
--radius-cover: 20px;
|
|
434
421
|
--radius-sheet: 24px;
|
|
435
422
|
--radius: 0.5rem; /* 8px chrome default (was 10px) */
|
|
@@ -461,12 +448,13 @@
|
|
|
461
448
|
--transcript-font-size: 1rem; /* 16px: the chat reading size */
|
|
462
449
|
--transcript-line-height: var(--line-height-base);
|
|
463
450
|
|
|
464
|
-
/* Typography scale
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
--font-size-
|
|
468
|
-
--font-size-
|
|
469
|
-
--font-size-
|
|
451
|
+
/* Typography scale. Each --font-size-<step> is Tailwind's text-<step>, so a
|
|
452
|
+
step name means one size everywhere ("sm" is 14px); 12px is the floor.
|
|
453
|
+
Components reference these instead of pixel values; an app may override. */
|
|
454
|
+
--font-size-xs: 0.75rem; /* 12px — meta, timestamps, eyebrows */
|
|
455
|
+
--font-size-sm: 0.875rem; /* 14px — UI text */
|
|
456
|
+
--font-size-base: 1rem; /* 16px — reading text */
|
|
457
|
+
--font-size-lg: 1.125rem; /* 18px — emphasized text */
|
|
470
458
|
--font-size-xl: 1.25rem; /* 20px — section title (h2) */
|
|
471
459
|
/* Display tier — page titles, marketing section heads, and heroes.
|
|
472
460
|
Hero sizes are fluid (clamp) so they scale with the viewport without
|
|
@@ -540,6 +528,11 @@
|
|
|
540
528
|
--motion-dismiss: var(--duration-instant) var(--ease-exit);
|
|
541
529
|
--motion-stream: var(--duration-stream) var(--ease-expo);
|
|
542
530
|
--motion-arrive: var(--duration-arrive) var(--ease-expo);
|
|
531
|
+
/* The only loop period on a page. A running mark breathes on it with
|
|
532
|
+
`tangle-breathe` (opacity, never position or scale), so every live thing
|
|
533
|
+
shares one rhythm. Reduced motion keeps it: see the rule below. */
|
|
534
|
+
--duration-ambient: 2000ms;
|
|
535
|
+
--motion-ambient: var(--duration-ambient) ease-in-out;
|
|
543
536
|
|
|
544
537
|
/* Elapsed interaction thresholds, not animation durations. See
|
|
545
538
|
* docs/design-bible.md#interaction. Reduced motion leaves these unchanged. */
|
|
@@ -589,7 +582,7 @@
|
|
|
589
582
|
--hsl-background: 223 23% 94%; /* L0 canvas #eceef3, BELOW the wells */
|
|
590
583
|
--hsl-input: 225 29% 97%; /* L1 chrome and field wells #f5f6fa */
|
|
591
584
|
--hsl-card: 0 0% 100%; /* L2 card: paper */
|
|
592
|
-
--hsl-secondary:
|
|
585
|
+
--hsl-secondary: 228 25% 94.5%; /* L3 raised: a card in a card #edeff4, 4.8 L under its paper */
|
|
593
586
|
--hsl-popover: var(--hsl-card); /* L4 overlay: paper, lifted by shadow, never out-lightening a card */
|
|
594
587
|
--md3-surface-dim: #dcdee6; /* the recessed page */
|
|
595
588
|
--md3-surface-container-lowest: hsl(var(--hsl-card)); /* insets read as paper */
|
|
@@ -609,10 +602,8 @@
|
|
|
609
602
|
--text-dim: #686868;
|
|
610
603
|
--md3-outline: #787878;
|
|
611
604
|
|
|
612
|
-
/* ──
|
|
605
|
+
/* ── Hairline: 1.7:1 on paper, 1.4:1 on the canvas ──────────────────────── */
|
|
613
606
|
--hsl-border: 244 16% 81%; /* #c8c7d6 */
|
|
614
|
-
--border-subtle: rgba(0, 0, 0, 0.10);
|
|
615
|
-
--border-default: rgba(0, 0, 0, 0.14);
|
|
616
607
|
--border-hover: rgba(0, 0, 0, 0.24);
|
|
617
608
|
|
|
618
609
|
/* ── Accent ─────────────────────────────────────────────────────────────
|
|
@@ -752,11 +743,11 @@
|
|
|
752
743
|
--glass-bg-strong: var(--depth-1);
|
|
753
744
|
--code-header-bg: color-mix(in srgb, hsl(var(--hsl-muted)) 50%, transparent);
|
|
754
745
|
|
|
755
|
-
/* Shadows —
|
|
756
|
-
--shadow-
|
|
757
|
-
--shadow-
|
|
758
|
-
--shadow-
|
|
759
|
-
--shadow-
|
|
746
|
+
/* Shadows — the dark roles in the light shadow colour, at light alphas. */
|
|
747
|
+
--shadow-color: 220 40% 8%;
|
|
748
|
+
--shadow-card: 0 1px 2px hsl(var(--shadow-color) / 0.06);
|
|
749
|
+
--shadow-dropdown: 0 8px 30px hsl(var(--shadow-color) / 0.1);
|
|
750
|
+
--shadow-overlay: 0 2px 6px hsl(var(--shadow-color) / 0.1), 0 16px 40px hsl(var(--shadow-color) / 0.16);
|
|
760
751
|
}
|
|
761
752
|
|
|
762
753
|
|
|
@@ -807,15 +798,13 @@
|
|
|
807
798
|
* level in, --hsl-raised-2 two levels in.
|
|
808
799
|
* --well the field fill: --well-page on the page, --well-card in a card,
|
|
809
800
|
* --well-raised in a card in a card, --well-raised-2 deeper still.
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
* levels still paint the card here: --hsl-raised is an HSL triplet and cannot
|
|
818
|
-
* be derived per theme.
|
|
801
|
+
* The canonical ladder is the spine's own planes: raised is L3 (--hsl-secondary),
|
|
802
|
+
* raised-2 is L4 (--hsl-popover), and each well is a plane at least 4.4 OKLCH L
|
|
803
|
+
* off the level it fills. Dark wells always sit deeper (page + 30% black on the
|
|
804
|
+
* page and in a card, the card in a raised plane); light wells take the
|
|
805
|
+
* opposite plane (paper on a grey level, the canvas on paper). Dark raised
|
|
806
|
+
* planes also carry a 1px top highlight. Hospitality tunes its own ladder; the
|
|
807
|
+
* other named themes keep a flat one (below) until theirs is tuned.
|
|
819
808
|
*
|
|
820
809
|
* INTERACTION. Four states, all from the focus ring hue (--hsl-ring), so one
|
|
821
810
|
* green or one indigo marks every interactive thing in a theme:
|
|
@@ -833,20 +822,20 @@
|
|
|
833
822
|
[data-sandbox-ui],
|
|
834
823
|
[data-sandbox-theme],
|
|
835
824
|
[data-theme] {
|
|
836
|
-
--hsl-raised: var(--hsl-
|
|
837
|
-
--hsl-raised-2: var(--hsl-
|
|
825
|
+
--hsl-raised: var(--hsl-secondary);
|
|
826
|
+
--hsl-raised-2: var(--hsl-popover);
|
|
838
827
|
--well-page: color-mix(in oklab, hsl(var(--hsl-background)) 70%, black);
|
|
839
828
|
--well-card: var(--well-page);
|
|
840
|
-
--well-raised: var(--
|
|
841
|
-
--well-raised-2: var(--well-
|
|
829
|
+
--well-raised: hsl(var(--hsl-card));
|
|
830
|
+
--well-raised-2: var(--well-raised);
|
|
842
831
|
--well: var(--well-page);
|
|
843
832
|
/* The hairline is 1.5:1 by design; a field edge is non-text UI and needs 3:1.
|
|
844
|
-
* 45% toward the ink
|
|
833
|
+
* 45% toward the ink clears 4:1 on the spine card in both modes. */
|
|
845
834
|
--field-border: color-mix(in oklab, hsl(var(--hsl-border)), hsl(var(--hsl-foreground)) 45%);
|
|
846
835
|
--shadow-raised: var(--shadow-card);
|
|
847
836
|
/* A shadow token joins a comma list, so "none" would void the whole list. */
|
|
848
837
|
--shadow-well: 0 0 0 0 transparent;
|
|
849
|
-
--edge-highlight:
|
|
838
|
+
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
850
839
|
--ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
|
|
851
840
|
--ix-hover-border: hsl(var(--hsl-ring) / 0.7);
|
|
852
841
|
--ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
|
|
@@ -870,8 +859,20 @@
|
|
|
870
859
|
--ix-hover-bg: hsl(var(--hsl-ring) / 0.07);
|
|
871
860
|
--well-page: hsl(var(--hsl-card));
|
|
872
861
|
--well-card: hsl(var(--hsl-background));
|
|
873
|
-
--well-raised:
|
|
874
|
-
--
|
|
862
|
+
--well-raised-2: var(--well-card);
|
|
863
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/* Named themes with their own planes keep a flat ladder until each is tuned as
|
|
867
|
+
* Hospitality was: a nested card paints the card, and its field the card's well. */
|
|
868
|
+
[data-theme="aubergine"], [data-theme="aubergine-light"], [data-theme="arena"], [data-theme="arena-light"],
|
|
869
|
+
[data-theme="super"], [data-theme="super-light"], [data-theme="tangle-light"], [data-theme="agents"],
|
|
870
|
+
[data-theme="agents-light"], [data-theme="website"], [data-theme="intelligence"] {
|
|
871
|
+
--hsl-raised: var(--hsl-card);
|
|
872
|
+
--hsl-raised-2: var(--hsl-card);
|
|
873
|
+
--well-raised: var(--well-card);
|
|
874
|
+
--well-raised-2: var(--well-card);
|
|
875
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
875
876
|
}
|
|
876
877
|
|
|
877
878
|
/* Surface context: keyed only on the Card primitive's data-slot, never on app
|
|
@@ -945,7 +946,11 @@
|
|
|
945
946
|
|
|
946
947
|
|
|
947
948
|
/* Motion is opt-in at the component, but its reduced-motion preference is
|
|
948
|
-
* shared. Re-declare composites at the same boundary as their durations.
|
|
949
|
+
* shared. Re-declare composites at the same boundary as their durations.
|
|
950
|
+
* --duration-ambient is not zeroed: the essential running mark
|
|
951
|
+
* (data-motion="essential") keeps its opacity-only breathe, because it is the
|
|
952
|
+
* only "still working" signal and it never moves anything. */
|
|
953
|
+
@keyframes tangle-breathe { 50% { opacity: 0.45; } }
|
|
949
954
|
@media (prefers-reduced-motion: reduce) {
|
|
950
955
|
:root,
|
|
951
956
|
.dark,
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
blockIn,
|
|
6
6
|
compositeOver,
|
|
7
7
|
contrastRatio,
|
|
8
|
+
declIn,
|
|
8
9
|
hexIn,
|
|
9
10
|
hexRelativeLuminanceIn,
|
|
10
11
|
hslIn,
|
|
11
12
|
hslToRgb,
|
|
13
|
+
resolveIn,
|
|
12
14
|
} from "./css-test-utils";
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -608,3 +610,39 @@ describe("each colour is declared once: a role, then aliases", () => {
|
|
|
608
610
|
expect(literalsOutsideTones(spine).size).toBeLessThanOrEqual(ceiling);
|
|
609
611
|
});
|
|
610
612
|
});
|
|
613
|
+
|
|
614
|
+
describe("shadow, radius and loop period are one role each", () => {
|
|
615
|
+
// The run-UI critique of 2026-10-27 (fixes 9, 10 and 22) found a white shadow
|
|
616
|
+
// haze in dark mode, six radii and seven loop periods. Each is now one role
|
|
617
|
+
// that every other name aliases, so a surface cannot drift to a second value.
|
|
618
|
+
it.each([
|
|
619
|
+
["dark", DARK, "0 0% 0%"],
|
|
620
|
+
["light", LIGHT, "220 40% 8%"],
|
|
621
|
+
] as const)("%s: every shadow is drawn in --shadow-color and never inverts", (_, spine, colour) => {
|
|
622
|
+
expect(declIn(spine, "shadow-color")).toBe(colour);
|
|
623
|
+
for (const token of ["shadow-card", "shadow-dropdown", "shadow-overlay"]) {
|
|
624
|
+
const value = declIn(spine, token);
|
|
625
|
+
expect(value, `--${token}`).toMatch(/^[^,]*hsl\(var\(--shadow-color\) \/ [\d.]+\)/);
|
|
626
|
+
expect(value, `--${token} names a colour of its own`).not.toMatch(/rgba?\(|#[0-9a-f]{3,8}\b|--hsl-foreground/i);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("draws radii from one ramp, and the older role names alias it", () => {
|
|
631
|
+
const ramp = { tag: "4px", control: "8px", card: "12px", container: "16px", pill: "999px" };
|
|
632
|
+
for (const [role, px] of Object.entries(ramp)) expect(resolveIn(DARK, `radius-${role}`), role).toBe(px);
|
|
633
|
+
for (const [older, role] of [["chip", "control"], ["field", "card"], ["panel", "container"]]) {
|
|
634
|
+
expect(declIn(DARK, `radius-${older}`)).toBe(`var(--radius-${role})`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("has one loop period, which reduced motion keeps for an opacity-only breathe", () => {
|
|
639
|
+
expect(declIn(DARK, "duration-ambient")).toBe("2000ms");
|
|
640
|
+
expect(declIn(DARK, "motion-ambient")).toBe("var(--duration-ambient) ease-in-out");
|
|
641
|
+
const reduced = tokens.match(/@media \(prefers-reduced-motion: reduce\) \{([\s\S]*?)\n\}/)![1];
|
|
642
|
+
expect(reduced).toContain("--duration-fast: 0ms");
|
|
643
|
+
expect(reduced, "the essential breathe keeps its period").not.toContain("--duration-ambient");
|
|
644
|
+
const breathe = tokens.match(/@keyframes tangle-breathe \{([\s\S]*?)\}\s*\}/)?.[1];
|
|
645
|
+
expect(breathe, "the shared breathe keyframe").toMatch(/opacity/);
|
|
646
|
+
expect(breathe, "a loop never moves or scales").not.toMatch(/transform|translate|scale|rotate/);
|
|
647
|
+
});
|
|
648
|
+
});
|