@crewlethq/tokens 0.9.0 → 0.11.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 CHANGED
@@ -424,6 +424,7 @@ Every other state and series step ships as approved: the dark inks, dark success
424
424
  In light, the yellow series sits beside the warning ochre (`#c27600` and `#c17300`). The approved palette drew the two a few steps apart, and both were darkened onto the same floor, a mark that clears 3:1 on the frame. Nothing in the suite keeps a series from a state other than the reserved red, and a series is read only inside a figure that names it.
425
425
 
426
426
  - **The danger fill carries a white label**, on a toast's destructive action and on an upload's remove button, so it is dark enough for 4.5:1 in both palettes (4.59:1 dark, 6.61:1 light), and its hover, `--color-feedback-danger-hover`, is a step darker again, the least a reader can see (dE 3.03 dark, 3.02 light). The palette suite holds the hover as it holds the primary action's: `a hovered destructive action is a visible step away from its label`.
427
+ - **A status glyph is held to 3:1 on its fill.** An `Avatar` status badge is the info, warning or danger fill carrying a small glyph in `--color-text-inverse`, the one ink that clears all three in both palettes (white sits at 1.6:1 on the dark info step). The pairs are `MARK_ON_FILL`, measured over every ground as a label is, under `a glyph clears 3:1 on its own fill`: the 3:1 a mark is held to, since a glyph is a shape rather than running text. The tightest is 3.33:1, the inverse ink on the light info step.
427
428
  - **The reserved red is kept away from every series**: dE 14 under normal vision and 8 under protanopia and deuteranopia, where red, orange and green fall onto one axis. The approved chart green sat dE 0.6 from the shipped red under deuteranopia.
428
429
  - **Neighbouring series** sit dE 9 apart under every vision and dE 15 under normal vision. The second floor is new: on its own, the dichromat floor let a pair sit dE 9 apart for every reader, which to full-colour vision is two shades of one hue.
429
430
 
@@ -448,7 +449,7 @@ The marketing root keeps its own state and series values (`tokens/color.json`),
448
449
 
449
450
  ## The palette suite
450
451
 
451
- `test/palette.mjs` holds the rule table and the colour maths, and `test/palette.test.mjs` runs it over `dist/css` in import order, in every theme state: the base marketing root (`base`), the dark root (`dark`), light by media query (`light (media query)`) and light by attribute (`light (attribute)`), which are the keys `paletteStates()` returns. It measures every text step on every surface it can land on (the four opaque rungs in `OPAQUE_SURFACES`, and the translucent overlays composited over each of them), the step between a word and a dimmed one, every ink on its own soft tint, every fill as a mark, the primary action's three fills under its label and the steps between them, the destructive action's fill and its hover under the same label, the focus ring, the control boundary, the rail's current row, the steps between the rungs, the hairline that finds a card and the strong hairline round a raised chip, whether a hovered and a pressed row can be seen, the hue separations under normal, protan and deuteranopic vision (the states from each other, the chart series from their neighbours and from the danger red, and every hue from the accent), and the structure of the theme file itself. A component suite that measures a colour of its own measures it on `OPAQUE_SURFACES` too, rather than on a list of surface names it keeps itself.
452
+ `test/palette.mjs` holds the rule table and the colour maths, and `test/palette.test.mjs` runs it over `dist/css` in import order, in every theme state: the base marketing root (`base`), the dark root (`dark`), light by media query (`light (media query)`) and light by attribute (`light (attribute)`), which are the keys `paletteStates()` returns. It measures every text step on every surface it can land on (the four opaque rungs in `OPAQUE_SURFACES`, and the translucent overlays composited over each of them), the step between a word and a dimmed one, every ink on its own soft tint, every fill as a mark, each status glyph's ink on its fill, the primary action's three fills under its label and the steps between them, the destructive action's fill and its hover under the same label, the focus ring, the control boundary, the rail's current row, the steps between the rungs, the hairline that finds a card and the strong hairline round a raised chip, whether a hovered and a pressed row can be seen, the hue separations under normal, protan and deuteranopic vision (the states from each other, the chart series from their neighbours and from the danger red, and every hue from the accent), and the structure of the theme file itself. A component suite that measures a colour of its own measures it on `OPAQUE_SURFACES` too, rather than on a list of surface names it keeps itself.
452
453
 
453
454
  The module is **published**, as `@crewlethq/tokens/test/palette`, so a consumer runs the same rules over the version it installed:
454
455
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewlethq/tokens",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Crewlet design tokens. Single JSON source compiled to CSS variables and typed JS modules.",
5
5
  "license": "MIT AND OFL-1.1",
