@tangle-network/brand 1.28.0 → 1.29.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 +33 -0
- 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 +27 -22
- package/src/styles/named-themes.css +1 -1
- package/src/styles/surface-ladder.test.ts +104 -37
- package/src/styles/theme.css +1 -1
- package/src/styles/tokens.css +59 -64
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.29.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4678ab2: The canonical spine (`:root`, `.dark`, `.light`, `data-theme` `dark`, `light`
|
|
8
|
+
and `tangle-dark`) has a real surface ladder. `--hsl-raised` is now the spine's
|
|
9
|
+
L3 plane and `--hsl-raised-2` its L4 plane, instead of both repeating the card,
|
|
10
|
+
and every adjacent plane and well is at least 4.4 OKLCH L apart (gated in
|
|
11
|
+
`surface-ladder.test.ts`):
|
|
12
|
+
|
|
13
|
+
- Dark: page `#0a0a14`, card `#191826`, raised `#252237`, raised-2 and popover
|
|
14
|
+
`#312e47`. A field in a raised card takes the card colour instead of the page's
|
|
15
|
+
near-black well, and dark cards and menus carry the 1px top highlight
|
|
16
|
+
(`--edge-highlight`).
|
|
17
|
+
- Light: page `#eceef3`, card white, raised `#edeff4`, raised-2 white. A field in
|
|
18
|
+
a raised card is white.
|
|
19
|
+
|
|
20
|
+
`--hsl-secondary` and `--hsl-popover` moved with the ladder (dark `#221f33` to
|
|
21
|
+
`#252237`, `#2c2942` to `#312e47`; light `#f2f3f8` to `#edeff4`). Named themes
|
|
22
|
+
other than Hospitality keep their flat ladder.
|
|
23
|
+
|
|
24
|
+
One hairline: `--border-subtle` and `--border-default` now alias `--hsl-border`
|
|
25
|
+
in the spine, so every divider, card edge and table rule renders one colour. The
|
|
26
|
+
dark hairline is `#37364a` (1.5:1 on the card, as documented; it measured 1.2:1).
|
|
27
|
+
`--glass-border-hover` now follows `--border-hover`.
|
|
28
|
+
|
|
29
|
+
The `--font-size-xs`, `-sm`, `-base` and `-lg` tokens now equal Tailwind's
|
|
30
|
+
`text-xs`, `text-sm`, `text-base` and `text-lg` (12, 14, 16 and 18px; they were
|
|
31
|
+
11, 12, 15 and 16px), so `sm` means 14px in CSS and in a class. `text-eyebrow`,
|
|
32
|
+
the `Heading` eyebrow and subsection variants and `.tangle-api-meta` keep their
|
|
33
|
+
rendered sizes on the new steps. Code that relied on the old values reads
|
|
34
|
+
`--font-size-xs` for 12px captions and `--font-size-base` for 16px emphasis.
|
|
35
|
+
|
|
3
36
|
## 1.28.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
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: 0a80a683644d15e5e56e938a3094142b9f1a2dd6f3fe136154af4b6bc7c033e0
|
|
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;
|
|
@@ -311,10 +311,10 @@
|
|
|
311
311
|
--shell-inset-gutter: 0.5rem;
|
|
312
312
|
--transcript-font-size: 1rem;
|
|
313
313
|
--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:
|
|
314
|
+
--font-size-xs: 0.75rem;
|
|
315
|
+
--font-size-sm: 0.875rem;
|
|
316
|
+
--font-size-base: 1rem;
|
|
317
|
+
--font-size-lg: 1.125rem;
|
|
318
318
|
--font-size-xl: 1.25rem;
|
|
319
319
|
--font-size-2xl: 1.5rem;
|
|
320
320
|
--font-size-3xl: 1.875rem;
|
|
@@ -408,7 +408,7 @@
|
|
|
408
408
|
--hsl-background: 223 23% 94%;
|
|
409
409
|
--hsl-input: 225 29% 97%;
|
|
410
410
|
--hsl-card: 0 0% 100%;
|
|
411
|
-
--hsl-secondary:
|
|
411
|
+
--hsl-secondary: 228 25% 94.5%;
|
|
412
412
|
--hsl-popover: var(--hsl-card);
|
|
413
413
|
--md3-surface-dim: #dcdee6;
|
|
414
414
|
--md3-surface-container-lowest: hsl(var(--hsl-card));
|
|
@@ -423,8 +423,6 @@
|
|
|
423
423
|
--text-dim: #686868;
|
|
424
424
|
--md3-outline: #787878;
|
|
425
425
|
--hsl-border: 244 16% 81%;
|
|
426
|
-
--border-subtle: rgba(0, 0, 0, 0.10);
|
|
427
|
-
--border-default: rgba(0, 0, 0, 0.14);
|
|
428
426
|
--border-hover: rgba(0, 0, 0, 0.24);
|
|
429
427
|
--hsl-ring: var(--hsl-primary);
|
|
430
428
|
--brand-primary: #6366F1;
|
|
@@ -558,17 +556,17 @@
|
|
|
558
556
|
[data-sandbox-ui],
|
|
559
557
|
[data-sandbox-theme],
|
|
560
558
|
[data-theme] {
|
|
561
|
-
--hsl-raised: var(--hsl-
|
|
562
|
-
--hsl-raised-2: var(--hsl-
|
|
559
|
+
--hsl-raised: var(--hsl-secondary);
|
|
560
|
+
--hsl-raised-2: var(--hsl-popover);
|
|
563
561
|
--well-page: color-mix(in oklab, hsl(var(--hsl-background)) 70%, black);
|
|
564
562
|
--well-card: var(--well-page);
|
|
565
|
-
--well-raised: var(--
|
|
566
|
-
--well-raised-2: var(--well-
|
|
563
|
+
--well-raised: hsl(var(--hsl-card));
|
|
564
|
+
--well-raised-2: var(--well-raised);
|
|
567
565
|
--well: var(--well-page);
|
|
568
566
|
--field-border: color-mix(in oklab, hsl(var(--hsl-border)), hsl(var(--hsl-foreground)) 45%);
|
|
569
567
|
--shadow-raised: var(--shadow-card);
|
|
570
568
|
--shadow-well: 0 0 0 0 transparent;
|
|
571
|
-
--edge-highlight:
|
|
569
|
+
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
572
570
|
--ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
|
|
573
571
|
--ix-hover-border: hsl(var(--hsl-ring) / 0.7);
|
|
574
572
|
--ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
|
|
@@ -589,8 +587,15 @@
|
|
|
589
587
|
--ix-hover-bg: hsl(var(--hsl-ring) / 0.07);
|
|
590
588
|
--well-page: hsl(var(--hsl-card));
|
|
591
589
|
--well-card: hsl(var(--hsl-background));
|
|
592
|
-
--well-raised:
|
|
593
|
-
--
|
|
590
|
+
--well-raised-2: var(--well-card);
|
|
591
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
592
|
+
}
|
|
593
|
+
[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"] {
|
|
594
|
+
--hsl-raised: var(--hsl-card);
|
|
595
|
+
--hsl-raised-2: var(--hsl-card);
|
|
596
|
+
--well-raised: var(--well-card);
|
|
597
|
+
--well-raised-2: var(--well-card);
|
|
598
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
594
599
|
}
|
|
595
600
|
[data-slot="card"] :where(*) {
|
|
596
601
|
--card: var(--hsl-raised);
|
|
@@ -746,7 +751,7 @@
|
|
|
746
751
|
--text-hero: var(--font-size-hero);
|
|
747
752
|
--text-page: var(--font-size-3xl);
|
|
748
753
|
--text-section: var(--font-size-xl);
|
|
749
|
-
--text-eyebrow: var(--font-size-
|
|
754
|
+
--text-eyebrow: var(--font-size-xs);
|
|
750
755
|
--text-control-sm: var(--control-text-sm);
|
|
751
756
|
--text-control-md: var(--control-text-md);
|
|
752
757
|
--text-control-lg: var(--control-text-lg);
|
|
@@ -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);
|
|
@@ -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/theme.css
CHANGED
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
--text-hero: var(--font-size-hero);
|
|
67
67
|
--text-page: var(--font-size-3xl);
|
|
68
68
|
--text-section: var(--font-size-xl);
|
|
69
|
-
--text-eyebrow: var(--font-size-
|
|
69
|
+
--text-eyebrow: var(--font-size-xs);
|
|
70
70
|
|
|
71
71
|
/* Control and form type: text-control-sm/md/lg, text-label, text-help. */
|
|
72
72
|
--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 */
|
|
@@ -461,12 +447,13 @@
|
|
|
461
447
|
--transcript-font-size: 1rem; /* 16px: the chat reading size */
|
|
462
448
|
--transcript-line-height: var(--line-height-base);
|
|
463
449
|
|
|
464
|
-
/* Typography scale
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
--font-size-
|
|
468
|
-
--font-size-
|
|
469
|
-
--font-size-
|
|
450
|
+
/* Typography scale. Each --font-size-<step> is Tailwind's text-<step>, so a
|
|
451
|
+
step name means one size everywhere ("sm" is 14px); 12px is the floor.
|
|
452
|
+
Components reference these instead of pixel values; an app may override. */
|
|
453
|
+
--font-size-xs: 0.75rem; /* 12px — meta, timestamps, eyebrows */
|
|
454
|
+
--font-size-sm: 0.875rem; /* 14px — UI text */
|
|
455
|
+
--font-size-base: 1rem; /* 16px — reading text */
|
|
456
|
+
--font-size-lg: 1.125rem; /* 18px — emphasized text */
|
|
470
457
|
--font-size-xl: 1.25rem; /* 20px — section title (h2) */
|
|
471
458
|
/* Display tier — page titles, marketing section heads, and heroes.
|
|
472
459
|
Hero sizes are fluid (clamp) so they scale with the viewport without
|
|
@@ -589,7 +576,7 @@
|
|
|
589
576
|
--hsl-background: 223 23% 94%; /* L0 canvas #eceef3, BELOW the wells */
|
|
590
577
|
--hsl-input: 225 29% 97%; /* L1 chrome and field wells #f5f6fa */
|
|
591
578
|
--hsl-card: 0 0% 100%; /* L2 card: paper */
|
|
592
|
-
--hsl-secondary:
|
|
579
|
+
--hsl-secondary: 228 25% 94.5%; /* L3 raised: a card in a card #edeff4, 4.8 L under its paper */
|
|
593
580
|
--hsl-popover: var(--hsl-card); /* L4 overlay: paper, lifted by shadow, never out-lightening a card */
|
|
594
581
|
--md3-surface-dim: #dcdee6; /* the recessed page */
|
|
595
582
|
--md3-surface-container-lowest: hsl(var(--hsl-card)); /* insets read as paper */
|
|
@@ -609,10 +596,8 @@
|
|
|
609
596
|
--text-dim: #686868;
|
|
610
597
|
--md3-outline: #787878;
|
|
611
598
|
|
|
612
|
-
/* ──
|
|
599
|
+
/* ── Hairline: 1.7:1 on paper, 1.4:1 on the canvas ──────────────────────── */
|
|
613
600
|
--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
601
|
--border-hover: rgba(0, 0, 0, 0.24);
|
|
617
602
|
|
|
618
603
|
/* ── Accent ─────────────────────────────────────────────────────────────
|
|
@@ -807,15 +792,13 @@
|
|
|
807
792
|
* level in, --hsl-raised-2 two levels in.
|
|
808
793
|
* --well the field fill: --well-page on the page, --well-card in a card,
|
|
809
794
|
* --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.
|
|
795
|
+
* The canonical ladder is the spine's own planes: raised is L3 (--hsl-secondary),
|
|
796
|
+
* raised-2 is L4 (--hsl-popover), and each well is a plane at least 4.4 OKLCH L
|
|
797
|
+
* off the level it fills. Dark wells always sit deeper (page + 30% black on the
|
|
798
|
+
* page and in a card, the card in a raised plane); light wells take the
|
|
799
|
+
* opposite plane (paper on a grey level, the canvas on paper). Dark raised
|
|
800
|
+
* planes also carry a 1px top highlight. Hospitality tunes its own ladder; the
|
|
801
|
+
* other named themes keep a flat one (below) until theirs is tuned.
|
|
819
802
|
*
|
|
820
803
|
* INTERACTION. Four states, all from the focus ring hue (--hsl-ring), so one
|
|
821
804
|
* green or one indigo marks every interactive thing in a theme:
|
|
@@ -833,20 +816,20 @@
|
|
|
833
816
|
[data-sandbox-ui],
|
|
834
817
|
[data-sandbox-theme],
|
|
835
818
|
[data-theme] {
|
|
836
|
-
--hsl-raised: var(--hsl-
|
|
837
|
-
--hsl-raised-2: var(--hsl-
|
|
819
|
+
--hsl-raised: var(--hsl-secondary);
|
|
820
|
+
--hsl-raised-2: var(--hsl-popover);
|
|
838
821
|
--well-page: color-mix(in oklab, hsl(var(--hsl-background)) 70%, black);
|
|
839
822
|
--well-card: var(--well-page);
|
|
840
|
-
--well-raised: var(--
|
|
841
|
-
--well-raised-2: var(--well-
|
|
823
|
+
--well-raised: hsl(var(--hsl-card));
|
|
824
|
+
--well-raised-2: var(--well-raised);
|
|
842
825
|
--well: var(--well-page);
|
|
843
826
|
/* The hairline is 1.5:1 by design; a field edge is non-text UI and needs 3:1.
|
|
844
|
-
* 45% toward the ink
|
|
827
|
+
* 45% toward the ink clears 4:1 on the spine card in both modes. */
|
|
845
828
|
--field-border: color-mix(in oklab, hsl(var(--hsl-border)), hsl(var(--hsl-foreground)) 45%);
|
|
846
829
|
--shadow-raised: var(--shadow-card);
|
|
847
830
|
/* A shadow token joins a comma list, so "none" would void the whole list. */
|
|
848
831
|
--shadow-well: 0 0 0 0 transparent;
|
|
849
|
-
--edge-highlight:
|
|
832
|
+
--edge-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
|
850
833
|
--ix-hover: 0 0 0 1px hsl(var(--hsl-ring) / 0.45), 0 0 0 4px hsl(var(--hsl-ring) / 0.16);
|
|
851
834
|
--ix-hover-border: hsl(var(--hsl-ring) / 0.7);
|
|
852
835
|
--ix-press: 0 0 0 1px hsl(var(--hsl-ring) / 0.95), 0 0 0 4px hsl(var(--hsl-ring) / 0.3);
|
|
@@ -870,8 +853,20 @@
|
|
|
870
853
|
--ix-hover-bg: hsl(var(--hsl-ring) / 0.07);
|
|
871
854
|
--well-page: hsl(var(--hsl-card));
|
|
872
855
|
--well-card: hsl(var(--hsl-background));
|
|
873
|
-
--well-raised:
|
|
874
|
-
--
|
|
856
|
+
--well-raised-2: var(--well-card);
|
|
857
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* Named themes with their own planes keep a flat ladder until each is tuned as
|
|
861
|
+
* Hospitality was: a nested card paints the card, and its field the card's well. */
|
|
862
|
+
[data-theme="aubergine"], [data-theme="aubergine-light"], [data-theme="arena"], [data-theme="arena-light"],
|
|
863
|
+
[data-theme="super"], [data-theme="super-light"], [data-theme="tangle-light"], [data-theme="agents"],
|
|
864
|
+
[data-theme="agents-light"], [data-theme="website"], [data-theme="intelligence"] {
|
|
865
|
+
--hsl-raised: var(--hsl-card);
|
|
866
|
+
--hsl-raised-2: var(--hsl-card);
|
|
867
|
+
--well-raised: var(--well-card);
|
|
868
|
+
--well-raised-2: var(--well-card);
|
|
869
|
+
--edge-highlight: 0 0 0 0 transparent;
|
|
875
870
|
}
|
|
876
871
|
|
|
877
872
|
/* Surface context: keyed only on the Card primitive's data-slot, never on app
|