@crewlethq/tokens 0.6.0 → 0.6.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 CHANGED
@@ -39,6 +39,15 @@ const accent = color.brand.accent;
39
39
  CSS variables follow Style Dictionary's `--{group}-{path}` convention,
40
40
  for example `var(--color-brand-accent)` or `var(--spacing-4)`.
41
41
 
42
+ ## Changed in 0.6.1: the dark text ramp
43
+
44
+ In 0.6.0 the dark primary and tertiary text steps sat 1.98:1 apart, so a word a screen dims from the first step to the third (a calendar's days from the neighbouring month) read as a rounding rather than a step. The dark primary is lifted dE 0.30, and the palette suite now holds the step in every state.
45
+
46
+ | What changed | What to change | Details |
47
+ | --- | --- | --- |
48
+ | `--color-text-primary` in dark is `#e7eaf3`. It was `#e6e9f2`. The fit placed the danger fill, its hover and the third chart series one 8-bit step from their 0.6.0 values beside it. | Nothing, for a stylesheet that reads the tokens. | [Surfaces](#surfaces) |
49
+ | In `@crewlethq/tokens/test/palette`, the rule `a dimmed word is a step from the word being read` is new, with the export `DIMMED_TEXT`: the primary and tertiary text steps clear 2:1 between themselves. | Nothing, unless an application's own palette override draws the two steps closer. | [The palette suite](#the-palette-suite) |
50
+
42
51
  ## Changed in 0.6.0: the default palette
43
52
 
44
53
  The dark and light themes take the default palette approved on 4 October 2026: blue-black neutrals, a violet accent (`#725be6` in dark, `#735af2` in light) and a cyan working state. No token is renamed or removed; the themed values change. [`tokens/intent.json`](./tokens/intent.json) records the approved values, and every value the palette suite's floors moved names its approved value, the distance and the measurement in its own comment. The marketing root, `tokens.css` on its own, keeps its values.
@@ -165,6 +174,7 @@ Where an approved value failed one of the suite's floors, the floor won and the
165
174
  | `--color-border-default` dark | `#14151a` | `#191b20` | 2.53 | dE 0.99 off the card, under 3 |
166
175
  | `--color-border-default` light | `#f5f5f5` | `#e7e8ec` | 3.92 | dE 0.84 off the sheet and 1.02 off raised, under 3 |
167
176
  | `--color-border-strong` dark | `#2b3245` | `#2d3448` | 0.85 | dE 2.43 as a raised chip's edge on an inset well inside a raised surface, under 3 |
177
+ | `--color-text-primary` dark | `#e6e9f2` | `#e7eaf3` | 0.30 | 1.98:1 from the tertiary step, under the 2:1 a dimmed word keeps |
168
178
  | `--color-text-tertiary` dark | `#8a91a6` | `#9fa7bc` | 7.08 | 3.45:1 on a pressed row inside a raised surface |
169
179
  | `--color-text-tertiary` light | `#5a6075` | `#585e73` | 0.71 | 4.37:1 on a pressed row on the frame |
170
180
  | `--color-text-muted` light | `#8a90a3` | `#878d9f` | 1.04 | 2.69:1 on the frame, under the decoration band |
@@ -393,7 +403,7 @@ Both families are the approved palette's hues, or a move from them that the pale
393
403
 
394
404
  | Token | Palette | Design | Shipped | Moved (dE) | Because |
395
405
  | --- | --- | --- | --- | --- | --- |
396
- | `--color-feedback-danger` | dark | `#f0506e` | `#d93855` | 6.66 | white on it 3.46:1; dE 5.9 from done under deuteranopia; dE 5.1 from the chart green under deuteranopia |
406
+ | `--color-feedback-danger` | dark | `#f0506e` | `#d73756` | 7.04 | white on it 3.46:1; dE 5.9 from done under deuteranopia; dE 4.9 from the chart green under deuteranopia |
397
407
  | `--color-feedback-danger` | light | `#cc2a4a` | `#b9112c` | 5.75 | dE 7.2 from done under deuteranopia; dE 6.9 from the chart green under deuteranopia |
398
408
  | `--color-feedback-warning` | dark | `#f0b429` | `#f0b425` | 0.17 | dE 9.8 from done under protanopia |
399
409
  | `--color-feedback-warning` | light | `#ffb300` | `#c17300` | 19.65 | 1.52:1 as a mark on the frame |
@@ -405,7 +415,7 @@ Both families are the approved palette's hues, or a move from them that the pale
405
415
  | `--color-data-1` | dark | `#3c7fec` | `#4d8ff6` | 4.75 | dE 9.7 from the accent under normal vision and 3.3 under deuteranopia |
406
416
  | `--color-data-1` | light | `#4381e5` | `#5c76c3` | 5.71 | dE 4.5 from the accent under deuteranopia |
407
417
  | `--color-data-2` | light | `#ed810c` | `#dd5920` | 9.59 | 2.29:1 as a mark on the frame |
408
- | `--color-data-3` | dark | `#2fa24e` | `#26b583` | 7.39 | dE 0.9 from danger under deuteranopia; dE 2.6 from the orange under protanopia |
418
+ | `--color-data-3` | dark | `#2fa24e` | `#26b582` | 7.30 | dE 0.6 from danger under deuteranopia; dE 2.6 from the orange under protanopia |
409
419
  | `--color-data-3` | light | `#2f9d4c` | `#00956e` | 5.75 | 2.94:1 as a mark on the frame; dE 5.6 from the yellow under protanopia; dE 7.9 from danger under deuteranopia |
410
420
  | `--color-data-4` | light | `#edb302` | `#c27600` | 17.33 | 1.61:1 as a mark on the frame: a yellow dark enough to be seen on a light page is an ochre |
411
421
 
@@ -413,8 +423,8 @@ Every other state and series step ships as approved: the dark inks, dark success
413
423
 
414
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.
415
425
 
416
- - **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.52: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`.
417
- - **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.9 from the shipped red under deuteranopia.
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
+ - **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.
418
428
  - **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.
419
429
 
420
430
  The marketing root keeps its own state and series values (`tokens/color.json`), which the suite measures in the `base` state.
@@ -438,7 +448,7 @@ The marketing root keeps its own state and series values (`tokens/color.json`),
438
448
 
439
449
  ## The palette suite
440
450
 
441
- `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), 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.
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.
442
452
 
443
453
  The module is **published**, as `@crewlethq/tokens/test/palette`, so a consumer runs the same rules over the version it installed:
444
454
 
@@ -25,7 +25,7 @@
25
25
  --color-surface-scrim: rgba(0, 0, 0, 0.6);
26
26
  --color-surface-veil: rgba(7, 8, 13, 0.65);
27
27
  --color-surface-inverse: #fcfcfc;
28
- --color-text-primary: #e6e9f2;
28
+ --color-text-primary: #e7eaf3;
29
29
  --color-text-secondary: #a9b0c3;
30
30
  --color-text-tertiary: #9fa7bc;
31
31
  --color-text-muted: #5a6176;
@@ -48,8 +48,8 @@
48
48
  --color-feedback-success-ink: #74dcae;
49
49
  --color-feedback-warning: #f0b425;
50
50
  --color-feedback-warning-ink: #f6cd72;
51
- --color-feedback-danger: #d93855;
52
- --color-feedback-danger-hover: #ce2c4d;
51
+ --color-feedback-danger: #d73756;
52
+ --color-feedback-danger-hover: #cc2b4e;
53
53
  --color-feedback-danger-ink: #ff9fb0;
54
54
  --color-feedback-info: #61d0ff;
55
55
  --color-feedback-info-ink: #8ad6fb;
@@ -57,13 +57,13 @@
57
57
  --color-feedback-success-line: rgba(52, 201, 139, 0.3);
58
58
  --color-feedback-warning-soft: rgba(240, 180, 37, 0.12);
59
59
  --color-feedback-warning-line: rgba(240, 180, 37, 0.3);
60
- --color-feedback-danger-soft: rgba(217, 56, 85, 0.12);
61
- --color-feedback-danger-line: rgba(217, 56, 85, 0.3);
60
+ --color-feedback-danger-soft: rgba(215, 55, 86, 0.12);
61
+ --color-feedback-danger-line: rgba(215, 55, 86, 0.3);
62
62
  --color-feedback-info-soft: rgba(97, 208, 255, 0.12);
63
63
  --color-feedback-info-line: rgba(97, 208, 255, 0.3);
64
64
  --color-data-1: #4d8ff6;
65
65
  --color-data-2: #e67737;
66
- --color-data-3: #26b583;
66
+ --color-data-3: #26b582;
67
67
  --color-data-4: #eab106;
68
68
  --color-data-other: #6a7186;
69
69
  --color-node-purple: #8e6ed4;
package/dist/themes.d.ts CHANGED
@@ -141,7 +141,7 @@ export declare const themes: {
141
141
  readonly inverse: "#fcfcfc";
142
142
  };
143
143
  readonly text: {
144
- readonly primary: "#e6e9f2";
144
+ readonly primary: "#e7eaf3";
145
145
  readonly secondary: "#a9b0c3";
146
146
  readonly tertiary: "#9fa7bc";
147
147
  readonly muted: "#5a6176";
@@ -170,8 +170,8 @@ export declare const themes: {
170
170
  readonly successInk: "#74dcae";
171
171
  readonly warning: "#f0b425";
172
172
  readonly warningInk: "#f6cd72";
173
- readonly danger: "#d93855";
174
- readonly dangerHover: "#ce2c4d";
173
+ readonly danger: "#d73756";
174
+ readonly dangerHover: "#cc2b4e";
175
175
  readonly dangerInk: "#ff9fb0";
176
176
  readonly info: "#61d0ff";
177
177
  readonly infoInk: "#8ad6fb";
@@ -179,15 +179,15 @@ export declare const themes: {
179
179
  readonly successLine: "rgba(52, 201, 139, 0.3)";
180
180
  readonly warningSoft: "rgba(240, 180, 37, 0.12)";
181
181
  readonly warningLine: "rgba(240, 180, 37, 0.3)";
182
- readonly dangerSoft: "rgba(217, 56, 85, 0.12)";
183
- readonly dangerLine: "rgba(217, 56, 85, 0.3)";
182
+ readonly dangerSoft: "rgba(215, 55, 86, 0.12)";
183
+ readonly dangerLine: "rgba(215, 55, 86, 0.3)";
184
184
  readonly infoSoft: "rgba(97, 208, 255, 0.12)";
185
185
  readonly infoLine: "rgba(97, 208, 255, 0.3)";
186
186
  };
187
187
  readonly data: {
188
188
  readonly "1": "#4d8ff6";
189
189
  readonly "2": "#e67737";
190
- readonly "3": "#26b583";
190
+ readonly "3": "#26b582";
191
191
  readonly "4": "#eab106";
192
192
  readonly other: "#6a7186";
193
193
  };
package/dist/themes.js CHANGED
@@ -142,7 +142,7 @@ export const themes = {
142
142
  "inverse": "#fcfcfc"
143
143
  },
144
144
  "text": {
145
- "primary": "#e6e9f2",
145
+ "primary": "#e7eaf3",
146
146
  "secondary": "#a9b0c3",
147
147
  "tertiary": "#9fa7bc",
148
148
  "muted": "#5a6176",
@@ -171,8 +171,8 @@ export const themes = {
171
171
  "successInk": "#74dcae",
172
172
  "warning": "#f0b425",
173
173
  "warningInk": "#f6cd72",
174
- "danger": "#d93855",
175
- "dangerHover": "#ce2c4d",
174
+ "danger": "#d73756",
175
+ "dangerHover": "#cc2b4e",
176
176
  "dangerInk": "#ff9fb0",
177
177
  "info": "#61d0ff",
178
178
  "infoInk": "#8ad6fb",
@@ -180,15 +180,15 @@ export const themes = {
180
180
  "successLine": "rgba(52, 201, 139, 0.3)",
181
181
  "warningSoft": "rgba(240, 180, 37, 0.12)",
182
182
  "warningLine": "rgba(240, 180, 37, 0.3)",
183
- "dangerSoft": "rgba(217, 56, 85, 0.12)",
184
- "dangerLine": "rgba(217, 56, 85, 0.3)",
183
+ "dangerSoft": "rgba(215, 55, 86, 0.12)",
184
+ "dangerLine": "rgba(215, 55, 86, 0.3)",
185
185
  "infoSoft": "rgba(97, 208, 255, 0.12)",
186
186
  "infoLine": "rgba(97, 208, 255, 0.3)"
187
187
  },
188
188
  "data": {
189
189
  "1": "#4d8ff6",
190
190
  "2": "#e67737",
191
- "3": "#26b583",
191
+ "3": "#26b582",
192
192
  "4": "#eab106",
193
193
  "other": "#6a7186"
194
194
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewlethq/tokens",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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": {
@@ -90,6 +90,12 @@ export const OPAQUE_SURFACES: readonly string[];
90
90
  */
91
91
  export const TEXT_STEPS: readonly (readonly [string, number])[];
92
92
 
93
+ /**
94
+ * A dimmed word against the word being read, as [read, dimmed, contrast]: the
95
+ * two text steps clear that contrast between themselves.
96
+ */
97
+ export const DIMMED_TEXT: readonly [string, string, number];
98
+
93
99
  /**
94
100
  * The steps of the ladder, each as [rule, upper rung, lower rung, the dE the
95
101
  * upper rung has to sit off the lower one].
package/test/palette.mjs CHANGED
@@ -155,6 +155,18 @@ export const TEXT_STEPS = [
155
155
  ['--color-text-tertiary', 4.5],
156
156
  ];
157
157
 
158
+ /**
159
+ * A DIMMED word against the word being read, as [read, dimmed, contrast]: the
160
+ * primary step and the tertiary one. A screen dims what is not its subject by
161
+ * dropping it from the first step to the third (a calendar's days from the
162
+ * neighbouring month, a resolved row under an open one), and both are facts,
163
+ * so each clears its own floor above. What this holds is that the two are a
164
+ * STEP apart rather than a rounding: 2:1 between the inks themselves. A ramp
165
+ * squeezed from both ends, a dimmer primary over a tertiary lifted for its
166
+ * floor on a pressed raised row, can keep every floor and lose the step.
167
+ */
168
+ export const DIMMED_TEXT = ['--color-text-primary', '--color-text-tertiary', 2];
169
+
158
170
  /** A hue used as TEXT on a surface. */
159
171
  export const INK_STEPS = [
160
172
  '--color-brand-accent-ink',
@@ -681,6 +693,12 @@ function checkState(state, values, profile, push) {
681
693
  const low = worst(colour(name), all);
682
694
  say('text step clears its floor', low.ratio >= floor, name, low.ratio, `${low.ratio.toFixed(2)}:1 >= ${floor} (worst on ${low.name})`);
683
695
  }
696
+ {
697
+ // A dimmed word against the word being read. See DIMMED_TEXT.
698
+ const [read, dimmed, floor] = DIMMED_TEXT;
699
+ const ratio = contrast(colour(read), colour(dimmed));
700
+ say('a dimmed word is a step from the word being read', ratio >= floor, `${read} vs ${dimmed}`, ratio, `${ratio.toFixed(2)}:1 >= ${floor}`);
701
+ }
684
702
 
685
703
  {
686
704
  // It exists for a hairline glyph and a disabled affordance. The assertion
@@ -22,6 +22,7 @@ import {
22
22
  DATA_ADJACENT_DE,
23
23
  DATA_ADJACENT_NORMAL_DE,
24
24
  describeFailure,
25
+ DIMMED_TEXT,
25
26
  fromOklab,
26
27
  hex,
27
28
  oklabToLinear,
@@ -156,6 +157,17 @@ describe('the palette', () => {
156
157
  assert.ok(onBase.includes('text step clears its floor'));
157
158
  });
158
159
 
160
+ test('a dimmed word is measured in every state, and one a rounding from the word being read is caught', () => {
161
+ const DIMMED_RULE = 'a dimmed word is a step from the word being read';
162
+ assert.deepEqual(DIMMED_TEXT, ['--color-text-primary', '--color-text-tertiary', 2]);
163
+ for (const state of STATES) {
164
+ assert.equal(checks.filter((check) => check.state === state && check.rule === DIMMED_RULE).length, 1, state);
165
+ }
166
+ // The tertiary step lifted onto the secondary one: each clears its own
167
+ // floor, and the step between the first and the third is gone.
168
+ assert.deepEqual(failuresAfter('dark', '--color-text-tertiary', typed.dark.color.text.secondary), [DIMMED_RULE]);
169
+ });
170
+
159
171
  test('the theme layer is dark first, and wins over the token layer', () => {
160
172
  // themes.css is imported second on purpose. If that ever stopped being
161
173
  // true the dark state would silently be the marketing palette; and if the
@@ -444,13 +456,13 @@ describe('the state and chart hues', () => {
444
456
  // 3.46:1, and a deuteranopic reader finds it dE 5.9 from done. It also
445
457
  // sits under the floors the reserved red keeps from the orange (11.4
446
458
  // under normal vision, 6.8 under deuteranopia) and from the green
447
- // (deuteranopia 5.1).
459
+ // (deuteranopia 4.9).
448
460
  const failing = checksAfter('dark', '--color-feedback-danger', '#f0506e').map(summary);
449
461
  assert.deepEqual(failing, [
450
462
  `${LABEL_RULE}: --color-text-on-accent on --color-feedback-danger: 3.46:1 (worst on --color-feedback-danger on --color-surface-frame)`,
451
463
  `${STATUS_RULE}: --color-feedback-success vs --color-feedback-danger: dE n34.5/p18.6/d5.9 >= 10`,
452
464
  `${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`,
465
+ `${DANGER_RULE}: --color-data-3: dE n32.5/p12.7/d4.9 >= 14 normal, 8 dichromat`,
454
466
  ]);
455
467
  });
456
468
 
@@ -465,12 +477,12 @@ describe('the state and chart hues', () => {
465
477
  test('a neighbour that clears every dichromat floor is still caught under normal vision', () => {
466
478
  // A teal fourth series beside the aqua green third: dE 13.0 apart under
467
479
  // 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
480
+ // passes, and 13.0 under normal vision, two shades of one hue to every
469
481
  // reader who sees colour. Nothing else it touches fails, so the
470
482
  // normal-vision floor is the only thing standing between it and a chart.
471
483
  const failing = checksAfter('dark', '--color-data-4', '#008c87');
472
484
  assert.deepEqual(failing.map(summary), [
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`,
485
+ `${ADJACENT_RULE}: --color-data-3 vs --color-data-4: dE n13.0/p13.0/d12.3 >= ${DATA_ADJACENT_NORMAL_DE} normal, ${DATA_ADJACENT_DE} every vision`,
474
486
  ]);
475
487
  assert.ok(failing[0].value >= DATA_ADJACENT_DE, 'the mutation has to clear the dichromat floor, or it proves nothing about the normal one');
476
488
  });