6
6
  "repository": {
@@ -158,6 +158,11 @@ export const DATA_ADJACENT_NORMAL_DE: number;
158
158
  * ACTION_LABEL on it.
159
159
  */
160
160
  export const ACTION_STEPS: readonly (readonly [string, string, string])[];
161
+ /**
162
+ * Each state fill with the ink an avatar's status glyph is drawn in on it:
163
+ * [ink, fill], held to 3:1 over every ground the fill can sit on.
164
+ */
165
+ export const MARK_ON_FILL: readonly (readonly [string, string])[];
161
166
  export const ACTION_LABEL: string;
162
167
  export const ACTION_DE: number;
163
168
 
package/test/palette.mjs CHANGED
@@ -235,6 +235,23 @@ export const LABEL_ON_FILL = [
235
235
  ['--color-text-inverse', '--color-surface-inverse'],
236
236
  ];
237
237
 
238
+ /**
239
+ * A fill with the GLYPH a component draws inside it: an avatar's status badge,
240
+ * which is a state's fill step carrying a small play, question or stop mark in
241
+ * the inverse ink. A glyph is a graphic rather than running text, so it is
242
+ * held to the 3:1 a mark is held to rather than to a label's 4.5:1.
243
+ *
244
+ * THE INVERSE INK, because it is the one ink that clears every state fill in
245
+ * both palettes: white sits under 2:1 on the dark palette's info and warning
246
+ * steps, and the dark ink under 3:1 on none of them. Each fill is measured
247
+ * over every ground it can sit on, as a label's is.
248
+ */
249
+ export const MARK_ON_FILL = [
250
+ ['--color-text-inverse', '--color-feedback-info'],
251
+ ['--color-text-inverse', '--color-feedback-warning'],
252
+ ['--color-text-inverse', '--color-feedback-danger'],
253
+ ];
254
+
238
255
  /**
239
256
  * THE PRIMARY ACTION'S THREE FILLS, at rest, under the pointer and pressed,
240
257
  * each held against the one before it: [rule, the step, the fill it follows].
@@ -759,6 +776,11 @@ function checkState(state, values, profile, push) {
759
776
  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})`);
760
777
  }
761
778
 
779
+ for (const [ink, fill] of MARK_ON_FILL) {
780
+ const low = worst(colour(ink), opaque.map(([name, rgb]) => [`${fill} on ${name}`, flatten(values.get(fill), rgb)]));
781
+ say('a glyph clears 3:1 on its own fill', low.ratio >= 3, `${ink} on ${fill}`, low.ratio, `${low.ratio.toFixed(2)}:1 (worst on ${low.name})`);
782
+ }
783
+
762
784
  // THE PRIMARY ACTION'S STEPS. See ACTION_STEPS.
763
785
  for (const [rule, step, from] of ACTION_STEPS) {
764
786
  const label = colour(ACTION_LABEL);
@@ -25,6 +25,7 @@ import {
25
25
  DIMMED_TEXT,
26
26
  fromOklab,
27
27
  hex,
28
+ MARK_ON_FILL,
28
29
  oklabToLinear,
29
30
  OPAQUE_SURFACES,
30
31
  OVERLAY_STEPS,
@@ -418,6 +419,30 @@ describe('the state and chart hues', () => {
418
419
  const DESTRUCTIVE_RULE = ACTION_STEPS[2][0];
419
420
  const summary = (check) => `${check.rule}: ${check.subject}: ${check.detail}`;
420
421
 
422
+ test('every status glyph is measured on its fill, in every state', () => {
423
+ // An avatar's status badge is a state fill carrying a glyph. A table that
424
+ // stopped measuring one of the three would pass the one badge nobody can
425
+ // read: white on the dark info step is 1.6:1.
426
+ const GLYPH_RULE = 'a glyph clears 3:1 on its own fill';
427
+ assert.deepEqual(
428
+ MARK_ON_FILL.map(([, fill]) => fill),
429
+ ['--color-feedback-info', '--color-feedback-warning', '--color-feedback-danger'],
430
+ );
431
+ for (const state of THEME_STATES) {
432
+ for (const [ink, fill] of MARK_ON_FILL) {
433
+ const measured = checks.filter((check) => check.state === state && check.rule === GLYPH_RULE && check.subject === `${ink} on ${fill}`);
434
+ assert.equal(measured.length, 1, `${state}: ${fill}`);
435
+ assert.ok(measured[0].ok, `${state}: ${describeFailure(measured[0])}`);
436
+ }
437
+ }
438
+ // And it can fail: white glyphs on the dark info step.
439
+ const white = checksAfter('dark', '--color-text-inverse', '#ffffff').filter((check) => check.rule === GLYPH_RULE);
440
+ assert.deepEqual(
441
+ white.map((check) => check.subject),
442
+ ['--color-text-inverse on --color-feedback-info', '--color-text-inverse on --color-feedback-warning'],
443
+ );
444
+ });
445
+
421
446
  test('the chart ramp is four series, in every theme state and in the typed export', () => {
422
447
  // A fifth series would have to sit between two of the four in a hue
423
448
  // budget the reserved red and the accent have spent, so it is the