@tangle-network/brand 1.8.2 → 1.9.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 +10 -0
- package/package.json +1 -1
- package/src/styles/named-themes.css +23 -31
- package/src/styles/named-themes.test.ts +3 -21
- package/src/styles/tokens.css +104 -111
- package/src/styles/tokens.test.ts +72 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 720de9a: Normalize the shared neutral palette and regenerate standalone chart tokens so charts follow the palette. Make user message cards wrap in narrow layouts.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- d163fb8: Align the named tangle-dark island with the neutral canonical dark spine while keeping Tangle accents and dark status colors intact.
|
|
12
|
+
|
|
3
13
|
## 1.8.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -167,40 +167,32 @@
|
|
|
167
167
|
--text-primary: #06231a; --text-secondary: #285048; --text-muted: #5f8472; --text-dim: #82a699;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
/* ───────────────────────── Tangle
|
|
171
|
-
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
|
|
176
|
-
* indigo hue family as tangle-light (~239-246), held to the dark discipline —
|
|
177
|
-
* graphite-indigo neutrals, elevation by lightening, the fill indigo that carries
|
|
178
|
-
* white (7.4:1) with the bright indigo reserved for accent text. The foreground
|
|
179
|
-
* and destructive redeclares equal :root standalone, but the block must be
|
|
180
|
-
* self-sufficient: in the island nesting an omitted token inherits the WRAPPER's
|
|
181
|
-
* light value (near-black secondary text on the dark secondary measured ~1.5:1).
|
|
182
|
-
* Status tokens are deliberately not redeclared: the :root dark status palette is
|
|
183
|
-
* already dark-tuned, same as aubergine/arena. */
|
|
170
|
+
/* ───────────────────────── Tangle — neutral dark spine ─────────────────────
|
|
171
|
+
* tangle-dark names the default :root/.dark spine explicitly. The named block
|
|
172
|
+
* repeats its neutral surfaces and ink so a dark island remains self-sufficient
|
|
173
|
+
* inside a light-named wrapper. Indigo stays in the accent; status tokens inherit
|
|
174
|
+
* the dark-tuned root palette.
|
|
175
|
+
*/
|
|
184
176
|
[data-theme="tangle-dark"] {
|
|
185
|
-
--hsl-background:
|
|
186
|
-
--hsl-card:
|
|
187
|
-
--hsl-popover:
|
|
188
|
-
--hsl-primary: 246 54% 51%;
|
|
189
|
-
--hsl-secondary:
|
|
190
|
-
--hsl-muted:
|
|
191
|
-
--hsl-accent:
|
|
192
|
-
--hsl-destructive: 348 74% 48%;
|
|
193
|
-
--hsl-border:
|
|
177
|
+
--hsl-background: 0 0% 8.6275%; --hsl-foreground: 0 0% 90.1961%;
|
|
178
|
+
--hsl-card: 0 0% 18.8235%; --hsl-card-foreground: 0 0% 90.1961%;
|
|
179
|
+
--hsl-popover: 0 0% 20.7843%; --hsl-popover-foreground: 0 0% 90.1961%;
|
|
180
|
+
--hsl-primary: 246 54% 51%; --hsl-primary-foreground: 0 0% 100%;
|
|
181
|
+
--hsl-secondary: 0 0% 20.7843%; --hsl-secondary-foreground: 0 0% 90.1961%;
|
|
182
|
+
--hsl-muted: 0 0% 20.7843%; --hsl-muted-foreground: 0 0% 70.9804%;
|
|
183
|
+
--hsl-accent: 0 0% 20.7843%; --hsl-accent-foreground: 0 0% 90.1961%;
|
|
184
|
+
--hsl-destructive: 348 74% 48%; --hsl-destructive-foreground: 0 0% 100%;
|
|
185
|
+
--hsl-border: 0 0% 29.8039%; --hsl-input: 0 0% 12.1569%;
|
|
194
186
|
--hsl-ring: 233 100% 87%;
|
|
195
|
-
--depth-1: #
|
|
196
|
-
--bg-root: #
|
|
197
|
-
--md3-surface-container-lowest: #
|
|
198
|
-
--md3-surface-container: #
|
|
199
|
-
--md3-surface-container-highest: #
|
|
200
|
-
--border-subtle: rgba(
|
|
201
|
-
--border-hover: rgba(
|
|
187
|
+
--depth-1: #161616; --depth-2: #303030; --depth-3: #353535; --depth-4: #3b3b3b;
|
|
188
|
+
--bg-root: #161616; --bg-card: var(--depth-2); --bg-input: hsl(var(--hsl-input));
|
|
189
|
+
--md3-surface-container-lowest: #101010; --md3-surface-container-low: #242424;
|
|
190
|
+
--md3-surface-container: #303030; --md3-surface-container-high: #353535;
|
|
191
|
+
--md3-surface-container-highest: #3b3b3b; --md3-outline-variant: #4c4c4c;
|
|
192
|
+
--border-subtle: rgba(255, 255, 255, 0.10); --border-default: rgba(255, 255, 255, 0.16);
|
|
193
|
+
--border-hover: rgba(255, 255, 255, 0.28); --accent-text: #A5B4FC;
|
|
202
194
|
--brand-primary: #7C6CF6; --brand-glow: #A5AAFC;
|
|
203
|
-
--text-primary: #
|
|
195
|
+
--text-primary: #e6e6e6; --text-secondary: #c7c7c7; --text-muted: #b5b5b5; --text-dim: #a5a5a5;
|
|
204
196
|
}
|
|
205
197
|
/* Its light mode. */
|
|
206
198
|
[data-theme="tangle-light"] {
|
|
@@ -167,35 +167,17 @@ describe('named theme: [data-theme="tangle-dark"]', () => {
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
it("
|
|
171
|
-
for (const token of ["hsl-primary", "hsl-ring"
|
|
170
|
+
it("keeps the Tangle accent in the same indigo family", () => {
|
|
171
|
+
for (const token of ["hsl-primary", "hsl-ring"]) {
|
|
172
172
|
const dark = hslIn(css, token);
|
|
173
173
|
const lite = hslIn(light, token);
|
|
174
174
|
expect(
|
|
175
175
|
Math.abs(dark.h - lite.h),
|
|
176
|
-
`--${token} hue must sit within 10 of tangle-light
|
|
176
|
+
`--${token} hue must sit within 10 of tangle-light`,
|
|
177
177
|
).toBeLessThanOrEqual(10);
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
it("tints its neutrals with the hue — nothing achromatic", () => {
|
|
182
|
-
for (const token of [
|
|
183
|
-
"hsl-background",
|
|
184
|
-
"hsl-card",
|
|
185
|
-
"hsl-popover",
|
|
186
|
-
"hsl-secondary",
|
|
187
|
-
"hsl-muted",
|
|
188
|
-
"hsl-accent",
|
|
189
|
-
"hsl-border",
|
|
190
|
-
"hsl-input",
|
|
191
|
-
]) {
|
|
192
|
-
const { h, s } = hslIn(css, token);
|
|
193
|
-
expect(h, `--${token} hue in the indigo band`).toBeGreaterThanOrEqual(220);
|
|
194
|
-
expect(h, `--${token} hue in the indigo band`).toBeLessThanOrEqual(250);
|
|
195
|
-
expect(s, `--${token} is hue-tinted, not dead grey`).toBeGreaterThan(5);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
181
|
it("elevates by lightening, never darkening", () => {
|
|
200
182
|
const planes = ["depth-1", "depth-2", "depth-3", "depth-4"].map((t) =>
|
|
201
183
|
hexRelativeLuminanceIn(css, t),
|
package/src/styles/tokens.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The surface ladder is NEUTRAL GREY and the indigo is trim — accent, never
|
|
4
4
|
* field. It steps in even lightness increments so a card, a nested panel and an
|
|
5
5
|
* overlay each read as a distinct plane by their own fill, and carries a crisp
|
|
6
|
-
* hairline (1.
|
|
6
|
+
* hairline (1.5:1 against the card in dark) because shadow does no work on a dark
|
|
7
7
|
* canvas. Light mirrors it: white paper on a grey canvas, never white-on-white.
|
|
8
8
|
*
|
|
9
9
|
* Apps consume this spine as-is. If a product needs its own accent identity,
|
|
@@ -22,35 +22,34 @@
|
|
|
22
22
|
* an indigo page, and the eye stops finding them. Neutral planes give the brand
|
|
23
23
|
* a field to sit on, so a single accent does all the signalling.
|
|
24
24
|
*
|
|
25
|
-
* The canvas
|
|
26
|
-
*
|
|
27
|
-
* sessions.
|
|
25
|
+
* The canvas stays above pure black (#161616). Near-white ink on it is the
|
|
26
|
+
* glare end of the range, so its luminance leaves room for distinct text tiers.
|
|
28
27
|
*
|
|
29
28
|
* Elevation is carried by surface lightness plus a hairline that can actually be
|
|
30
29
|
* seen — a black shadow on a dark canvas renders as nothing, so shadow does no
|
|
31
|
-
* work here. The hairline is 1.
|
|
30
|
+
* work here. The hairline is about 1.5:1 against the resting card — quiet by design,
|
|
32
31
|
* enough to state an edge without drawing a box around every panel.
|
|
33
32
|
*/
|
|
34
33
|
:root,
|
|
35
34
|
[data-sandbox-ui],
|
|
36
35
|
[data-theme="dark"],
|
|
37
36
|
.dark {
|
|
38
|
-
--md3-surface: #
|
|
39
|
-
--md3-surface-dim: #
|
|
40
|
-
--md3-surface-bright: #
|
|
37
|
+
--md3-surface: #161616; /* L0 page canvas */
|
|
38
|
+
--md3-surface-dim: #101010; /* recessed page */
|
|
39
|
+
--md3-surface-bright: #3b3b3b;
|
|
41
40
|
/* Same value as --md3-surface-dim by design, not by drift: both name the floor
|
|
42
41
|
* of the ladder, and dark has no plane below it to give them. They stay two
|
|
43
42
|
* tokens because the roles diverge in light, where the well is paper (#ffffff)
|
|
44
|
-
* and the recessed page is grey (#
|
|
43
|
+
* and the recessed page is grey (#dadada) — a consumer binding to the wrong one
|
|
45
44
|
* there gets the wrong surface. */
|
|
46
|
-
--md3-surface-container-lowest: #
|
|
47
|
-
--md3-surface-container-low: #
|
|
48
|
-
--md3-surface-container: #
|
|
49
|
-
--md3-surface-container-high: #
|
|
50
|
-
--md3-surface-container-highest: #
|
|
51
|
-
--md3-surface-variant: #
|
|
52
|
-
--md3-on-surface: #
|
|
53
|
-
--md3-on-surface-variant: #
|
|
45
|
+
--md3-surface-container-lowest: #101010; /* L-1 terminal/code well */
|
|
46
|
+
--md3-surface-container-low: #242424; /* L1 chrome, field wells */
|
|
47
|
+
--md3-surface-container: #303030; /* L2 resting card */
|
|
48
|
+
--md3-surface-container-high: #353535; /* L3 nested / hover */
|
|
49
|
+
--md3-surface-container-highest: #3b3b3b; /* L4 overlay */
|
|
50
|
+
--md3-surface-variant: #353535;
|
|
51
|
+
--md3-on-surface: #e6e6e6;
|
|
52
|
+
--md3-on-surface-variant: #b5b5b5;
|
|
54
53
|
|
|
55
54
|
--md3-primary: #A5B4FC;
|
|
56
55
|
--md3-primary-dim: #7C6CF6;
|
|
@@ -67,8 +66,8 @@
|
|
|
67
66
|
--md3-on-secondary-container: #E8E5FF;
|
|
68
67
|
|
|
69
68
|
--md3-tertiary: #10B981;
|
|
70
|
-
--md3-outline: #
|
|
71
|
-
--md3-outline-variant: #
|
|
69
|
+
--md3-outline: #8e8e8e;
|
|
70
|
+
--md3-outline-variant: #4c4c4c;
|
|
72
71
|
|
|
73
72
|
--md3-error: #ff5277;
|
|
74
73
|
--md3-error-dim: #eb2855;
|
|
@@ -82,37 +81,37 @@
|
|
|
82
81
|
--md3-surface-tint: #A5B4FC;
|
|
83
82
|
|
|
84
83
|
/* Semantic HSL bridge — the neutral-grey surface ladder */
|
|
85
|
-
--hsl-background:
|
|
86
|
-
/*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
--hsl-foreground:
|
|
90
|
-
--hsl-card:
|
|
91
|
-
--hsl-card-foreground:
|
|
92
|
-
--hsl-popover:
|
|
84
|
+
--hsl-background: 0 0% 8.6275%; /* L0 canvas #161616 */
|
|
85
|
+
/* Near-white ink on the canvas is the glare end of the range, and the tiers
|
|
86
|
+
* below it need room to separate. This neutral ink keeps the original 10.6:1
|
|
87
|
+
* contrast on the card and leaves four steps the eye can tell apart. */
|
|
88
|
+
--hsl-foreground: 0 0% 90.1961%; /* #e6e6e6 */
|
|
89
|
+
--hsl-card: 0 0% 18.8235%; /* L2 card #303030 */
|
|
90
|
+
--hsl-card-foreground: 0 0% 90.1961%;
|
|
91
|
+
--hsl-popover: 0 0% 20.7843%; /* L3 #353535 — the NESTED plane despite
|
|
93
92
|
the name; the overlay plane is --md3-surface-container-highest. A popover
|
|
94
93
|
carries body text, and the ladder above L3 is reserved for planes that lift
|
|
95
94
|
by shadow, so it sits on L3 and the ink ramp reaches it. */
|
|
96
|
-
--hsl-popover-foreground:
|
|
95
|
+
--hsl-popover-foreground: 0 0% 90.1961%;
|
|
97
96
|
/* The fill indigo, NOT the bright one: `.btn-primary` puts white on this, and
|
|
98
97
|
* #6366F1 carries white at only 4.47:1. This clears the floor at 7.4:1. The
|
|
99
98
|
* bright indigo stays available for accent TEXT via --accent-text. */
|
|
100
99
|
--hsl-primary: 246 54% 51%; /* #4c3fc6 */
|
|
101
100
|
--hsl-primary-foreground: 0 0% 100%;
|
|
102
|
-
--hsl-secondary:
|
|
103
|
-
--hsl-secondary-foreground:
|
|
104
|
-
--hsl-muted:
|
|
105
|
-
--hsl-muted-foreground:
|
|
101
|
+
--hsl-secondary: 0 0% 20.7843%; /* L3 #353535 */
|
|
102
|
+
--hsl-secondary-foreground: 0 0% 90.1961%;
|
|
103
|
+
--hsl-muted: 0 0% 20.7843%; /* L3 hover well */
|
|
104
|
+
--hsl-muted-foreground: 0 0% 70.9804%; /* #b5b5b5 — high enough that the
|
|
106
105
|
reduced-opacity caption derivatives (/70, /60) still clear the large-text
|
|
107
106
|
contrast floor against the L2 card. */
|
|
108
|
-
--hsl-accent:
|
|
109
|
-
--hsl-accent-foreground:
|
|
107
|
+
--hsl-accent: 0 0% 20.7843%; /* neutral hover (indigo lives in accent-surface-strong) */
|
|
108
|
+
--hsl-accent-foreground: 0 0% 90.1961%;
|
|
110
109
|
--hsl-destructive: 348 74% 48%;
|
|
111
110
|
--hsl-destructive-foreground: 0 0% 100%;
|
|
112
|
-
/* Quiet on purpose: 1.
|
|
111
|
+
/* Quiet on purpose: 1.5:1 against the resting card. Shadow does no work on a
|
|
113
112
|
* dark canvas, so the hairline is what states where a plane ends. */
|
|
114
|
-
--hsl-border:
|
|
115
|
-
--hsl-input:
|
|
113
|
+
--hsl-border: 0 0% 29.8039%; /* #4c4c4c = --md3-outline-variant */
|
|
114
|
+
--hsl-input: 0 0% 12.1569%; /* recessed field well #1f1f1f */
|
|
116
115
|
/* Brighter than --primary on purpose: the ring is NON-TEXT and has to clear
|
|
117
116
|
* 3:1 against BOTH canvas and card. The fill indigo is tuned to carry white
|
|
118
117
|
* text instead, so the two cannot be the same value. */
|
|
@@ -153,20 +152,20 @@
|
|
|
153
152
|
|
|
154
153
|
/* Sidebar tokens — L1 chrome: one step BELOW the canvas here, so the nav reads
|
|
155
154
|
* as chrome the content sits in front of rather than as another card. */
|
|
156
|
-
--sidebar-background:
|
|
157
|
-
--sidebar-foreground:
|
|
155
|
+
--sidebar-background: 0 0% 6.2745%; /* L1 chrome #101010 */
|
|
156
|
+
--sidebar-foreground: 0 0% 90.1961%;
|
|
158
157
|
--sidebar-primary: 246 54% 51%;
|
|
159
158
|
--sidebar-primary-foreground: 0 0% 100%;
|
|
160
|
-
--sidebar-accent:
|
|
161
|
-
--sidebar-accent-foreground:
|
|
162
|
-
--sidebar-border:
|
|
159
|
+
--sidebar-accent: 0 0% 20.7843%; /* L3 hover well */
|
|
160
|
+
--sidebar-accent-foreground: 0 0% 90.1961%;
|
|
161
|
+
--sidebar-border: 0 0% 29.8039%;
|
|
163
162
|
--sidebar-ring: 233 100% 87%;
|
|
164
163
|
|
|
165
164
|
/* Depth scale — tracks the MD3 ladder (canvas / card / nested / overlay) */
|
|
166
|
-
--depth-1: #
|
|
167
|
-
--depth-2: #
|
|
168
|
-
--depth-3: #
|
|
169
|
-
--depth-4: #
|
|
165
|
+
--depth-1: #161616;
|
|
166
|
+
--depth-2: #303030;
|
|
167
|
+
--depth-3: #353535;
|
|
168
|
+
--depth-4: #3b3b3b;
|
|
170
169
|
|
|
171
170
|
/* Status colors */
|
|
172
171
|
--status-running: #10B981;
|
|
@@ -252,7 +251,7 @@
|
|
|
252
251
|
--accent-text: #A5B4FC;
|
|
253
252
|
|
|
254
253
|
/* Surfaces — solid fills off the ladder (no radial mesh) */
|
|
255
|
-
--bg-root: #
|
|
254
|
+
--bg-root: #161616;
|
|
256
255
|
--bg-dark: var(--depth-1);
|
|
257
256
|
--bg-card: var(--depth-2);
|
|
258
257
|
--bg-elevated: var(--depth-4);
|
|
@@ -262,7 +261,7 @@
|
|
|
262
261
|
--bg-selection: rgba(124, 108, 246, 0.28);
|
|
263
262
|
|
|
264
263
|
/* Text — neutral */
|
|
265
|
-
/* Four steps the eye can actually separate: 10.6 / 7.8 / 6.
|
|
264
|
+
/* Four steps the eye can actually separate: 10.6 / 7.8 / 6.4 / 5.4 against the
|
|
266
265
|
* L2 card. --text-dim is the faintest tier but it is still TEXT, so it clears
|
|
267
266
|
* the 4.5:1 body floor rather than only the 3:1 large-text one.
|
|
268
267
|
*
|
|
@@ -274,10 +273,10 @@
|
|
|
274
273
|
* card, the chrome and the canvas are fixed points. tokens.test.ts asserts
|
|
275
274
|
* every tier on every plane, so a retune cannot open a gap on one plane while
|
|
276
275
|
* looking correct on another. */
|
|
277
|
-
--text-primary: #
|
|
278
|
-
--text-secondary: #
|
|
279
|
-
--text-muted: #
|
|
280
|
-
--text-dim: #
|
|
276
|
+
--text-primary: #e6e6e6;
|
|
277
|
+
--text-secondary: #c7c7c7;
|
|
278
|
+
--text-muted: #b5b5b5;
|
|
279
|
+
--text-dim: #a5a5a5;
|
|
281
280
|
|
|
282
281
|
/* Brand accents — Tangle */
|
|
283
282
|
--brand-primary: #7C6CF6;
|
|
@@ -292,9 +291,9 @@
|
|
|
292
291
|
--brand-emerald: #10B981;
|
|
293
292
|
|
|
294
293
|
/* Borders — neutral hairlines */
|
|
295
|
-
--border-subtle: rgba(
|
|
296
|
-
--border-default: rgba(
|
|
297
|
-
--border-hover: rgba(
|
|
294
|
+
--border-subtle: rgba(255, 255, 255, 0.10);
|
|
295
|
+
--border-default: rgba(255, 255, 255, 0.16);
|
|
296
|
+
--border-hover: rgba(255, 255, 255, 0.28);
|
|
298
297
|
--border-accent: rgba(129, 140, 248, 0.35);
|
|
299
298
|
--border-accent-hover: rgba(129, 140, 248, 0.50);
|
|
300
299
|
|
|
@@ -420,26 +419,26 @@
|
|
|
420
419
|
[data-sandbox-theme="vault"],
|
|
421
420
|
[data-theme="light"],
|
|
422
421
|
.light {
|
|
423
|
-
/* Light theme —
|
|
422
|
+
/* Light theme — neutral surfaces with purple accents */
|
|
424
423
|
|
|
425
424
|
/* MD3 surface hierarchy */
|
|
426
425
|
/* The light ladder ALTERNATES; it is not monotonic, and that is deliberate.
|
|
427
|
-
* On a
|
|
428
|
-
*
|
|
426
|
+
* On a neutral canvas, a raised plane is PAPER (white) and a recessed one is a
|
|
427
|
+
* neutral WELL. So card and overlay are both white and separate from each other
|
|
429
428
|
* by shadow and ring, not by fill — which is how light-mode elevation actually
|
|
430
429
|
* reads. Forcing a strictly-darkening ladder here is what produced the
|
|
431
430
|
* white-on-white flatness this palette replaced. Pinned in tokens.test.ts. */
|
|
432
|
-
--md3-surface: #
|
|
433
|
-
--md3-surface-dim: #
|
|
431
|
+
--md3-surface: #eaeaea; /* L0 page canvas — neutral grey, BELOW the wells */
|
|
432
|
+
--md3-surface-dim: #dadada;
|
|
434
433
|
--md3-surface-bright: #ffffff;
|
|
435
434
|
--md3-surface-container-lowest: #ffffff; /* insets read as paper */
|
|
436
|
-
--md3-surface-container-low: #
|
|
435
|
+
--md3-surface-container-low: #f7f7f7; /* L1 chrome, field wells — well */
|
|
437
436
|
--md3-surface-container: #ffffff; /* L2 resting card — paper */
|
|
438
|
-
--md3-surface-container-high: #
|
|
437
|
+
--md3-surface-container-high: #f2f2f2; /* L3 nested / hover — well */
|
|
439
438
|
--md3-surface-container-highest: #ffffff; /* L4 overlay — paper + shadow */
|
|
440
|
-
--md3-surface-variant: #
|
|
441
|
-
--md3-on-surface: #
|
|
442
|
-
--md3-on-surface-variant: #
|
|
439
|
+
--md3-surface-variant: #f2f2f2;
|
|
440
|
+
--md3-on-surface: #2d2d2d;
|
|
441
|
+
--md3-on-surface-variant: #4c4c4c;
|
|
443
442
|
|
|
444
443
|
--md3-primary: #3E349E;
|
|
445
444
|
--md3-primary-dim: #352B88;
|
|
@@ -457,8 +456,8 @@
|
|
|
457
456
|
|
|
458
457
|
--md3-tertiary: #065F46;
|
|
459
458
|
--md3-tertiary-container: #D1FAE5;
|
|
460
|
-
--md3-outline: #
|
|
461
|
-
--md3-outline-variant: #
|
|
459
|
+
--md3-outline: #787878;
|
|
460
|
+
--md3-outline-variant: #d0d0d0;
|
|
462
461
|
|
|
463
462
|
--md3-error: #ba1a1a;
|
|
464
463
|
--md3-error-dim: #93000a;
|
|
@@ -471,35 +470,30 @@
|
|
|
471
470
|
--md3-inverse-on-surface: #f0f1f4;
|
|
472
471
|
--md3-surface-tint: #6366F1;
|
|
473
472
|
|
|
474
|
-
/* Semantic HSL bridge — white paper on
|
|
475
|
-
--hsl-background:
|
|
476
|
-
/*
|
|
477
|
-
*
|
|
478
|
-
|
|
479
|
-
* headroom in both places, and a step the eye reads as text instead of as a
|
|
480
|
-
* hole. */
|
|
481
|
-
--hsl-foreground: 235 18% 20.5%; /* #2b2c3e */
|
|
473
|
+
/* Semantic HSL bridge — white paper on an achromatic canvas */
|
|
474
|
+
--hsl-background: 0 0% 91.7647%; /* L0 canvas #eaeaea */
|
|
475
|
+
/* Neutral ink rather than pure black keeps the original 13.7:1 contrast on
|
|
476
|
+
* white paper and 11.4:1 on the canvas: AAA with headroom in both places. */
|
|
477
|
+
--hsl-foreground: 0 0% 17.6471%; /* #2d2d2d */
|
|
482
478
|
--hsl-card: 0 0% 100%; /* L2 card = white paper */
|
|
483
|
-
--hsl-card-foreground:
|
|
479
|
+
--hsl-card-foreground: 0 0% 17.6471%;
|
|
484
480
|
--hsl-popover: 0 0% 100%;
|
|
485
|
-
--hsl-popover-foreground:
|
|
481
|
+
--hsl-popover-foreground: 0 0% 17.6471%;
|
|
486
482
|
--hsl-primary: 246 54% 51%; /* #4c3fc6 — carries white at 7.4:1 */
|
|
487
483
|
--hsl-primary-foreground: 0 0% 100%;
|
|
488
|
-
--hsl-secondary:
|
|
489
|
-
--hsl-secondary-foreground:
|
|
490
|
-
--hsl-muted:
|
|
491
|
-
/*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
--hsl-
|
|
497
|
-
--hsl-accent: 240 26% 96%; /* neutral hover */
|
|
498
|
-
--hsl-accent-foreground: 235 18% 20.5%;
|
|
484
|
+
--hsl-secondary: 0 0% 94.902%; /* L3 #f2f2f2 */
|
|
485
|
+
--hsl-secondary-foreground: 0 0% 17.6471%;
|
|
486
|
+
--hsl-muted: 0 0% 94.902%; /* L3 hover well */
|
|
487
|
+
/* The shared components fade captions with /70 and /60, so a translucent
|
|
488
|
+
* foreground renders composited over the surface behind it. This neutral value
|
|
489
|
+
* keeps 3.0:1 at /60 and 8.6:1 at full strength on paper. */
|
|
490
|
+
--hsl-muted-foreground: 0 0% 29.8039%; /* #4c4c4c text-muted */
|
|
491
|
+
--hsl-accent: 0 0% 94.902%; /* neutral hover */
|
|
492
|
+
--hsl-accent-foreground: 0 0% 17.6471%;
|
|
499
493
|
--hsl-destructive: 0 72% 41%;
|
|
500
494
|
--hsl-destructive-foreground: 0 0% 100%;
|
|
501
|
-
--hsl-border:
|
|
502
|
-
--hsl-input:
|
|
495
|
+
--hsl-border: 0 0% 81.5686%; /* #d0d0d0 = --md3-outline-variant */
|
|
496
|
+
--hsl-input: 0 0% 96.0784%; /* field well #f5f5f5 */
|
|
503
497
|
--hsl-ring: 246 54% 51%;
|
|
504
498
|
--hsl-success: 160 84% 30%;
|
|
505
499
|
--hsl-warning: 41 96% 50%;
|
|
@@ -508,9 +502,9 @@
|
|
|
508
502
|
/* depth-2 (card) and depth-4 (elevated) are BOTH paper, on purpose: in light,
|
|
509
503
|
* a popover does not out-lighten a white card — it lifts by shadow. See the
|
|
510
504
|
* ladder note above. */
|
|
511
|
-
--depth-1: #
|
|
505
|
+
--depth-1: #eaeaea; /* canvas */
|
|
512
506
|
--depth-2: #ffffff; /* card — paper */
|
|
513
|
-
--depth-3: #
|
|
507
|
+
--depth-3: #f2f2f2; /* nested well */
|
|
514
508
|
--depth-4: #ffffff; /* elevated — paper, separated by shadow */
|
|
515
509
|
|
|
516
510
|
--status-running: #059669;
|
|
@@ -579,12 +573,12 @@
|
|
|
579
573
|
|
|
580
574
|
/* Sidebar — light, flat surface + hairline */
|
|
581
575
|
--sidebar-background: 0 0% 100%; /* white paper */
|
|
582
|
-
--sidebar-foreground:
|
|
576
|
+
--sidebar-foreground: 0 0% 17.6471%;
|
|
583
577
|
--sidebar-primary: 246 54% 51%; /* #4c3fc6 */
|
|
584
578
|
--sidebar-primary-foreground: 0 0% 100%;
|
|
585
|
-
--sidebar-accent:
|
|
586
|
-
--sidebar-accent-foreground:
|
|
587
|
-
--sidebar-border:
|
|
579
|
+
--sidebar-accent: 0 0% 94.902%; /* L3 hover well */
|
|
580
|
+
--sidebar-accent-foreground: 0 0% 17.6471%;
|
|
581
|
+
--sidebar-border: 0 0% 81.5686%; /* = --md3-outline-variant */
|
|
588
582
|
--sidebar-ring: 246 54% 51%;
|
|
589
583
|
|
|
590
584
|
--mesh-teal: transparent;
|
|
@@ -592,7 +586,7 @@
|
|
|
592
586
|
--mesh-blue: transparent;
|
|
593
587
|
|
|
594
588
|
/* Surfaces — flat single-neutral light */
|
|
595
|
-
--bg-root: #
|
|
589
|
+
--bg-root: #eaeaea;
|
|
596
590
|
--bg-dark: var(--depth-1);
|
|
597
591
|
--bg-card: var(--depth-2);
|
|
598
592
|
/* Paper, exactly like --bg-card: in light you cannot raise a plane by making it
|
|
@@ -606,19 +600,18 @@
|
|
|
606
600
|
--bg-hover: rgba(76, 63, 198, 0.06);
|
|
607
601
|
--bg-selection: rgba(76, 63, 198, 0.16);
|
|
608
602
|
|
|
609
|
-
/* Mirrors the dark ramp's spacing
|
|
610
|
-
*
|
|
603
|
+
/* Mirrors the dark ramp's spacing against white paper. The ink uses neutral
|
|
604
|
+
* grays so purple stays reserved for interaction accents.
|
|
611
605
|
*
|
|
612
|
-
* --text-dim is pinned from the CANVAS (#
|
|
613
|
-
* the faintest tier, so the
|
|
614
|
-
*
|
|
615
|
-
* 4.6 on the canvas, 5.6 on the card — both floors are asserted in
|
|
606
|
+
* --text-dim is pinned from the CANVAS (#eaeaea), not the white card: it is
|
|
607
|
+
* the faintest tier, so the lighter L0 is the surface that binds it. Its 4.6:1
|
|
608
|
+
* canvas and 5.6:1 card contrasts clear the body-text floor, as asserted in
|
|
616
609
|
* tokens.test.ts, because a ratio quoted in a comment drifts the moment a
|
|
617
610
|
* surface moves and nothing catches it. */
|
|
618
|
-
--text-primary: #
|
|
619
|
-
--text-secondary: #
|
|
620
|
-
--text-muted: #
|
|
621
|
-
--text-dim: #
|
|
611
|
+
--text-primary: #2d2d2d;
|
|
612
|
+
--text-secondary: #3f3f3f;
|
|
613
|
+
--text-muted: #4c4c4c;
|
|
614
|
+
--text-dim: #686868;
|
|
622
615
|
|
|
623
616
|
--brand-primary: #6366F1;
|
|
624
617
|
--brand-strong: #2E2A5E;
|
|
@@ -631,9 +624,9 @@
|
|
|
631
624
|
--brand-vibrant: #6366F1;
|
|
632
625
|
--brand-emerald: #059669;
|
|
633
626
|
|
|
634
|
-
--border-subtle: rgba(
|
|
635
|
-
--border-default: rgba(
|
|
636
|
-
--border-hover: rgba(
|
|
627
|
+
--border-subtle: rgba(0, 0, 0, 0.10);
|
|
628
|
+
--border-default: rgba(0, 0, 0, 0.14);
|
|
629
|
+
--border-hover: rgba(0, 0, 0, 0.24);
|
|
637
630
|
--border-accent: rgba(91, 78, 212, 0.18);
|
|
638
631
|
--border-accent-hover: rgba(91, 78, 212, 0.35);
|
|
639
632
|
|
|
@@ -727,7 +720,7 @@
|
|
|
727
720
|
* draws as a --chart-muted outline, the same mark as an unsolved attempt. No
|
|
728
721
|
* categorical palette ships: identity is carried by direct labels.
|
|
729
722
|
*
|
|
730
|
-
* Contrast against the dark canvas (#
|
|
723
|
+
* Contrast against the dark canvas (#161616) and white paper, checked with the
|
|
731
724
|
* dataviz palette validator and WCAG ratios:
|
|
732
725
|
* ink 14.45 / 13.70, ink-muted 7.34 / 5.56, accent 6.05 / 7.41,
|
|
733
726
|
* muted 5.54 / 4.41, grid 1.46 / 1.12 (recessive by design).
|
|
@@ -13,12 +13,9 @@ import {
|
|
|
13
13
|
/**
|
|
14
14
|
* The canonical spine's structural contract.
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* numbers are therefore not decoration; they are the contract. These assertions
|
|
20
|
-
* pin the PROPERTIES rather than the exact values, so a retune stays free but a
|
|
21
|
-
* collapse back to flat does not.
|
|
16
|
+
* The canonical surface and ink spine is neutral so purple can signal interaction.
|
|
17
|
+
* Each surface remains distinct by fill and elevation. These assertions pin that
|
|
18
|
+
* structure without fixing every shade, so the ladder can be retuned safely.
|
|
22
19
|
*/
|
|
23
20
|
const tokens = readFileSync(
|
|
24
21
|
path.resolve(import.meta.dirname, "tokens.css"),
|
|
@@ -31,27 +28,73 @@ const tokens = readFileSync(
|
|
|
31
28
|
const DARK = blockIn(tokens, ".dark");
|
|
32
29
|
const LIGHT = blockIn(tokens, ".light");
|
|
33
30
|
|
|
34
|
-
describe("canonical
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
describe("canonical surfaces, ink, and chrome are neutral", () => {
|
|
32
|
+
const neutralHslRoles = [
|
|
33
|
+
"hsl-background",
|
|
34
|
+
"hsl-foreground",
|
|
35
|
+
"hsl-card",
|
|
36
|
+
"hsl-card-foreground",
|
|
37
|
+
"hsl-popover",
|
|
38
|
+
"hsl-popover-foreground",
|
|
39
|
+
"hsl-secondary",
|
|
40
|
+
"hsl-secondary-foreground",
|
|
41
|
+
"hsl-muted",
|
|
42
|
+
"hsl-muted-foreground",
|
|
43
|
+
"hsl-accent",
|
|
44
|
+
"hsl-accent-foreground",
|
|
45
|
+
"hsl-border",
|
|
46
|
+
"hsl-input",
|
|
47
|
+
"sidebar-background",
|
|
48
|
+
"sidebar-foreground",
|
|
49
|
+
"sidebar-accent",
|
|
50
|
+
"sidebar-accent-foreground",
|
|
51
|
+
"sidebar-border",
|
|
52
|
+
];
|
|
53
|
+
const neutralHexRoles = [
|
|
54
|
+
"md3-surface",
|
|
55
|
+
"md3-surface-dim",
|
|
56
|
+
"md3-surface-bright",
|
|
57
|
+
"md3-surface-container-lowest",
|
|
58
|
+
"md3-surface-container-low",
|
|
59
|
+
"md3-surface-container",
|
|
60
|
+
"md3-surface-container-high",
|
|
61
|
+
"md3-surface-container-highest",
|
|
62
|
+
"md3-surface-variant",
|
|
63
|
+
"md3-on-surface",
|
|
64
|
+
"md3-on-surface-variant",
|
|
65
|
+
"md3-outline",
|
|
66
|
+
"md3-outline-variant",
|
|
67
|
+
"depth-1",
|
|
68
|
+
"depth-2",
|
|
69
|
+
"depth-3",
|
|
70
|
+
"depth-4",
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
for (const [theme, spine] of [
|
|
74
|
+
["dark", DARK],
|
|
75
|
+
["light", LIGHT],
|
|
76
|
+
] as const) {
|
|
77
|
+
it(`${theme}: shared surface and ink roles are achromatic`, () => {
|
|
78
|
+
for (const token of neutralHslRoles) {
|
|
79
|
+
expect(hslIn(spine, token).s, `--${token}`).toBe(0);
|
|
80
|
+
}
|
|
81
|
+
for (const token of neutralHexRoles) {
|
|
82
|
+
const hex = spine.match(
|
|
83
|
+
new RegExp(`--${token}:\\s*(#[0-9a-fA-F]{6})`),
|
|
84
|
+
)?.[1];
|
|
85
|
+
expect(hex, `--${token} must be defined`).toBeDefined();
|
|
86
|
+
expect(hex?.slice(1, 3), `--${token} red equals green`).toBe(
|
|
87
|
+
hex?.slice(3, 5),
|
|
88
|
+
);
|
|
89
|
+
expect(hex?.slice(3, 5), `--${token} green equals blue`).toBe(
|
|
90
|
+
hex?.slice(5, 7),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
54
96
|
|
|
97
|
+
describe("canonical dark spine", () => {
|
|
55
98
|
it("guards reading comfort at the INK, not at the canvas", () => {
|
|
56
99
|
// A deep canvas is fine — glare comes from the ratio between ink and ground,
|
|
57
100
|
// not from the ground alone. So the floor here is loose, and the real guard
|
|
@@ -388,7 +431,7 @@ describe("input tokens carry two DIFFERENT roles and must not be conflated", ()
|
|
|
388
431
|
});
|
|
389
432
|
|
|
390
433
|
describe("canonical light spine", () => {
|
|
391
|
-
it("is white paper on a
|
|
434
|
+
it("is white paper on a neutral canvas, never white-on-white", () => {
|
|
392
435
|
const canvas = hslIn(LIGHT, "hsl-background");
|
|
393
436
|
const card = hslIn(LIGHT, "hsl-card");
|
|
394
437
|
expect(card.l, "the card is paper").toBe(100);
|
|
@@ -396,22 +439,11 @@ describe("canonical light spine", () => {
|
|
|
396
439
|
canvas.l,
|
|
397
440
|
"a pure-white canvas gives a white card nothing to lift off",
|
|
398
441
|
).toBeLessThan(97);
|
|
399
|
-
// Mirrors the dark spine exactly: HUE is the brand constraint and is pinned;
|
|
400
|
-
// saturation is a taste dial and is not. How far the surfaces lean is a live
|
|
401
|
-
// product decision, so asserting a band here only guarantees this test fails
|
|
402
|
-
// the next time that decision is revisited.
|
|
403
|
-
expect(canvas.h, "canvas hue sits in the indigo band").toBeGreaterThanOrEqual(
|
|
404
|
-
215,
|
|
405
|
-
);
|
|
406
|
-
expect(canvas.h).toBeLessThanOrEqual(260);
|
|
407
|
-
expect(canvas.s, "a colourless canvas has left the brand behind").toBeGreaterThan(
|
|
408
|
-
5,
|
|
409
|
-
);
|
|
410
442
|
});
|
|
411
443
|
|
|
412
444
|
it("ALTERNATES paper and well — light elevation is not a darkening ramp", () => {
|
|
413
|
-
// Deliberately not monotonic. On a
|
|
414
|
-
// a recessed one is a
|
|
445
|
+
// Deliberately not monotonic. On a neutral canvas a raised plane is paper and
|
|
446
|
+
// a recessed one is a neutral well, so the ladder alternates. Pinning it stops
|
|
415
447
|
// a well-meaning "fix" from forcing a strictly-ordered ramp, which is exactly
|
|
416
448
|
// what produced the white-on-white flatness this palette replaced.
|
|
417
449
|
const paper = ["md3-surface-container", "md3-surface-container-highest"];
|