@crewlethq/tokens 0.5.7 → 0.6.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/README.md +82 -68
- package/dist/css/themes.css +170 -170
- package/dist/css/tokens.css +7 -7
- package/dist/themes.d.ts +109 -109
- package/dist/themes.js +109 -109
- package/package.json +1 -1
- package/test/palette.d.mts +6 -0
- package/test/palette.mjs +70 -37
- package/test/palette.test.mjs +69 -41
package/test/palette.mjs
CHANGED
|
@@ -74,7 +74,7 @@ export const OPAQUE_SURFACES = [
|
|
|
74
74
|
* - THE SHEET OVER THE FRAME is the whole layering device: a content sheet
|
|
75
75
|
* floating inside the application ground. At dE 3, the floor this file holds
|
|
76
76
|
* every "can a reader notice it" difference to, the sheet reads as a
|
|
77
|
-
* separate plane; the approved dark sheet, #
|
|
77
|
+
* separate plane; the approved dark sheet, #0c0e15, measured 2.96 and was
|
|
78
78
|
* nudged to clear it.
|
|
79
79
|
* - A CARD OVER THE SHEET is deliberately near-flat, dE 1.5, because a card is
|
|
80
80
|
* found by its HAIRLINE rather than by its fill: a page of cards each lifted
|
|
@@ -105,16 +105,28 @@ export const RUNG_STEPS = [
|
|
|
105
105
|
export const CARD_HAIRLINE = ['--color-border-default', '--color-surface-subtle', '--color-surface-background'];
|
|
106
106
|
export const HAIRLINE_DE = 3;
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A chip drawn on the raised rung inside the strong hairline, as [fill,
|
|
110
|
+
* edge]: the neutral pill, and a key cap. Its fill IS raised, so on a raised
|
|
111
|
+
* ground (a lead block, a raised card, the rail's current row) the fill alone
|
|
112
|
+
* draws nothing, and what finds the chip is its edge. So the chip's boundary,
|
|
113
|
+
* the fill or the edge, whichever stands further off its ground, is held to
|
|
114
|
+
* HAIRLINE_DE on every ground it can land on: each opaque rung, and each
|
|
115
|
+
* overlay a hovered, pressed or inset row composites onto one. A component
|
|
116
|
+
* suite holds the chip to drawing this pair; this is what keeps a palette
|
|
117
|
+
* from taking the edge away.
|
|
118
|
+
*/
|
|
119
|
+
export const RAISED_CHIP = ['--color-surface-elevated', '--color-border-strong'];
|
|
120
|
+
|
|
108
121
|
/**
|
|
109
122
|
* THE OVERLAY STEPS HAVE TO BE SEEN, on every rung.
|
|
110
123
|
*
|
|
111
|
-
* A translucent overlay is one alpha on four grounds, and
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* 0.60 off the frame. This is the floor that stops it. A hovered row separates
|
|
124
|
+
* A translucent overlay is one alpha on four grounds, and a stronger overlay
|
|
125
|
+
* costs the tertiary text step contrast on the rungs it lightens or darkens
|
|
126
|
+
* most. Left to that trade alone, the cheapest answer for the text is an
|
|
127
|
+
* overlay nobody sees, and a light frame keeps asking for it: an overlay that
|
|
128
|
+
* clears the sheet and the card can still sit under dE 3 on the frame. This
|
|
129
|
+
* is the floor that stops it. A hovered row separates
|
|
118
130
|
* from its rung by dE 3, and a pressed row from a hovered one by dE 3 more,
|
|
119
131
|
* because dE 3 is the floor this file holds every "can a reader notice it"
|
|
120
132
|
* difference to. The marketing root is exempt, as it is from the rungs.
|
|
@@ -169,11 +181,11 @@ export const STATUS = [
|
|
|
169
181
|
];
|
|
170
182
|
|
|
171
183
|
/**
|
|
172
|
-
* The chart series, in the order a figure assigns them: blue, orange,
|
|
173
|
-
*
|
|
184
|
+
* The chart series, in the order a figure assigns them: blue, orange, green
|
|
185
|
+
* and yellow, the four the approved palette draws.
|
|
174
186
|
*
|
|
175
|
-
* FOUR, because four is what both floors below can hold at once. The
|
|
176
|
-
* four clear them with room (
|
|
187
|
+
* FOUR, because four is what both floors below can hold at once. The shipped
|
|
188
|
+
* four clear them with room (their tightest neighbours sit dE 20.6 apart under
|
|
177
189
|
* normal vision), and a fifth would have to sit between two of them in a hue
|
|
178
190
|
* budget the reserved red, the accent and a dichromat's collapse of red onto
|
|
179
191
|
* green have already spent.
|
|
@@ -219,13 +231,13 @@ export const LABEL_ON_FILL = [
|
|
|
219
231
|
* the fill is the whole of the feedback, so every step clears two things. It
|
|
220
232
|
* is SEEN: ACTION_DE, the floor this file holds every "can a reader notice
|
|
221
233
|
* 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.
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* only its direction
|
|
234
|
+
* on every fill: a hover or a press only ever gains contrast. A hover drawn as
|
|
235
|
+
* brightness(1.08) is the step TOWARD a white label: on either accent it
|
|
236
|
+
* takes white under the text floor (4.25:1 in dark, 4.19:1 in light) at the
|
|
237
|
+
* moment a reader is about to press, which the label rule above catches. This
|
|
238
|
+
* rule is what catches a step toward the label that still clears it: the
|
|
239
|
+
* accent dulled at its own lightness reads as a step and keeps its label over
|
|
240
|
+
* 4.5:1, and only its direction is wrong.
|
|
229
241
|
*
|
|
230
242
|
* The DESTRUCTIVE action is the other filled action, and it is held the same
|
|
231
243
|
* way: a toast's destructive action is the danger fill under the same white
|
|
@@ -284,12 +296,12 @@ export const RAIL_GROUND = '--color-surface-frame';
|
|
|
284
296
|
* border drawn round it INSIDE the row, and the primary ink.
|
|
285
297
|
*
|
|
286
298
|
* 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.
|
|
299
|
+
* the frame, RAIL_CURRENT_LIFT, because in light raised is only dE 1.93 off
|
|
288
300
|
* the frame (the design draws it there); what carries the row is the
|
|
289
301
|
* 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:
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* is also what tells the current row from a HOVERED one, which draws none: a
|
|
303
|
+
* hover overlay strong enough to be seen on the frame can lift it close to
|
|
304
|
+
* raised, and nothing else keeps the two apart.
|
|
293
305
|
*
|
|
294
306
|
* It used to be the accent's own soft tint with the label in the accent ink.
|
|
295
307
|
* The accent is the primary action's fill now, so a violet row in the rail
|
|
@@ -443,14 +455,13 @@ export const VEIL_BAND = [1.5, 3];
|
|
|
443
455
|
* somewhere the ink steps were never measured against, and it does it in the
|
|
444
456
|
* one state where a reader cannot tell a rendering fault from a design.
|
|
445
457
|
*
|
|
446
|
-
* 0.65 is the
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* nobody had faded at all.
|
|
458
|
+
* 0.65 is the least alpha that brings every root into the band: the marketing
|
|
459
|
+
* root, near-white on black, needs it before its brightest composite comes
|
|
460
|
+
* under the 3:1 ceiling, and the approved palette draws its veil at it in both
|
|
461
|
+
* themes. At 0.65 dark measures 2.56:1 to 2.76:1, light 2.18:1 to 2.29:1 and
|
|
462
|
+
* the marketing root 2.75:1 to 2.93:1. A light veil drawn as a dark wash
|
|
463
|
+
* instead leaves the light page's text readable through it: a page nobody has
|
|
464
|
+
* faded at all.
|
|
454
465
|
*/
|
|
455
466
|
export const VEIL_GROUND = '--color-surface-frame';
|
|
456
467
|
export const VEIL_ALPHA = 0.65;
|
|
@@ -550,12 +561,11 @@ const ACCENT_DICHROMAT = 8;
|
|
|
550
561
|
// because it is the one hue that means the same thing everywhere in a product,
|
|
551
562
|
// and the accent's dichromat floor. Red, orange and green fall onto one axis
|
|
552
563
|
// for a dichromat, so this is the floor the chart's orange and aqua spend the
|
|
553
|
-
// most on: the approved
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
// costs the red.
|
|
564
|
+
// most on: the approved green sat dE 6.79 from the approved red under
|
|
565
|
+
// deuteranopia in dark and 2.30 in light, and the approved orange 11.39 from
|
|
566
|
+
// it under normal vision in dark. The fit moves the red darker, because it had
|
|
567
|
+
// to darken for its white label anyway, and turns the green toward aqua, which
|
|
568
|
+
// takes it off the axis the red falls on.
|
|
559
569
|
const DANGER_NORMAL = 14;
|
|
560
570
|
const DANGER_DICHROMAT = 8;
|
|
561
571
|
|
|
@@ -1042,6 +1052,29 @@ function checkState(state, values, profile, push) {
|
|
|
1042
1052
|
);
|
|
1043
1053
|
}
|
|
1044
1054
|
}
|
|
1055
|
+
{
|
|
1056
|
+
// A chip on every ground it can land on. See RAISED_CHIP.
|
|
1057
|
+
const [fill, edge] = RAISED_CHIP;
|
|
1058
|
+
const grounds = [
|
|
1059
|
+
...opaque,
|
|
1060
|
+
...OVERLAYS.flatMap((overlay) => opaque.map(([name, rgb]) => [`${overlay} on ${name}`, flatten(values.get(overlay), rgb)])),
|
|
1061
|
+
];
|
|
1062
|
+
const low = grounds.reduce(
|
|
1063
|
+
(least, [name, ground]) => {
|
|
1064
|
+
const filled = flatten(values.get(fill), ground);
|
|
1065
|
+
const drawn = Math.max(deltaE(filled, ground), deltaE(flatten(values.get(edge), filled), ground));
|
|
1066
|
+
return drawn < least.drawn ? { drawn, name } : least;
|
|
1067
|
+
},
|
|
1068
|
+
{ drawn: Infinity, name: '' },
|
|
1069
|
+
);
|
|
1070
|
+
say(
|
|
1071
|
+
'the strong hairline finds a raised chip on every ground',
|
|
1072
|
+
low.drawn >= HAIRLINE_DE,
|
|
1073
|
+
`${edge} round ${fill}`,
|
|
1074
|
+
low.drawn,
|
|
1075
|
+
`dE ${low.drawn.toFixed(2)} >= ${HAIRLINE_DE} (worst on ${low.name})`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1045
1078
|
}
|
|
1046
1079
|
|
|
1047
1080
|
const describe = (measured) => measured.map((value, i) => `${VISIONS[i].slice(0, 1)}${value.toFixed(1)}`).join('/');
|
package/test/palette.test.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
paletteStates,
|
|
31
31
|
parseHex,
|
|
32
32
|
RAIL_CURRENT_ROW,
|
|
33
|
+
RAISED_CHIP,
|
|
33
34
|
runPalette,
|
|
34
35
|
RUNG_STEPS,
|
|
35
36
|
tightest,
|
|
@@ -257,6 +258,18 @@ describe('the four rungs', () => {
|
|
|
257
258
|
assert.deepEqual(darkFailures(line, dark.surface.subtle), ['the hairline that finds a card is visible on both sides of it']);
|
|
258
259
|
});
|
|
259
260
|
|
|
261
|
+
test('a raised chip is measured in every theme state, and one whose edge is its own fill is caught', () => {
|
|
262
|
+
const CHIP_RULE = 'the strong hairline finds a raised chip on every ground';
|
|
263
|
+
const [fill, edge] = RAISED_CHIP;
|
|
264
|
+
assert.deepEqual([fill, edge], ['--color-surface-elevated', '--color-border-strong']);
|
|
265
|
+
for (const state of THEME_STATES) {
|
|
266
|
+
assert.equal(checks.filter((check) => check.state === state && check.rule === CHIP_RULE).length, 1, state);
|
|
267
|
+
}
|
|
268
|
+
// The edge drawn in the chip's own fill: on a raised ground the chip is
|
|
269
|
+
// loose words, and nothing else the palette measures notices.
|
|
270
|
+
assert.deepEqual(darkFailures(edge, dark.surface.elevated), [CHIP_RULE]);
|
|
271
|
+
});
|
|
272
|
+
|
|
260
273
|
test('raised at the card\'s own value is caught', () => {
|
|
261
274
|
assert.deepEqual(darkFailures('--color-surface-elevated', dark.surface.subtle), ['raised separates from the card']);
|
|
262
275
|
});
|
|
@@ -279,8 +292,8 @@ describe('the accent and the primary action', () => {
|
|
|
279
292
|
const [HOVER_RULE, PRESS_RULE] = ACTION_STEPS.map(([rule]) => rule);
|
|
280
293
|
|
|
281
294
|
/**
|
|
282
|
-
* What `filter: brightness(1.08)` does to a colour,
|
|
283
|
-
*
|
|
295
|
+
* What `filter: brightness(1.08)` does to a colour, the usual way to light a
|
|
296
|
+
* button under the pointer: each sRGB channel scaled and clipped.
|
|
284
297
|
*/
|
|
285
298
|
const brightened = (value) => {
|
|
286
299
|
const { r, g, b } = parseHex(value);
|
|
@@ -303,18 +316,33 @@ describe('the accent and the primary action', () => {
|
|
|
303
316
|
}
|
|
304
317
|
});
|
|
305
318
|
|
|
306
|
-
test(
|
|
307
|
-
//
|
|
308
|
-
// the label rule sees
|
|
309
|
-
//
|
|
310
|
-
assert.equal(brightened(typed.dark.color.brand.accent), '#
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
319
|
+
test('a hover that brightens the fill is caught in both palettes', () => {
|
|
320
|
+
// It takes the white label under the text floor (4.25:1 in dark, 4.19:1
|
|
321
|
+
// in light), which the label rule sees, and the step rule sees that it
|
|
322
|
+
// moved toward the label rather than away from it.
|
|
323
|
+
assert.equal(brightened(typed.dark.color.brand.accent), '#7b62f8');
|
|
324
|
+
for (const palette of ['dark', 'light']) {
|
|
325
|
+
assert.deepEqual(
|
|
326
|
+
failuresAfter(palette, '--color-brand-accent-hover', brightened(typed[palette].color.brand.accent)),
|
|
327
|
+
[LABEL_RULE, HOVER_RULE],
|
|
328
|
+
palette,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('a visible step toward the label is caught while the label still clears', () => {
|
|
334
|
+
// The accent dulled to 0.8 of its chroma at its own lightness: a step a
|
|
335
|
+
// reader sees (dE 3.94 in dark), under which the white label still clears
|
|
336
|
+
// the floor (4.72:1), and which moves toward the label all the same. Only
|
|
337
|
+
// the step rule's direction stands between it and a hover that fades.
|
|
338
|
+
const dulled = (value) => {
|
|
339
|
+
const lab = toOklab(parseHex(value));
|
|
340
|
+
return hex(fromOklab({ L: lab.L, a: lab.a * 0.8, b: lab.b * 0.8 }));
|
|
341
|
+
};
|
|
342
|
+
assert.equal(dulled(typed.dark.color.brand.accent), '#7164d1');
|
|
343
|
+
for (const palette of ['dark', 'light']) {
|
|
344
|
+
assert.deepEqual(failuresAfter(palette, '--color-brand-accent-hover', dulled(typed[palette].color.brand.accent)), [HOVER_RULE], palette);
|
|
345
|
+
}
|
|
318
346
|
});
|
|
319
347
|
|
|
320
348
|
test('a step nobody can see is caught, even one in the right direction', () => {
|
|
@@ -357,10 +385,12 @@ describe('the accent and the primary action', () => {
|
|
|
357
385
|
assert.deepEqual(failuresAfter('dark', line, typed.dark.color.surface.elevated), [
|
|
358
386
|
"the hairline round the rail's current row is visible on it",
|
|
359
387
|
]);
|
|
360
|
-
// Raised two steps off the
|
|
361
|
-
// to still holds, and the row is no longer there.
|
|
362
|
-
|
|
363
|
-
|
|
388
|
+
// Raised two steps off the dark frame: every other rule raised is held
|
|
389
|
+
// to still holds, and the row is no longer there. In light the hairline
|
|
390
|
+
// sits dE 1.23 from the frame, so a row lowered onto the frame loses its
|
|
391
|
+
// line as well, and only dark shows the lift on its own.
|
|
392
|
+
const { r, g, b } = parseHex(typed.dark.color.surface.frame);
|
|
393
|
+
assert.deepEqual(failuresAfter('dark', fill, hex({ r: r + 2, g: g + 2, b: b + 2 })), [
|
|
364
394
|
"the rail's current row lifts off the rail",
|
|
365
395
|
]);
|
|
366
396
|
});
|
|
@@ -410,39 +440,37 @@ describe('the state and chart hues', () => {
|
|
|
410
440
|
});
|
|
411
441
|
|
|
412
442
|
test("the design's stopped red is caught by its label, by deuteranopia and by the chart hues", () => {
|
|
413
|
-
// The approved #
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// under
|
|
417
|
-
// (
|
|
418
|
-
const failing = checksAfter('dark', '--color-feedback-danger', '#
|
|
443
|
+
// The approved #f0506e carries a destructive action's white label at
|
|
444
|
+
// 3.46:1, and a deuteranopic reader finds it dE 5.9 from done. It also
|
|
445
|
+
// sits under the floors the reserved red keeps from the orange (11.4
|
|
446
|
+
// under normal vision, 6.8 under deuteranopia) and from the green
|
|
447
|
+
// (deuteranopia 5.1).
|
|
448
|
+
const failing = checksAfter('dark', '--color-feedback-danger', '#f0506e').map(summary);
|
|
419
449
|
assert.deepEqual(failing, [
|
|
420
|
-
`${LABEL_RULE}: --color-text-on-accent on --color-feedback-danger:
|
|
421
|
-
`${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE
|
|
422
|
-
`${DANGER_RULE}: --color-data-2: dE
|
|
423
|
-
`${DANGER_RULE}: --color-data-3: dE
|
|
424
|
-
`${DANGER_RULE}: --color-data-4: dE n13.6/p8.9/d7.0 >= 14 normal, 8 dichromat`,
|
|
450
|
+
`${LABEL_RULE}: --color-text-on-accent on --color-feedback-danger: 3.46:1 (worst on --color-feedback-danger on --color-surface-frame)`,
|
|
451
|
+
`${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE n34.5/p18.6/d5.9 >= 10`,
|
|
452
|
+
`${DANGER_RULE}: --color-data-2: dE n11.4/p9.9/d6.8 >= 14 normal, 8 dichromat`,
|
|
453
|
+
`${DANGER_RULE}: --color-data-3: dE n32.4/p12.7/d5.1 >= 14 normal, 8 dichromat`,
|
|
425
454
|
]);
|
|
426
455
|
});
|
|
427
456
|
|
|
428
457
|
test("the design's light yellow is caught as a mark nobody can see on the frame", () => {
|
|
429
|
-
// #
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
`${MARK_RULE}: --color-data-4: 1.85:1 (worst on --color-surface-frame)`,
|
|
458
|
+
// #edb302 on the light frame is 1.61:1, and that is the only rule it
|
|
459
|
+
// breaks: a yellow dark enough to be seen on a light page is an ochre.
|
|
460
|
+
assert.deepEqual(checksAfter('light', '--color-data-4', '#edb302').map(summary), [
|
|
461
|
+
`${MARK_RULE}: --color-data-4: 1.61:1 (worst on --color-surface-frame)`,
|
|
434
462
|
]);
|
|
435
463
|
});
|
|
436
464
|
|
|
437
465
|
test('a neighbour that clears every dichromat floor is still caught under normal vision', () => {
|
|
438
|
-
//
|
|
439
|
-
// protanopia and deuteranopia, which the dichromat floor alone
|
|
440
|
-
// and
|
|
441
|
-
// sees colour. Nothing else it touches fails, so the
|
|
442
|
-
// is the only thing standing between it and a chart.
|
|
443
|
-
const failing = checksAfter('dark', '--color-data-4', '#
|
|
466
|
+
// A teal fourth series beside the aqua green third: dE 13.0 apart under
|
|
467
|
+
// protanopia and 12.3 under deuteranopia, which the dichromat floor alone
|
|
468
|
+
// passes, and 12.9 under normal vision, two shades of one hue to every
|
|
469
|
+
// reader who sees colour. Nothing else it touches fails, so the
|
|
470
|
+
// normal-vision floor is the only thing standing between it and a chart.
|
|
471
|
+
const failing = checksAfter('dark', '--color-data-4', '#008c87');
|
|
444
472
|
assert.deepEqual(failing.map(summary), [
|
|
445
|
-
`${ADJACENT_RULE}: --color-data-3 vs --color-data-4: dE
|
|
473
|
+
`${ADJACENT_RULE}: --color-data-3 vs --color-data-4: dE n12.9/p13.0/d12.3 >= ${DATA_ADJACENT_NORMAL_DE} normal, ${DATA_ADJACENT_DE} every vision`,
|
|
446
474
|
]);
|
|
447
475
|
assert.ok(failing[0].value >= DATA_ADJACENT_DE, 'the mutation has to clear the dichromat floor, or it proves nothing about the normal one');
|
|
448
476
|
});
|