@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/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, #0f0f12, measured 2.91 and was
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 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
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, aqua and
173
- * an ochre yellow, the four the approved design draws.
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 design's
176
- * four clear them with room (its tightest neighbours sit dE 19.8 apart under
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. 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.
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.84 off
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: in
291
- * dark the two fills land dE 1.56 apart, because the fitted hover overlay
292
- * lifts the frame almost to raised.
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 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.
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 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.
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('/');
@@ -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, which is the hover the
283
- * approved design drew: each sRGB channel scaled and clipped.
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("the approved design's brightening hover is caught in both palettes", () => {
307
- // In dark it takes the white label under the text floor (4.18:1), which
308
- // the label rule sees; in light it still clears 5.01:1, and only the step
309
- // rule sees that it moved toward the label rather than away from it.
310
- assert.equal(brightened(typed.dark.color.brand.accent), '#865dff');
311
- assert.deepEqual(failuresAfter('dark', '--color-brand-accent-hover', brightened(typed.dark.color.brand.accent)), [
312
- LABEL_RULE,
313
- HOVER_RULE,
314
- ]);
315
- assert.deepEqual(failuresAfter('light', '--color-brand-accent-hover', brightened(typed.light.color.brand.accent)), [
316
- HOVER_RULE,
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 light frame: every other rule raised is held
361
- // to still holds, and the row is no longer there.
362
- const { r, g, b } = parseHex(typed.light.color.surface.frame);
363
- assert.deepEqual(failuresAfter('light', fill, hex({ r: r + 2, g: g + 2, b: b + 2 })), [
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 #f26d6d carries a destructive action's white label at
414
- // 2.92:1, and a deuteranopic reader finds it dE 5.7 from done (5.59
415
- // against the design's own green, before either moved). It also sits
416
- // under the floors the reserved red keeps from the orange, the aqua
417
- // (protanopia 2.0) and the yellow.
418
- const failing = checksAfter('dark', '--color-feedback-danger', '#f26d6d').map(summary);
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: 2.92:1 (worst on --color-feedback-danger on --color-surface-frame)`,
421
- `${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE n30.4/p15.2/d5.7 >= 10`,
422
- `${DANGER_RULE}: --color-data-2: dE n7.8/p8.3/d7.3 >= 14 normal, 8 dichromat`,
423
- `${DANGER_RULE}: --color-data-3: dE n28.7/p2.0/d14.0 >= 14 normal, 8 dichromat`,
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
- // #eda100 on the light frame is 1.85:1, the palest mark the design draws,
430
- // and the only rule it breaks: a yellow dark enough to be seen on a light
431
- // page is an ochre.
432
- assert.deepEqual(checksAfter('light', '--color-data-4', '#eda100').map(summary), [
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
- // An olive fourth series beside the aqua third: dE 12.1 apart under both
439
- // protanopia and deuteranopia, which the dichromat floor alone passes,
440
- // and 11.6 under normal vision, two shades of one hue to every reader who
441
- // sees colour. Nothing else it touches fails, so the normal-vision floor
442
- // is the only thing standing between it and a chart.
443
- const failing = checksAfter('dark', '--color-data-4', '#5a8100');
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 n11.6/p12.1/d12.1 >= ${DATA_ADJACENT_NORMAL_DE} normal, ${DATA_ADJACENT_DE} every vision`,
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
  });