@crewlethq/tokens 0.4.4 → 0.5.1
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/README.md +359 -31
- package/dist/css/base.css +9 -3
- package/dist/css/breakpoint.css +2 -1
- package/dist/css/fonts.css +11 -11
- package/dist/css/legacy.css +11 -9
- package/dist/css/themes.css +267 -300
- package/dist/css/tokens.css +55 -65
- package/dist/index.d.ts +47 -59
- package/dist/index.js +50 -62
- package/dist/themes.d.ts +98 -126
- package/dist/themes.js +98 -126
- package/fonts/OFL.txt +15 -14
- package/fonts/README.md +22 -13
- package/fonts/SHA256SUMS +4 -4
- package/fonts/geist-latin-ext.woff2 +0 -0
- package/fonts/geist-latin.woff2 +0 -0
- package/fonts/geist-mono-latin-ext.woff2 +0 -0
- package/fonts/geist-mono-latin.woff2 +0 -0
- package/package.json +1 -1
- package/test/color.mjs +26 -12
- package/test/palette.d.mts +88 -3
- package/test/palette.mjs +464 -179
- package/test/palette.test.mjs +448 -9
- package/fonts/inter-latin-ext.woff2 +0 -0
- package/fonts/inter-latin.woff2 +0 -0
- package/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/fonts/jetbrains-mono-latin.woff2 +0 -0
package/test/palette.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* selector: themes.css is imported second and is what paints.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import { cascade, chroma, contrast, deltaE, flatten, parseHex, separation, VISIONS, withAlpha } from './color.mjs';
|
|
33
|
+
import { cascade, chroma, contrast, deltaE, flatten, parseBlocks, parseHex, separation, VISIONS, withAlpha } from './color.mjs';
|
|
34
34
|
|
|
35
35
|
export * from './color.mjs';
|
|
36
36
|
|
|
@@ -42,23 +42,89 @@ export const SOFT_ALPHA = 0.12;
|
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Every opaque ground a piece of text can end up on.
|
|
45
|
+
* Every opaque ground a piece of text can end up on: the four RUNGS.
|
|
46
|
+
*
|
|
47
|
+
* frame the application ground: the rail, and the body around the sheet
|
|
48
|
+
* background the SHEET, the page content sits on
|
|
49
|
+
* subtle the CARD: a card, a popover, a dialog body
|
|
50
|
+
* elevated RAISED: a chip, a well, a key cap, a track, a lifted row
|
|
46
51
|
*
|
|
47
52
|
* This list is the whole point of the exercise. A text ramp anchored to the
|
|
48
53
|
* panel it was designed against and then spent on a selected row inside a
|
|
49
54
|
* dialog body is how a palette ships six steps under 4.5:1 without anybody
|
|
50
55
|
* noticing.
|
|
56
|
+
*
|
|
57
|
+
* It is exported so a component suite measures on these grounds rather than
|
|
58
|
+
* on a copy of them: six suites in @crewlethq/ui each kept a copy of their
|
|
59
|
+
* own, and five of the six had already drifted from this list.
|
|
51
60
|
*/
|
|
52
61
|
export const OPAQUE_SURFACES = [
|
|
62
|
+
'--color-surface-frame',
|
|
53
63
|
'--color-surface-background',
|
|
54
64
|
'--color-surface-subtle',
|
|
55
|
-
'--color-surface-muted',
|
|
56
65
|
'--color-surface-elevated',
|
|
57
|
-
'--color-surface-topbar',
|
|
58
|
-
'--color-surface-topbar-lift',
|
|
59
|
-
'--color-surface-topbar-active',
|
|
60
66
|
];
|
|
61
67
|
|
|
68
|
+
/**
|
|
69
|
+
* How far apart each rung sits from the one beneath it, in OKLab dE, and why.
|
|
70
|
+
*
|
|
71
|
+
* The four rungs are one ladder, and each step is held to what the step has
|
|
72
|
+
* to do on its own:
|
|
73
|
+
*
|
|
74
|
+
* - THE SHEET OVER THE FRAME is the whole layering device: a content sheet
|
|
75
|
+
* floating inside the application ground. At dE 3, the floor this file holds
|
|
76
|
+
* every "can a reader notice it" difference to, the sheet reads as a
|
|
77
|
+
* separate plane; the approved dark sheet, #0f0f12, measured 2.91 and was
|
|
78
|
+
* nudged to clear it.
|
|
79
|
+
* - A CARD OVER THE SHEET is deliberately near-flat, dE 1.5, because a card is
|
|
80
|
+
* found by its HAIRLINE rather than by its fill: a page of cards each lifted
|
|
81
|
+
* by dE 3 is a relief map. So the step comes with a second rule, and that is
|
|
82
|
+
* the one that carries the card: the plain border (CARD_HAIRLINE) has to be
|
|
83
|
+
* a line a reader can see, dE 3 against the card it bounds and against the
|
|
84
|
+
* sheet around it. That a card actually DRAWS that border is a component
|
|
85
|
+
* rule, held by the Card suite in @crewlethq/ui, because a palette cannot see
|
|
86
|
+
* a stylesheet.
|
|
87
|
+
* - RAISED OVER THE CARD is a chip, a well or a key cap with no border of its
|
|
88
|
+
* own, so it is the fill alone that says it is there: dE 3.
|
|
89
|
+
*
|
|
90
|
+
* There is deliberately no rule between the card and the frame, or raised and
|
|
91
|
+
* the sheet: nothing is drawn across two rungs at once, and a rule over a pair
|
|
92
|
+
* nobody puts side by side is a floor that binds for no reason.
|
|
93
|
+
*/
|
|
94
|
+
export const RUNG_STEPS = [
|
|
95
|
+
['the sheet lifts off the frame', '--color-surface-background', '--color-surface-frame', 3],
|
|
96
|
+
['a card separates from the sheet', '--color-surface-subtle', '--color-surface-background', 1.5],
|
|
97
|
+
['raised separates from the card', '--color-surface-elevated', '--color-surface-subtle', 3],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The line that finds a card, and the two grounds it runs between: the card
|
|
102
|
+
* inside it and the sheet outside it. See RUNG_STEPS for why the card leans
|
|
103
|
+
* on it.
|
|
104
|
+
*/
|
|
105
|
+
export const CARD_HAIRLINE = ['--color-border-default', '--color-surface-subtle', '--color-surface-background'];
|
|
106
|
+
export const HAIRLINE_DE = 3;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* THE OVERLAY STEPS HAVE TO BE SEEN, on every rung.
|
|
110
|
+
*
|
|
111
|
+
* A translucent overlay is one alpha on four grounds, and the alpha is FITTED
|
|
112
|
+
* (the hover token says how): the approved design paints its hover as one
|
|
113
|
+
* opaque colour, and a stronger overlay costs the tertiary text step contrast
|
|
114
|
+
* on the rungs it lightens or darkens most. Left to that trade alone, the
|
|
115
|
+
* cheapest answer for the text is an overlay nobody sees, and the light
|
|
116
|
+
* palette's frame kept asking for it: the design's own light hover sits dE
|
|
117
|
+
* 0.60 off the frame. This is the floor that stops it. A hovered row separates
|
|
118
|
+
* from its rung by dE 3, and a pressed row from a hovered one by dE 3 more,
|
|
119
|
+
* because dE 3 is the floor this file holds every "can a reader notice it"
|
|
120
|
+
* difference to. The marketing root is exempt, as it is from the rungs.
|
|
121
|
+
*/
|
|
122
|
+
export const OVERLAY_STEPS = [
|
|
123
|
+
['a hovered row is visible on every rung', '--color-surface-hover', null],
|
|
124
|
+
['a pressed row is visible over a hovered one', '--color-surface-pressed', '--color-surface-hover'],
|
|
125
|
+
];
|
|
126
|
+
export const OVERLAY_DE = 3;
|
|
127
|
+
|
|
62
128
|
/**
|
|
63
129
|
* The translucent overlays composited onto each ground. A hovered or pressed
|
|
64
130
|
* row IS one of these, and so is an inset well.
|
|
@@ -84,9 +150,6 @@ export const INK_STEPS = [
|
|
|
84
150
|
'--color-feedback-warning-ink',
|
|
85
151
|
'--color-feedback-danger-ink',
|
|
86
152
|
'--color-feedback-info-ink',
|
|
87
|
-
'--color-phase-onboarding-ink',
|
|
88
|
-
'--color-phase-execute-ink',
|
|
89
|
-
'--color-phase-review-ink',
|
|
90
153
|
];
|
|
91
154
|
|
|
92
155
|
/** A hue used as a MARK, or as a fill behind a label. */
|
|
@@ -96,9 +159,6 @@ export const FILL_STEPS = [
|
|
|
96
159
|
'--color-feedback-warning',
|
|
97
160
|
'--color-feedback-danger',
|
|
98
161
|
'--color-feedback-info',
|
|
99
|
-
'--color-phase-onboarding',
|
|
100
|
-
'--color-phase-execute',
|
|
101
|
-
'--color-phase-review',
|
|
102
162
|
];
|
|
103
163
|
|
|
104
164
|
export const STATUS = [
|
|
@@ -107,14 +167,42 @@ export const STATUS = [
|
|
|
107
167
|
'--color-feedback-danger',
|
|
108
168
|
'--color-feedback-info',
|
|
109
169
|
];
|
|
110
|
-
export const PHASE = ['--color-phase-onboarding', '--color-phase-execute', '--color-phase-review'];
|
|
111
|
-
export const DATA = ['--color-data-1', '--color-data-2', '--color-data-3', '--color-data-4', '--color-data-5'];
|
|
112
170
|
|
|
113
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* The chart series, in the order a figure assigns them: blue, orange, aqua and
|
|
173
|
+
* an ochre yellow, the four the approved design draws.
|
|
174
|
+
*
|
|
175
|
+
* FOUR, because four is what both floors below can hold at once. The design's
|
|
176
|
+
* four clear them with room (its tightest neighbours sit dE 19.8 apart under
|
|
177
|
+
* normal vision), and a fifth would have to sit between two of them in a hue
|
|
178
|
+
* budget the reserved red, the accent and a dichromat's collapse of red onto
|
|
179
|
+
* green have already spent.
|
|
180
|
+
*/
|
|
181
|
+
export const DATA = ['--color-data-1', '--color-data-2', '--color-data-3', '--color-data-4'];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* How far apart two NEIGHBOURING series sit: dE 9 under every vision, and dE
|
|
185
|
+
* 15 under normal vision on top of it.
|
|
186
|
+
*
|
|
187
|
+
* Neighbours, because a stacked bar, a legend and a line chart assign series
|
|
188
|
+
* in order and only neighbours touch. The dichromat floor is the budget a
|
|
189
|
+
* palette can afford once deuteranopia has folded red onto green; on its own
|
|
190
|
+
* it let a pair sit dE 9 apart for EVERY reader, which to full-colour vision
|
|
191
|
+
* is two shades of one hue rather than two series. The normal-vision floor is
|
|
192
|
+
* the companion gate of the validated categorical method the design's four
|
|
193
|
+
* hues come from: its worst adjacent pair clears dE 15 unsimulated, so a
|
|
194
|
+
* reader who sees every colour is never the one left squinting at a legend.
|
|
195
|
+
*/
|
|
196
|
+
export const DATA_ADJACENT_DE = 9;
|
|
197
|
+
export const DATA_ADJACENT_NORMAL_DE = 15;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A fill with the label a component paints on it: the primary action at rest,
|
|
201
|
+
* under the pointer and pressed, the danger fill, and the inverse ground. The
|
|
202
|
+
* label on every fill but the last is on-accent, which is white in every
|
|
203
|
+
* palette, and each fill is measured over every ground it can sit on.
|
|
204
|
+
*/
|
|
114
205
|
export const LABEL_ON_FILL = [
|
|
115
|
-
['--color-text-on-brand', '--color-brand-primary'],
|
|
116
|
-
['--color-text-on-brand', '--color-brand-primary-hover'],
|
|
117
|
-
['--color-text-on-brand', '--color-brand-primary-active'],
|
|
118
206
|
['--color-text-on-accent', '--color-brand-accent'],
|
|
119
207
|
['--color-text-on-accent', '--color-brand-accent-hover'],
|
|
120
208
|
['--color-text-on-accent', '--color-brand-accent-active'],
|
|
@@ -123,6 +211,45 @@ export const LABEL_ON_FILL = [
|
|
|
123
211
|
['--color-text-inverse', '--color-surface-inverse'],
|
|
124
212
|
];
|
|
125
213
|
|
|
214
|
+
/**
|
|
215
|
+
* THE PRIMARY ACTION'S THREE FILLS, at rest, under the pointer and pressed,
|
|
216
|
+
* each held against the one before it: [rule, the step, the fill it follows].
|
|
217
|
+
*
|
|
218
|
+
* The primary action IS the accent, in both palettes, and on a filled button
|
|
219
|
+
* the fill is the whole of the feedback, so every step clears two things. It
|
|
220
|
+
* is SEEN: ACTION_DE, the floor this file holds every "can a reader notice
|
|
221
|
+
* it" difference to. And it moves AWAY from its label, which is ACTION_LABEL
|
|
222
|
+
* on every fill: a hover or a press only ever gains contrast. The approved
|
|
223
|
+
* design brightened its hover instead (brightness(1.08)), which is the step
|
|
224
|
+
* TOWARD a white label, and on the dark accent it took white from 4.53:1 to
|
|
225
|
+
* 4.18:1, under the text floor at the moment a reader is about to press. The
|
|
226
|
+
* label rule above catches that number in dark; this rule is what catches the
|
|
227
|
+
* same move in light, where the brightened step still measured 5.01:1 and
|
|
228
|
+
* only its direction was wrong.
|
|
229
|
+
*
|
|
230
|
+
* The DESTRUCTIVE action is the other filled action, and it is held the same
|
|
231
|
+
* way: a toast's destructive action is the danger fill under the same white
|
|
232
|
+
* label, and its hover is --color-feedback-danger-hover. The danger fill is
|
|
233
|
+
* fitted from the design's stopped red and the design draws no hover for it,
|
|
234
|
+
* so this rule is what decides where its hover goes, as it decides the
|
|
235
|
+
* accent's: a step a reader can see, away from the label.
|
|
236
|
+
*/
|
|
237
|
+
export const ACTION_STEPS = [
|
|
238
|
+
['a hovered primary action is a visible step away from its label', '--color-brand-accent-hover', '--color-brand-accent'],
|
|
239
|
+
[
|
|
240
|
+
'a pressed primary action is a visible step past a hovered one',
|
|
241
|
+
'--color-brand-accent-active',
|
|
242
|
+
'--color-brand-accent-hover',
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
'a hovered destructive action is a visible step away from its label',
|
|
246
|
+
'--color-feedback-danger-hover',
|
|
247
|
+
'--color-feedback-danger',
|
|
248
|
+
],
|
|
249
|
+
];
|
|
250
|
+
export const ACTION_LABEL = '--color-text-on-accent';
|
|
251
|
+
export const ACTION_DE = 3;
|
|
252
|
+
|
|
126
253
|
/** The ten seeded avatar grounds, each carrying white initials. */
|
|
127
254
|
export const AVATAR_TINTS = Array.from({ length: 10 }, (_, i) => `--color-avatar-tint-${i}`);
|
|
128
255
|
|
|
@@ -131,51 +258,80 @@ export const AVATAR_TINTS = Array.from({ length: 10 }, (_, i) => `--color-avatar
|
|
|
131
258
|
// ---------------------------------------------------------------------------
|
|
132
259
|
|
|
133
260
|
/**
|
|
134
|
-
* The application rail,
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
261
|
+
* The application rail, and every pair it paints on its own grounds.
|
|
262
|
+
*
|
|
263
|
+
* The rail stands on the FRAME rather than on the sheet, which no other
|
|
264
|
+
* column of text does. Its rows have three grounds: the frame itself, the
|
|
265
|
+
* frame under the hover overlay, and RAISED for the row the reader is on
|
|
266
|
+
* (RAIL_CURRENT_ROW). The badge, a count of what is waiting on the reader, is
|
|
267
|
+
* the one hue in it, an opaque accent fill carrying the on-accent label; a
|
|
268
|
+
* project's key chip is the raised rung inside the plain hairline, carrying
|
|
269
|
+
* the secondary ink, on every row alike.
|
|
270
|
+
*
|
|
271
|
+
* WHAT THIS TABLE IS, HONESTLY. Every ink in it is also reached by a rule
|
|
272
|
+
* above, which measures the same composites over every rung: the text steps
|
|
273
|
+
* over the overlays on the frame, the focus ring over raised, the on-accent
|
|
274
|
+
* label on the accent fill. So none of these can fail on its own. What it is,
|
|
275
|
+
* is the MAP from what the rail paints to the ground it lands on, which the
|
|
276
|
+
* SidebarNav suite in @crewlethq/ui reads the stylesheet's own pairs against,
|
|
277
|
+
* so a rail that spent a token nobody measured there says so by name. The
|
|
278
|
+
* rules the table cannot reach are the current row's own, below.
|
|
151
279
|
*/
|
|
152
|
-
export const RAIL_GROUND = '--color-surface-
|
|
280
|
+
export const RAIL_GROUND = '--color-surface-frame';
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The row the reader is on, as [fill, hairline]: raised, with the plain
|
|
284
|
+
* border drawn round it INSIDE the row, and the primary ink.
|
|
285
|
+
*
|
|
286
|
+
* It is found the way a card is found. Its fill is allowed to be near-flat on
|
|
287
|
+
* the frame, RAIL_CURRENT_LIFT, because in light raised is only dE 1.84 off
|
|
288
|
+
* the frame (the design draws it there); what carries the row is the
|
|
289
|
+
* hairline, held to HAIRLINE_DE against the fill it is drawn on. The hairline
|
|
290
|
+
* is also what tells the current row from a HOVERED one, which draws none: in
|
|
291
|
+
* dark the two fills land dE 1.56 apart, because the fitted hover overlay
|
|
292
|
+
* lifts the frame almost to raised.
|
|
293
|
+
*
|
|
294
|
+
* It used to be the accent's own soft tint with the label in the accent ink.
|
|
295
|
+
* The accent is the primary action's fill now, so a violet row in the rail
|
|
296
|
+
* would read as a second primary on every screen, beside the one the screen
|
|
297
|
+
* actually asks for; where the reader is, is the row's position, its lift and
|
|
298
|
+
* its line.
|
|
299
|
+
*/
|
|
300
|
+
export const RAIL_CURRENT_ROW = ['--color-surface-elevated', '--color-border-default'];
|
|
301
|
+
export const RAIL_CURRENT_LIFT = 1.5;
|
|
153
302
|
|
|
154
303
|
/** The three grounds a row in the rail can have, over that ground. */
|
|
155
304
|
export const RAIL_ROWS = [
|
|
156
305
|
['a row', null],
|
|
157
306
|
['a hovered row', '--color-surface-hover'],
|
|
158
|
-
['the row the reader is on',
|
|
307
|
+
['the row the reader is on', RAIL_CURRENT_ROW[0]],
|
|
159
308
|
];
|
|
160
309
|
|
|
161
310
|
/**
|
|
162
|
-
* What the rail paints: the ink, the
|
|
311
|
+
* What the rail paints: the ink, the fill it is drawn on (null for the row's
|
|
163
312
|
* own ground), which rows carry it, and the floor it clears.
|
|
164
313
|
*
|
|
165
|
-
* The
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
314
|
+
* The glyph has no entry of its own: it is drawn in the row's colour on
|
|
315
|
+
* every row, at rest as much as pointed or current, so each label entry below
|
|
316
|
+
* is the glyph's too. A resting row is the secondary step, as the approved
|
|
317
|
+
* design draws it, and a hovered or current one lifts to the primary.
|
|
169
318
|
*/
|
|
170
319
|
export const RAIL_PAINTS = [
|
|
171
|
-
["a row's label", '--color-text-secondary', null, ['a row'], 4.5],
|
|
320
|
+
["a row's label and glyph", '--color-text-secondary', null, ['a row'], 4.5],
|
|
172
321
|
['a hovered row, label and glyph', '--color-text-primary', null, ['a hovered row'], 4.5],
|
|
173
|
-
['the current row, label and
|
|
174
|
-
['a group label, a
|
|
322
|
+
['the current row, label, glyph and count', '--color-text-primary', null, ['the row the reader is on'], 4.5],
|
|
323
|
+
['a group label and its action, a count, a foot row', '--color-text-tertiary', null, ['a row', 'a hovered row'], 4.5],
|
|
324
|
+
[
|
|
325
|
+
"a project's key chip",
|
|
326
|
+
'--color-text-secondary',
|
|
327
|
+
'--color-surface-elevated',
|
|
328
|
+
['a row', 'a hovered row', 'the row the reader is on'],
|
|
329
|
+
4.5,
|
|
330
|
+
],
|
|
175
331
|
[
|
|
176
|
-
'the
|
|
177
|
-
'--color-
|
|
178
|
-
'--color-
|
|
332
|
+
'the badge, what is waiting on the reader',
|
|
333
|
+
'--color-text-on-accent',
|
|
334
|
+
'--color-brand-accent',
|
|
179
335
|
['a row', 'a hovered row', 'the row the reader is on'],
|
|
180
336
|
4.5,
|
|
181
337
|
],
|
|
@@ -189,13 +345,17 @@ export const RAIL_PAINTS = [
|
|
|
189
345
|
* The scrim is the one ground in the palette that is not a surface step: it is
|
|
190
346
|
* a wash over whatever is already there, so the pair a reader sees depends on
|
|
191
347
|
* what that was. An upload overlay puts a word on it over an identity badge,
|
|
192
|
-
* which is a seeded tint
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
348
|
+
* which is a seeded tint or the initials tile on whichever surface the form
|
|
349
|
+
* sits on, and the tints are not reachable from the surface list. Every ground
|
|
350
|
+
* here is measured because the shallowest one is what sets the number: a 60
|
|
351
|
+
* percent black over a white page is much lighter than the same wash over a
|
|
352
|
+
* dark tint.
|
|
353
|
+
*
|
|
354
|
+
* The accent's fill is not one of them. It was, while a badge could be filled
|
|
355
|
+
* with it; @crewlethq/ui's Avatar has no such tone any more (a SELECTED badge
|
|
356
|
+
* is a ring round a neutral one), so it would be a composite nothing draws.
|
|
197
357
|
*/
|
|
198
|
-
export const SCRIM_GROUNDS = [
|
|
358
|
+
export const SCRIM_GROUNDS = [...AVATAR_TINTS];
|
|
199
359
|
|
|
200
360
|
// The controls
|
|
201
361
|
// ---------------------------------------------------------------------------
|
|
@@ -204,7 +364,7 @@ export const SCRIM_GROUNDS = ['--color-brand-accent', ...AVATAR_TINTS];
|
|
|
204
364
|
* The segmented row, which is the one CONTROL that paints a tint on a tint.
|
|
205
365
|
*
|
|
206
366
|
* Its container is the inset well and the option that is on is lifted off it
|
|
207
|
-
* with the
|
|
367
|
+
* with the card surface, so the label of the chosen answer sits on a
|
|
208
368
|
* composite of two overlays rather than on a ground any other rule reaches.
|
|
209
369
|
* It is the label a reader looks for first in the row, so it is held to the
|
|
210
370
|
* primary step's own floor rather than to the 4.5 a fact needs.
|
|
@@ -215,12 +375,13 @@ export const SCRIM_GROUNDS = ['--color-brand-accent', ...AVATAR_TINTS];
|
|
|
215
375
|
*
|
|
216
376
|
* WHAT THIS RULE DOES NOT CLAIM, and the next reader should not assume: that
|
|
217
377
|
* the chip's own FACE is separable from the well it sits in. It is not, on
|
|
218
|
-
* every ground. The face is the
|
|
219
|
-
* overlay, and on the
|
|
220
|
-
* in
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
378
|
+
* every ground. The face is the card surface and the well is the inset
|
|
379
|
+
* overlay, and on the sheet those land dE 1.17 apart in dark (3.61 in light)
|
|
380
|
+
* and inside a card 2.89 (1.83 in light), against a floor of 3 elsewhere in
|
|
381
|
+
* this file; only on the raised rung are they 5.84 and 5.40 apart. So a
|
|
382
|
+
* segmented row is drawn on the product's own page exactly as it is at
|
|
383
|
+
* :8020, and which option is on is read there from the INK step, primary
|
|
384
|
+
* against tertiary, and from the chip's shadow. Both inks
|
|
224
385
|
* are measured, which is why this holds the on label to the primary floor
|
|
225
386
|
* rather than to the 4.5 a fact needs. A restyle that flattened the two ink
|
|
226
387
|
* steps together would take the last thing saying which option is on.
|
|
@@ -269,17 +430,30 @@ export const VEIL = '--color-surface-veil';
|
|
|
269
430
|
export const VEIL_BAND = [1.5, 3];
|
|
270
431
|
|
|
271
432
|
/**
|
|
272
|
-
* The
|
|
273
|
-
* free to choose.
|
|
274
|
-
*
|
|
275
|
-
* Its COLOUR is not free: a veil is the
|
|
276
|
-
* it is each root's own
|
|
277
|
-
*
|
|
278
|
-
* the
|
|
279
|
-
*
|
|
280
|
-
*
|
|
433
|
+
* The ground the veil is drawn in, and the alpha it is drawn at: the only
|
|
434
|
+
* number the veil is free to choose.
|
|
435
|
+
*
|
|
436
|
+
* Its COLOUR is not free: a veil is the application seen through less of
|
|
437
|
+
* itself, so it is each root's own FRAME at this alpha and never a hue of its
|
|
438
|
+
* own. The frame rather than the sheet because the veil covers the whole
|
|
439
|
+
* window, the rail as well as the sheet, and the frame is the ground both of
|
|
440
|
+
* them stand on: the application recedes into its own ground rather than
|
|
441
|
+
* under a film of its page. A veil that is a different colour from what is
|
|
442
|
+
* under it is a wash laid over it, which moves every ground behind it
|
|
443
|
+
* somewhere the ink steps were never measured against, and it does it in the
|
|
444
|
+
* one state where a reader cannot tell a rendering fault from a design.
|
|
445
|
+
*
|
|
446
|
+
* 0.65 is the approved dark scrim's 0.62 moved the least distance into the
|
|
447
|
+
* band for every root. At 0.62 the veiled dark page's own primary text still
|
|
448
|
+
* measured 3.02:1 to 3.15:1 over the four rungs, over the 3:1 ceiling, and the
|
|
449
|
+
* marketing root, near-white on black, needs 0.65 before its brightest
|
|
450
|
+
* composite comes under it. At 0.65 dark measures 2.73:1 to 2.84:1, light
|
|
451
|
+
* 2.18:1 to 2.30:1 and the marketing root 2.75:1 to 2.93:1. The approved light
|
|
452
|
+
* scrim, a dark wash at 0.28, left the light page at 8.35:1 to 9.64:1: a page
|
|
453
|
+
* nobody had faded at all.
|
|
281
454
|
*/
|
|
282
|
-
export const
|
|
455
|
+
export const VEIL_GROUND = '--color-surface-frame';
|
|
456
|
+
export const VEIL_ALPHA = 0.65;
|
|
283
457
|
|
|
284
458
|
// The data surfaces
|
|
285
459
|
// ---------------------------------------------------------------------------
|
|
@@ -354,24 +528,34 @@ export const SHADOW_STEPS = [
|
|
|
354
528
|
'--shadow-glow',
|
|
355
529
|
];
|
|
356
530
|
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
531
|
+
// Two cross-family rules, and both protect a RESERVED meaning from being
|
|
532
|
+
// claimed by another family: nothing may read as the accent, which is what to
|
|
533
|
+
// act on and what is chosen, and no series may read as the danger red. Their
|
|
534
|
+
// dichromat floors are lower than the within-family ones because the hue
|
|
535
|
+
// budget is finite (nine hues, the accent, four states and four series, in a
|
|
536
|
+
// space deuteranopia collapses to blue, yellow and lightness) and because
|
|
537
|
+
// across families colour is never the only signal: a Tag always renders its
|
|
538
|
+
// label, a status Callout always renders its glyph, a series is named by its
|
|
539
|
+
// legend, and the accent appears only as position. They are floors, not
|
|
362
540
|
// targets, and every measured value is reported so a later edit shows its cost.
|
|
363
|
-
|
|
364
|
-
|
|
541
|
+
//
|
|
542
|
+
// No floor here is about a CATEGORY. A phase, a unit or a model is drawn in the
|
|
543
|
+
// neutral colour with its word, and inside a figure as the series its legend
|
|
544
|
+
// names, so no category family spends this budget. The phase family did: three
|
|
545
|
+
// hues held apart from each other, from the four states and from the accent,
|
|
546
|
+
// which is three hues fewer for everything that carries a meaning.
|
|
365
547
|
const ACCENT_NORMAL = 10;
|
|
366
548
|
const ACCENT_DICHROMAT = 8;
|
|
367
549
|
// The reserved red keeps the highest normal floor of any cross-family pair,
|
|
368
|
-
// because it is the one hue that means the same thing everywhere in a product
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
550
|
+
// because it is the one hue that means the same thing everywhere in a product,
|
|
551
|
+
// and the accent's dichromat floor. Red, orange and green fall onto one axis
|
|
552
|
+
// for a dichromat, so this is the floor the chart's orange and aqua spend the
|
|
553
|
+
// most on: the approved aqua sat dE 0.73 from the approved red under
|
|
554
|
+
// protanopia, and the approved orange 9.08 from it under normal vision in dark
|
|
555
|
+
// and 7.45 in light. The fit moves the red further than the orange or the
|
|
556
|
+
// aqua, darker, because it had to darken for its white label anyway, and in
|
|
557
|
+
// dark it turns toward crimson as well, which spares the orange more than it
|
|
558
|
+
// costs the red.
|
|
375
559
|
const DANGER_NORMAL = 14;
|
|
376
560
|
const DANGER_DICHROMAT = 8;
|
|
377
561
|
|
|
@@ -379,29 +563,65 @@ const DANGER_DICHROMAT = 8;
|
|
|
379
563
|
// Theme states
|
|
380
564
|
// ---------------------------------------------------------------------------
|
|
381
565
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
566
|
+
// The theme layer is DARK FIRST: the dark palette is the bare :root, and light
|
|
567
|
+
// is painted over it by two blocks, one for a system that asks for light and
|
|
568
|
+
// one for an explicit choice. There is no dark attribute block, because the
|
|
569
|
+
// bare root already is one; the media block's :not() is what lets an explicit
|
|
570
|
+
// dark beat a light system.
|
|
571
|
+
const THEME_ROOT = ':root';
|
|
572
|
+
const LIGHT_MEDIA = '@media (prefers-color-scheme: light)';
|
|
573
|
+
const LIGHT_MEDIA_SELECTOR = ':root:not([data-theme="dark"])';
|
|
574
|
+
const LIGHT_ATTRIBUTE_SELECTOR = ':root[data-theme="light"]';
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The two sheets, each parsed ONCE, as sources cascade() reads. A measurement
|
|
578
|
+
* reads four states and three theme blocks out of them, and parsing both
|
|
579
|
+
* sheets over again for every read was most of what a runPalette call cost,
|
|
580
|
+
* which scripts/fit-palette.mjs makes tens of thousands of.
|
|
581
|
+
*/
|
|
582
|
+
const sheetsOf = ({ tokens, themes }) => ({
|
|
583
|
+
tokens: { name: 'tokens.css', blocks: parseBlocks(tokens) },
|
|
584
|
+
themes: { name: 'themes.css', blocks: parseBlocks(themes) },
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
/** The theme file's three blocks, each on its own, as the file declares it. */
|
|
588
|
+
function themeBlocks(themesSource) {
|
|
589
|
+
const block = (atRule, selector) => cascade([themesSource], (b) => b.atRule === atRule && b.selector === selector);
|
|
590
|
+
return {
|
|
591
|
+
dark: block(null, THEME_ROOT),
|
|
592
|
+
lightMedia: block(LIGHT_MEDIA, LIGHT_MEDIA_SELECTOR),
|
|
593
|
+
lightAttribute: block(null, LIGHT_ATTRIBUTE_SELECTOR),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
385
596
|
|
|
386
597
|
/**
|
|
387
598
|
* The four sets of values a browser can end up with.
|
|
388
599
|
*
|
|
389
600
|
* `base` is tokens.css on its own, which is what an application that imports
|
|
390
601
|
* only the token layer paints: the marketing palette. The other three are what
|
|
391
|
-
* the theme layer paints over it
|
|
602
|
+
* the theme layer paints over it: `dark`, the bare root, which is what a
|
|
603
|
+
* document that sets nothing gets and what an explicit dark gets on either
|
|
604
|
+
* system; and light twice over, once by the media query and once by the
|
|
605
|
+
* attribute, because they are two declarations a browser reaches by two
|
|
606
|
+
* different routes and either can drift from the other.
|
|
392
607
|
*/
|
|
393
|
-
export function paletteStates(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
608
|
+
export function paletteStates(sources) {
|
|
609
|
+
return statesOf(sheetsOf(sources));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** paletteStates over sheets already parsed, which runPalette shares with the structural rules. */
|
|
613
|
+
function statesOf(sheets) {
|
|
614
|
+
const tokensOnly = [sheets.tokens];
|
|
615
|
+
const both = [...tokensOnly, sheets.themes];
|
|
616
|
+
const bare = (block) => block.atRule === null && block.selector === THEME_ROOT;
|
|
617
|
+
const dark = cascade(both, bare);
|
|
618
|
+
const withLight = (selector, atRule) =>
|
|
619
|
+
new Map([...dark, ...cascade(both, (block) => block.atRule === atRule && block.selector === selector)]);
|
|
400
620
|
return {
|
|
401
621
|
base: cascade(tokensOnly, bare),
|
|
402
|
-
|
|
403
|
-
'
|
|
404
|
-
'
|
|
622
|
+
dark,
|
|
623
|
+
'light (media query)': withLight(LIGHT_MEDIA_SELECTOR, LIGHT_MEDIA),
|
|
624
|
+
'light (attribute)': withLight(LIGHT_ATTRIBUTE_SELECTOR, null),
|
|
405
625
|
};
|
|
406
626
|
}
|
|
407
627
|
|
|
@@ -433,9 +653,9 @@ function resolve(values, name, ground) {
|
|
|
433
653
|
/**
|
|
434
654
|
* Run every rule over one state's values.
|
|
435
655
|
* `profile` is 'full' for a theme, or 'base' for the marketing root, which is
|
|
436
|
-
* exempt from the separation,
|
|
437
|
-
* its single brand hue are a deliberate marketing look rather than
|
|
438
|
-
* palette.
|
|
656
|
+
* exempt from the separation, rail, overlay and rung rules: its one pure black
|
|
657
|
+
* ground and its single brand hue are a deliberate marketing look rather than
|
|
658
|
+
* a product palette.
|
|
439
659
|
*/
|
|
440
660
|
function checkState(state, values, profile, push) {
|
|
441
661
|
const { ground, opaque, all } = surfacesOf(values);
|
|
@@ -456,9 +676,14 @@ function checkState(state, values, profile, push) {
|
|
|
456
676
|
// It exists for a hairline glyph and a disabled affordance. The assertion
|
|
457
677
|
// is that it stays BELOW the fact floor: a step that quietly crept up to
|
|
458
678
|
// 4.5 would invite itself into a table cell, which is the whole failure
|
|
459
|
-
// the separate name prevents.
|
|
460
|
-
|
|
461
|
-
|
|
679
|
+
// the separate name prevents. And that it stays ABOVE the band's floor on
|
|
680
|
+
// the three grounds a glyph beside its word is drawn on: the frame (the
|
|
681
|
+
// rail and the drawer), the sheet and a card. Raised is not one of them:
|
|
682
|
+
// it is a chip's or a well's fill, and what a chip carries is its label.
|
|
683
|
+
for (const [name, surface] of opaque.filter(([rung]) => rung !== '--color-surface-elevated')) {
|
|
684
|
+
const ratio = contrast(colour('--color-text-muted'), surface);
|
|
685
|
+
say('text-muted is decoration', ratio > 2.8 && ratio < 4.5, '--color-text-muted', ratio, `2.8 < ${ratio.toFixed(2)}:1 < 4.5 on ${name}`);
|
|
686
|
+
}
|
|
462
687
|
}
|
|
463
688
|
|
|
464
689
|
for (const name of INK_STEPS) {
|
|
@@ -469,7 +694,7 @@ function checkState(state, values, profile, push) {
|
|
|
469
694
|
// An ink on its OWN soft tint: a badge, a callout, a diff line. The tint is
|
|
470
695
|
// recomputed from the fill here rather than read from the token, so a
|
|
471
696
|
// hand-edited tint that no longer belongs to its fill fails.
|
|
472
|
-
for (const fill of
|
|
697
|
+
for (const fill of STATUS) {
|
|
473
698
|
const { r, g, b } = colour(fill);
|
|
474
699
|
const tints = opaque.map(([name, rgb]) => [
|
|
475
700
|
`${fill} soft on ${name}`,
|
|
@@ -499,12 +724,28 @@ function checkState(state, values, profile, push) {
|
|
|
499
724
|
}
|
|
500
725
|
|
|
501
726
|
for (const [label, fill] of LABEL_ON_FILL) {
|
|
502
|
-
//
|
|
503
|
-
//
|
|
727
|
+
// Measured over every ground the fill can sit on, so a translucent fill
|
|
728
|
+
// is measured as the composite a reader sees. None is today; the
|
|
729
|
+
// monochrome primary this list used to carry had two.
|
|
504
730
|
const low = worst(colour(label), opaque.map(([name, rgb]) => [`${fill} on ${name}`, flatten(values.get(fill), rgb)]));
|
|
505
731
|
say('a label clears 4.5:1 on its own fill', low.ratio >= 4.5, `${label} on ${fill}`, low.ratio, `${low.ratio.toFixed(2)}:1 (worst on ${low.name})`);
|
|
506
732
|
}
|
|
507
733
|
|
|
734
|
+
// THE PRIMARY ACTION'S STEPS. See ACTION_STEPS.
|
|
735
|
+
for (const [rule, step, from] of ACTION_STEPS) {
|
|
736
|
+
const label = colour(ACTION_LABEL);
|
|
737
|
+
const [was, now] = [colour(from), colour(step)];
|
|
738
|
+
const measured = deltaE(now, was);
|
|
739
|
+
const gain = contrast(label, now) - contrast(label, was);
|
|
740
|
+
say(
|
|
741
|
+
rule,
|
|
742
|
+
measured >= ACTION_DE && gain > 0,
|
|
743
|
+
`${step} after ${from}`,
|
|
744
|
+
measured,
|
|
745
|
+
`dE ${measured.toFixed(2)} >= ${ACTION_DE}, and ${ACTION_LABEL} ${contrast(label, was).toFixed(2)}:1 -> ${contrast(label, now).toFixed(2)}:1 rises`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
508
749
|
for (const name of AVATAR_TINTS) {
|
|
509
750
|
const ratio = contrast(parseHex('#ffffff'), colour(name));
|
|
510
751
|
say('an avatar tint clears 4.5:1 under white initials', ratio >= 4.5, name, ratio, `${ratio.toFixed(2)}:1`);
|
|
@@ -514,8 +755,8 @@ function checkState(state, values, profile, push) {
|
|
|
514
755
|
// A word on the scrim. The scrim is a wash rather than a surface, so the
|
|
515
756
|
// composite is taken over every ground it can be drawn on rather than read
|
|
516
757
|
// from a token: an upload overlay's label sits on the scrim over an
|
|
517
|
-
// identity badge, and the badge is a seeded tint
|
|
518
|
-
//
|
|
758
|
+
// identity badge, and the badge is a seeded tint or an initials tile on
|
|
759
|
+
// whichever surface the form is on.
|
|
519
760
|
const label = colour('--color-text-on-accent');
|
|
520
761
|
const scrim = values.get('--color-surface-scrim');
|
|
521
762
|
const over = [...opaque, ...SCRIM_GROUNDS.map((name) => [name, colour(name)])];
|
|
@@ -572,13 +813,13 @@ function checkState(state, values, profile, push) {
|
|
|
572
813
|
// THE VEIL, on every ground it can be drawn over. See VEIL.
|
|
573
814
|
const [floor, ceiling] = VEIL_BAND;
|
|
574
815
|
const veil = values.get(VEIL);
|
|
575
|
-
const own = withAlpha(values.get(
|
|
816
|
+
const own = withAlpha(values.get(VEIL_GROUND), VEIL_ALPHA);
|
|
576
817
|
say(
|
|
577
|
-
|
|
818
|
+
"the veil is this root's frame at an alpha, not a colour of its own",
|
|
578
819
|
veil === own,
|
|
579
820
|
VEIL,
|
|
580
821
|
VEIL_ALPHA,
|
|
581
|
-
`"${veil}" is the root's own
|
|
822
|
+
`"${veil}" is the root's own frame at ${VEIL_ALPHA} ("${own}")`,
|
|
582
823
|
);
|
|
583
824
|
const ink = flatten(veil, colour('--color-text-primary'));
|
|
584
825
|
for (const [name, rgb] of opaque) {
|
|
@@ -650,58 +891,53 @@ function checkState(state, values, profile, push) {
|
|
|
650
891
|
);
|
|
651
892
|
}
|
|
652
893
|
}
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
894
|
+
// THE ROW THE READER IS ON, found as a card is. See RAIL_CURRENT_ROW.
|
|
895
|
+
const [fill, line] = RAIL_CURRENT_ROW;
|
|
896
|
+
const current = rows.get('the row the reader is on');
|
|
897
|
+
const lift = deltaE(current, rail);
|
|
898
|
+
say(
|
|
899
|
+
"the rail's current row lifts off the rail",
|
|
900
|
+
lift >= RAIL_CURRENT_LIFT,
|
|
901
|
+
`${fill} on ${RAIL_GROUND}`,
|
|
902
|
+
lift,
|
|
903
|
+
`dE ${lift.toFixed(2)} >= ${RAIL_CURRENT_LIFT}`,
|
|
904
|
+
);
|
|
905
|
+
const drawn = deltaE(flatten(values.get(line), current), current);
|
|
660
906
|
say(
|
|
661
|
-
"the rail's
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
`
|
|
907
|
+
"the hairline round the rail's current row is visible on it",
|
|
908
|
+
drawn >= HAIRLINE_DE,
|
|
909
|
+
`${line} on ${fill}`,
|
|
910
|
+
drawn,
|
|
911
|
+
`dE ${drawn.toFixed(2)} >= ${HAIRLINE_DE}`,
|
|
666
912
|
);
|
|
667
913
|
}
|
|
668
914
|
|
|
669
|
-
for (const [set, floor, adjacent, rule] of [
|
|
670
|
-
[
|
|
671
|
-
[
|
|
672
|
-
[DATA, 9, true, 'adjacent data hues stay separable'],
|
|
915
|
+
for (const [set, floor, normal, adjacent, rule] of [
|
|
916
|
+
[STATUS, 10, 10, false, 'status hues stay separable'],
|
|
917
|
+
[DATA, DATA_ADJACENT_DE, DATA_ADJACENT_NORMAL_DE, true, 'adjacent data hues stay separable'],
|
|
673
918
|
]) {
|
|
674
919
|
// ADJACENT, not every pair, for the data ramp: a legend reader
|
|
675
920
|
// distinguishes series 2 from series 3 because they sit next to each
|
|
676
|
-
// other, and demanding every pair
|
|
677
|
-
//
|
|
921
|
+
// other, and demanding every pair be far apart is what forces a palette
|
|
922
|
+
// to spread until it is ugly. Its second floor is the normal-vision one;
|
|
923
|
+
// see DATA_ADJACENT_NORMAL_DE.
|
|
678
924
|
const pairs = adjacent
|
|
679
925
|
? set.slice(0, -1).map((name, i) => [name, set[i + 1]])
|
|
680
926
|
: set.flatMap((name, i) => set.slice(i + 1).map((other) => [name, other]));
|
|
927
|
+
const floors = normal > floor ? `${normal} normal, ${floor} every vision` : `${floor}`;
|
|
681
928
|
for (const [a, b] of pairs) {
|
|
682
929
|
const measured = separation(colour(a), colour(b));
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// A phase tag must not be read as a status badge beside it. Measured under
|
|
688
|
-
// every vision: the first draft of this palette put an onboarding tag dE 3.1
|
|
689
|
-
// from an info badge for a deuteranopic reader and passed under normal
|
|
690
|
-
// vision alone.
|
|
691
|
-
for (const phase of PHASE) {
|
|
692
|
-
for (const status of STATUS) {
|
|
693
|
-
const measured = separation(colour(phase), colour(status));
|
|
694
|
-
const ok = measured[0] >= CROSS_NORMAL && Math.min(measured[1], measured[2]) >= CROSS_DICHROMAT;
|
|
695
|
-
say('a phase hue clears the status family', ok, `${phase} vs ${status}`, Math.min(...measured), `dE ${describe(measured)} >= ${CROSS_NORMAL} normal, ${CROSS_DICHROMAT} dichromat`);
|
|
930
|
+
const ok = Math.min(...measured) >= floor && measured[0] >= normal;
|
|
931
|
+
say(rule, ok, `${a} vs ${b}`, Math.min(...measured), `dE ${describe(measured)} >= ${floors}`);
|
|
696
932
|
}
|
|
697
933
|
}
|
|
698
934
|
|
|
699
935
|
// Red means "this broke" everywhere in a product. A chart series that
|
|
700
936
|
// happens to be red says so too, to a reader who is scanning for it, and
|
|
701
937
|
// THAT READER IS OFTEN THE DICHROMAT ONE: red and green collapse onto one
|
|
702
|
-
// axis for them, so a chart's
|
|
703
|
-
// whole palette to be read as failure. Measured under normal vision
|
|
704
|
-
// this rule passed at dE 24.0 while
|
|
938
|
+
// axis for them, so a chart's aqua or orange series is the likeliest thing
|
|
939
|
+
// in the whole palette to be read as failure. Measured under normal vision
|
|
940
|
+
// alone this rule once passed at dE 24.0 while a green fourth series sat 6.0
|
|
705
941
|
// from the danger red under deuteranopia.
|
|
706
942
|
for (const name of DATA) {
|
|
707
943
|
const measured = separation(colour(name), colour('--color-feedback-danger'));
|
|
@@ -709,11 +945,13 @@ function checkState(state, values, profile, push) {
|
|
|
709
945
|
say('no data hue collides with danger', ok, name, Math.min(...measured), `dE ${describe(measured)} >= ${DANGER_NORMAL} normal, ${DANGER_DICHROMAT} dichromat`);
|
|
710
946
|
}
|
|
711
947
|
|
|
712
|
-
// "The accent is the most saturated hue"
|
|
713
|
-
// danger, both outer phase hues and two data hues out-
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
|
|
948
|
+
// "The accent is the most saturated hue" was a rule once, and was dropped
|
|
949
|
+
// when danger, both outer phase hues and two data hues out-saturated the
|
|
950
|
+
// indigo it was. It happens to hold of the violet (chroma 23.7 dark, 24.0
|
|
951
|
+
// light, above every state and series), but a ranking is still not what
|
|
952
|
+
// protects the selection: nothing else may READ as it, and that is a
|
|
953
|
+
// distance, measured here under every vision.
|
|
954
|
+
for (const name of [...STATUS, ...DATA]) {
|
|
717
955
|
const measured = separation(colour(name), colour('--color-brand-accent'));
|
|
718
956
|
const ok = measured[0] >= ACCENT_NORMAL && Math.min(measured[1], measured[2]) >= ACCENT_DICHROMAT;
|
|
719
957
|
say('no hue is read as the accent', ok, name, Math.min(...measured), `dE ${describe(measured)} >= ${ACCENT_NORMAL} normal, ${ACCENT_DICHROMAT} dichromat`);
|
|
@@ -729,7 +967,7 @@ function checkState(state, values, profile, push) {
|
|
|
729
967
|
const fromGround = deltaE(selected, rgb);
|
|
730
968
|
const fromHover = deltaE(selected, hovered);
|
|
731
969
|
const ok = fromGround >= deltaE(hovered, rgb) && fromHover >= 3;
|
|
732
|
-
say('the selected tint outreads the hover overlay', ok, name
|
|
970
|
+
say('the selected tint outreads the hover overlay', ok, `--color-brand-accent-soft on ${name}`, Math.min(fromGround - deltaE(hovered, rgb), fromHover), `dE ${fromGround.toFixed(2)} from the ground (hover ${deltaE(hovered, rgb).toFixed(2)}), dE ${fromHover.toFixed(2)} apart`);
|
|
733
971
|
}
|
|
734
972
|
|
|
735
973
|
{
|
|
@@ -740,13 +978,24 @@ function checkState(state, values, profile, push) {
|
|
|
740
978
|
say('a control boundary clears 3:1', low.ratio >= 3, '--color-border-control', low.ratio, `${low.ratio.toFixed(2)}:1 (worst on ${low.name})`);
|
|
741
979
|
}
|
|
742
980
|
|
|
743
|
-
// Glass
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
|
|
981
|
+
// Glass is the ground of what floats OVER CONTENT rather than over
|
|
982
|
+
// interface: a canvas's bars and notes, and a chart's reading, which stands
|
|
983
|
+
// beside the mark it reads and so over its neighbours. Each carries words,
|
|
984
|
+
// so every text step is held to 4.5:1 on the composite. The declared worst
|
|
985
|
+
// backdrop is the INVERSE ground, the far end of the palette from the glass
|
|
986
|
+
// itself, which is as far as anything under a floating panel can pull the
|
|
987
|
+
// composite; the accent fill, the primary action on a canvas, and every data
|
|
988
|
+
// hue a chart paints (the residual included) are measured by name beside it,
|
|
989
|
+
// so the reading over a column is a measured case rather than an inference.
|
|
990
|
+
const backdrops = [
|
|
991
|
+
['the inverse ground', colour('--color-surface-inverse')],
|
|
992
|
+
['the accent fill', colour('--color-brand-accent')],
|
|
993
|
+
...[...DATA, '--color-data-other'].map((name) => [name, colour(name)]),
|
|
994
|
+
...opaque,
|
|
995
|
+
];
|
|
747
996
|
for (const [name, backdrop] of backdrops) {
|
|
748
997
|
const glass = flatten(values.get('--color-surface-glass'), backdrop);
|
|
749
|
-
for (const step of ['--color-text-secondary', '--color-text-tertiary']) {
|
|
998
|
+
for (const step of ['--color-text-primary', '--color-text-secondary', '--color-text-tertiary']) {
|
|
750
999
|
const ratio = contrast(colour(step), glass);
|
|
751
1000
|
say('text on glass clears 4.5:1', ratio >= 4.5, `${step} over ${name}`, ratio, `${ratio.toFixed(2)}:1`);
|
|
752
1001
|
}
|
|
@@ -759,12 +1008,39 @@ function checkState(state, values, profile, push) {
|
|
|
759
1008
|
say('the neutral ramp stays neutral', measured <= 2.2, name, measured, `chroma ${measured.toFixed(2)} <= 2.2`);
|
|
760
1009
|
}
|
|
761
1010
|
|
|
1011
|
+
// THE OVERLAY STEPS. See OVERLAY_STEPS.
|
|
1012
|
+
for (const [rule, overlay, beneath] of OVERLAY_STEPS) {
|
|
1013
|
+
for (const [name, rgb] of opaque) {
|
|
1014
|
+
const from = beneath === null ? rgb : flatten(values.get(beneath), rgb);
|
|
1015
|
+
const measured = deltaE(flatten(values.get(overlay), rgb), from);
|
|
1016
|
+
say(
|
|
1017
|
+
rule,
|
|
1018
|
+
measured >= OVERLAY_DE,
|
|
1019
|
+
`${overlay} on ${name}`,
|
|
1020
|
+
measured,
|
|
1021
|
+
`dE ${measured.toFixed(2)} >= ${OVERLAY_DE} from ${beneath === null ? 'the rung' : `${beneath} on it`}`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// THE LADDER. See RUNG_STEPS.
|
|
1027
|
+
for (const [rule, upper, lower, floor] of RUNG_STEPS) {
|
|
1028
|
+
const measured = deltaE(colour(upper), colour(lower));
|
|
1029
|
+
say(rule, measured >= floor, `${upper} over ${lower}`, measured, `dE ${measured.toFixed(2)} >= ${floor}`);
|
|
1030
|
+
}
|
|
762
1031
|
{
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1032
|
+
// The other half of the card's step. See CARD_HAIRLINE.
|
|
1033
|
+
const [line, card, sheet] = CARD_HAIRLINE;
|
|
1034
|
+
for (const side of [card, sheet]) {
|
|
1035
|
+
const measured = deltaE(colour(line), colour(side));
|
|
1036
|
+
say(
|
|
1037
|
+
'the hairline that finds a card is visible on both sides of it',
|
|
1038
|
+
measured >= HAIRLINE_DE,
|
|
1039
|
+
`${line} on ${side}`,
|
|
1040
|
+
measured,
|
|
1041
|
+
`dE ${measured.toFixed(2)} >= ${HAIRLINE_DE}`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
768
1044
|
}
|
|
769
1045
|
}
|
|
770
1046
|
|
|
@@ -777,17 +1053,22 @@ const describe = (measured) => measured.map((value, i) => `${VISIONS[i].slice(0,
|
|
|
777
1053
|
function checkStructure(sources, states, push) {
|
|
778
1054
|
const say = (rule, ok, subject, detail) => push({ state: 'the token files', rule, ok, subject, value: null, detail });
|
|
779
1055
|
|
|
1056
|
+
const { dark: darkBlock, lightMedia, lightAttribute: lightBlock } = themeBlocks(sources.themesSource);
|
|
1057
|
+
|
|
780
1058
|
// A colour whose only definition is inside the theme layer is a colour that
|
|
781
|
-
// is missing for an application that imports the token layer alone.
|
|
782
|
-
|
|
1059
|
+
// is missing for an application that imports the token layer alone. Read
|
|
1060
|
+
// off the three blocks themselves, so a slot only one of them declares is
|
|
1061
|
+
// still asked about.
|
|
1062
|
+
const themed = [...new Set([...darkBlock.keys(), ...lightMedia.keys(), ...lightBlock.keys()])].filter(
|
|
1063
|
+
(name) => name.startsWith('--color-') || name.startsWith('--shadow-'),
|
|
1064
|
+
);
|
|
783
1065
|
const missing = themed.filter((name) => !states.base.has(name));
|
|
784
1066
|
say('every themed token has a value on the bare :root of tokens.css', missing.length === 0, 'tokens.css', missing.length === 0 ? 'none missing' : `missing: ${missing.join(', ')}`);
|
|
785
1067
|
|
|
786
1068
|
// The measured rules cannot catch a missing LIGHT slot: the token still has
|
|
787
|
-
// a value, inherited from
|
|
788
|
-
//
|
|
789
|
-
|
|
790
|
-
const darkBlock = cascade([sources.themesSource], (block) => block.atRule === null && block.selector === DARK_ATTRIBUTE_SELECTOR);
|
|
1069
|
+
// a value, inherited from the dark root under it, and that value is the dark
|
|
1070
|
+
// one. Nor a missing DARK slot, which inherits the marketing palette's from
|
|
1071
|
+
// tokens.css. So parity is asserted directly.
|
|
791
1072
|
const onlyLight = [...lightBlock.keys()].filter((name) => !darkBlock.has(name));
|
|
792
1073
|
const onlyDark = [...darkBlock.keys()].filter((name) => !lightBlock.has(name));
|
|
793
1074
|
say(
|
|
@@ -797,14 +1078,17 @@ function checkStructure(sources, states, push) {
|
|
|
797
1078
|
onlyLight.length === 0 && onlyDark.length === 0 ? 'the two key sets match' : `light only: ${onlyLight.join(', ') || 'none'}; dark only: ${onlyDark.join(', ') || 'none'}`,
|
|
798
1079
|
);
|
|
799
1080
|
|
|
800
|
-
// The
|
|
1081
|
+
// The light palette is written twice on purpose, once so the system setting
|
|
801
1082
|
// works and once so an explicit choice wins in both directions. A value that
|
|
802
|
-
// drifts between them means the toggle
|
|
803
|
-
// does not
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1083
|
+
// drifts between them means the toggle paints colours the system setting
|
|
1084
|
+
// does not, and a slot one of them lacks is painted DARK in that one alone,
|
|
1085
|
+
// so the comparison runs from both sides: a slot missing from either block
|
|
1086
|
+
// is drift, not agreement.
|
|
1087
|
+
say('the light media block is present', lightMedia.size > 0, 'themes.css', `${lightMedia.size} declarations`);
|
|
1088
|
+
const drift = [...new Set([...lightMedia.keys(), ...lightBlock.keys()])]
|
|
1089
|
+
.filter((name) => lightMedia.get(name) !== lightBlock.get(name))
|
|
1090
|
+
.map((name) => `${name}: ${lightMedia.get(name) ?? 'undeclared'} by media query vs ${lightBlock.get(name) ?? 'undeclared'} by attribute`);
|
|
1091
|
+
say('the two light blocks agree', drift.length === 0, 'themes.css', drift.length === 0 ? 'no drift' : drift.join('; '));
|
|
808
1092
|
|
|
809
1093
|
// A dark-tuned shadow on a white page is a smudge, so every step is declared
|
|
810
1094
|
// in both blocks rather than inherited from the token layer, AND the two
|
|
@@ -841,14 +1125,15 @@ function checkStructure(sources, states, push) {
|
|
|
841
1125
|
* Measure the whole rule table over one built pair of stylesheets.
|
|
842
1126
|
* Returns every check with its measured value, and the failing subset.
|
|
843
1127
|
*/
|
|
844
|
-
export function runPalette(
|
|
845
|
-
const
|
|
1128
|
+
export function runPalette(sources) {
|
|
1129
|
+
const sheets = sheetsOf(sources);
|
|
1130
|
+
const states = statesOf(sheets);
|
|
846
1131
|
const checks = [];
|
|
847
1132
|
const push = (check) => checks.push(check);
|
|
848
1133
|
for (const [name, values] of Object.entries(states)) {
|
|
849
1134
|
checkState(name, values, name === 'base' ? 'base' : 'full', push);
|
|
850
1135
|
}
|
|
851
|
-
checkStructure({ themesSource:
|
|
1136
|
+
checkStructure({ themesSource: sheets.themes }, states, push);
|
|
852
1137
|
return { states, checks, failures: checks.filter((check) => !check.ok) };
|
|
853
1138
|
}
|
|
854
1139
|
|