@crewlethq/tokens 0.4.4 → 0.5.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 +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/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Single source of truth for the Crewlet design tokens: color, spacing, size, typography (`font`), radius, shadow, blur, breakpoint, density, motion and z-index. Authored as JSON, compiled by Style Dictionary into:
|
|
4
4
|
|
|
5
5
|
- `dist/css/tokens.css` (CSS custom properties on `:root`)
|
|
6
|
-
- `dist/css/themes.css` (the
|
|
6
|
+
- `dist/css/themes.css` (the dark and light palettes)
|
|
7
7
|
- `dist/css/density.css`, `dist/css/base.css`, `dist/css/breakpoint.css`
|
|
8
8
|
- `dist/index.js` + `dist/index.d.ts` (typed JS object for runtime use)
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ Single source of truth for the Crewlet design tokens: color, spacing, size, typo
|
|
|
13
13
|
// 1) Canonical variables. Always import this once at the app entrypoint.
|
|
14
14
|
import '@crewlethq/tokens/css';
|
|
15
15
|
|
|
16
|
-
// 2) The
|
|
16
|
+
// 2) The dark and light palettes, as a three-state contract on <html>.
|
|
17
17
|
// Import it after the line above; see "Theming" below.
|
|
18
18
|
import '@crewlethq/tokens/css/themes';
|
|
19
19
|
|
|
@@ -21,7 +21,7 @@ import '@crewlethq/tokens/css/themes';
|
|
|
21
21
|
// control; see "Density" below.
|
|
22
22
|
import '@crewlethq/tokens/css/density';
|
|
23
23
|
|
|
24
|
-
// 4) Optional: self-hosted
|
|
24
|
+
// 4) Optional: self-hosted Geist and Geist Mono (see "Fonts" below).
|
|
25
25
|
// Skip this if the app already loads its own fonts at the shell.
|
|
26
26
|
import '@crewlethq/tokens/css/fonts';
|
|
27
27
|
|
|
@@ -37,28 +37,56 @@ const accent = color.brand.accent;
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
CSS variables follow Style Dictionary's `--{group}-{path}` convention,
|
|
40
|
-
for example `var(--color-brand-
|
|
40
|
+
for example `var(--color-brand-accent)` or `var(--spacing-4)`.
|
|
41
|
+
|
|
42
|
+
## Breaking changes in 0.5.0
|
|
43
|
+
|
|
44
|
+
0.5.0 is the approved visual system: Geist, a dark-first palette on four surface rungs, a per-palette violet accent that is the primary action, and the four states and four chart series fitted to the palette suite's floors. Every edit a consumer makes is one row below; each links to the section that explains the change and carries the full table for it. `@crewlethq/ui` and `@crewlethq/icons` carry their own "Breaking changes in 0.5.0", and the three packages are upgraded together.
|
|
45
|
+
|
|
46
|
+
| What changed | What to change | Details |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| The type families are Geist and Geist Mono. `inter-latin.woff2`, `inter-latin-ext.woff2`, `jetbrains-mono-latin.woff2` and `jetbrains-mono-latin-ext.woff2` are no longer shipped or exported under `@crewlethq/tokens/fonts/*`. | `'Inter'` becomes `'Geist'` and `'JetBrains Mono'` becomes `'Geist Mono'`; `fonts/inter-*.woff2` becomes `fonts/geist-*.woff2` and `fonts/jetbrains-mono-*.woff2` becomes `fonts/geist-mono-*.woff2` in a preload or a copy. | [Fonts](#breaking-change-in-050-geist-and-geist-mono) |
|
|
49
|
+
| The palette is dark first: the bare root is dark, and light is painted under `@media (prefers-color-scheme: light) :root:not([data-theme="dark"])` and `:root[data-theme="light"]`. | A per-theme override of your own takes the same shape; `data-theme="light"` keeps a page light in a browser that reports no preference. | [Theming](#breaking-change-in-050) |
|
|
50
|
+
| `--color-surface-muted`, `--color-surface-topbar`, `-topbar-lift` and `-topbar-active` are removed; `--color-surface-frame` is new and `css/base` paints `body` with it. | `-muted` and `-topbar-lift` become `--color-surface-subtle`, `-topbar-active` becomes `--color-surface-elevated`, and `-topbar` becomes `--color-surface-frame` (a rail's ground) or `--color-surface-background` (a bar inside the page). | [Surfaces](#breaking-change-in-050-the-four-rungs) |
|
|
51
|
+
| `--color-brand-primary`, `-hover`, `-active` and `--color-text-on-brand` are removed, and the accent is a themed, per-palette violet. | `--color-brand-accent`, `-hover`, `-active`; `--color-text-on-accent` on an accent fill, `--color-text-inverse` on the inverse ground. An accent override mirrors the three theme selectors. | [The accent](#breaking-change-in-050-the-accent-is-the-primary-action) |
|
|
52
|
+
| `--color-data-5` is removed; the four series and the four states take new values. | A fifth series is `--color-data-other`. A figure that relied on a series' colour meaning something takes a state token. | [The states and the chart series](#breaking-change-in-050-the-state-and-chart-hues) |
|
|
53
|
+
| The phase family (`--color-phase-*`, `color.phase`) is removed. | The neutral text steps and the phase's word; inside a figure, a series the application maps the phase to. | [What colour means](#breaking-change-in-050-the-phase-family-is-removed) |
|
|
54
|
+
| `--size-control-md` is 30px and `--size-control-sm` 26px; the rail is 236px, the bar 52px, `--breakpoint-shell` 1024px, and the body 13px on 1.45. | A literal written to line up with any of them reads the token; a query that switched with the shell at `(max-width: 900px)` is `(width < 1024px)`. | [Density](#changed-in-050-the-control-steps), [Breakpoints](#changed-in-050-the-body-the-shell-and-its-breakpoint) |
|
|
55
|
+
| In `@crewlethq/tokens/test/palette`, the state names, several rule names and exports changed (`paletteStates()` returns `light (media query)` and `light (attribute)`; `PHASE` is removed; `OPAQUE_SURFACES` is the four rungs). | Rename a state or a rule a suite filters on, as each section's table lists. | [The palette suite](#the-palette-suite) |
|
|
41
56
|
|
|
42
57
|
## Theming
|
|
43
58
|
|
|
44
|
-
`@crewlethq/tokens/css/themes` carries the
|
|
59
|
+
`@crewlethq/tokens/css/themes` carries the dark and light palettes as three blocks on the root element, dark first:
|
|
45
60
|
|
|
46
61
|
```css
|
|
47
|
-
:root { /*
|
|
48
|
-
@media (prefers-color-scheme:
|
|
49
|
-
:root:not([data-theme="
|
|
62
|
+
:root { /* dark */ }
|
|
63
|
+
@media (prefers-color-scheme: light) {
|
|
64
|
+
:root:not([data-theme="dark"]) { /* light */ }
|
|
50
65
|
}
|
|
51
|
-
:root[data-theme="
|
|
66
|
+
:root[data-theme="light"] { /* light */ }
|
|
52
67
|
```
|
|
53
68
|
|
|
54
|
-
- A document that sets nothing is
|
|
55
|
-
- `document.documentElement.dataset.theme = '
|
|
56
|
-
- Removing the attribute follows the system again.
|
|
57
|
-
- Both
|
|
69
|
+
- A document that sets nothing is dark, or light if the reader's system asks for light. A browser that reports no preference at all gets dark: dark is the palette the product is drawn in, and light is its translation.
|
|
70
|
+
- `document.documentElement.dataset.theme = 'light'` or `'dark'` pins one, and wins in both directions, which a media query alone cannot do. Light wins by a block of its own. Dark wins by keeping the light media block from matching: there is no dark attribute block, because the bare root already is one, and the `:not([data-theme="dark"])` on the media block is what lets an explicit dark beat a light system.
|
|
71
|
+
- Removing the attribute follows the system again. A value the file does not name (`data-theme="system"`, say) passes the media block's guard and misses the light attribute block, so it follows the system as well; `ThemeSwitcher` removes the attribute rather than writing one.
|
|
72
|
+
- Both light blocks are generated from one source, `tokens/themes/light.json`, and the palette suite compares them, key for key and value for value.
|
|
58
73
|
|
|
59
74
|
Import it **after** `@crewlethq/tokens/css`. The two files share the `:root` selector and neither adds specificity, so the later import is the one that paints. `tokens.css` on its own is the base marketing palette, which is dark, and which carries a value for every themed slot so an application that imports only the token layer is never missing one.
|
|
60
75
|
|
|
61
|
-
`color-scheme` is set by the
|
|
76
|
+
`color-scheme` is set by each of the three blocks, so form controls, scrollbars and the canvas behind the page follow the palette that won.
|
|
77
|
+
|
|
78
|
+
An application that overrides a themed token for one palette mirrors the same three selectors: its dark value on `:root`, its light value under both the media block and the attribute block. An override written only under `:root[data-theme="light"]` misses a reader whose system is light and who has chosen nothing.
|
|
79
|
+
|
|
80
|
+
### Breaking change in 0.5.0
|
|
81
|
+
|
|
82
|
+
The palette is dark first. `themes.css` used to paint light on the bare root and dark under `@media (prefers-color-scheme: dark) :root:not([data-theme="light"])` and `:root[data-theme="dark"]`; it now paints dark on the bare root and light under `@media (prefers-color-scheme: light) :root:not([data-theme="dark"])` and `:root[data-theme="light"]`. The palettes themselves did not move.
|
|
83
|
+
|
|
84
|
+
| What changed | What to change |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| A document with no `data-theme`, in a browser that reports no colour-scheme preference, is dark. It was light. A system that asks for light or for dark still gets what it asks for. | Nothing, if the application follows the system. To keep a page light whatever the browser reports, set `data-theme="light"` on `<html>`. |
|
|
87
|
+
| The `:root[data-theme="dark"]` and `@media (prefers-color-scheme: dark)` blocks are gone; light is painted by `@media (prefers-color-scheme: light) :root:not([data-theme="dark"])` and `:root[data-theme="light"]`. | Move an application's own per-theme overrides to the same shape: dark values on `:root`, light values under both light selectors. |
|
|
88
|
+
| `paletteStates()` in `@crewlethq/tokens/test/palette` returns `base`, `dark`, `light (media query)` and `light (attribute)`. It returned `base`, `light`, `dark (media query)` and `dark (attribute)`. | Rename a state a suite reads: `light` becomes `light (attribute)` (or `light (media query)`), and `dark (media query)` and `dark (attribute)` both become `dark`. |
|
|
89
|
+
| The structural rules `the dark media block is present` and `the two dark blocks agree` are `the light media block is present` and `the two light blocks agree`, and the second compares both blocks in both directions. | Rename a rule a suite filters on or expects. |
|
|
62
90
|
|
|
63
91
|
### Breaking change in 0.3.0
|
|
64
92
|
|
|
@@ -89,12 +117,59 @@ Other changes a consumer may notice, none of which needs an edit:
|
|
|
89
117
|
```ts
|
|
90
118
|
import { themes } from '@crewlethq/tokens';
|
|
91
119
|
|
|
92
|
-
themes.light.color.text.secondary; // '#
|
|
93
|
-
themes.dark.color.data['4']; // '#
|
|
120
|
+
themes.light.color.text.secondary; // '#46464e'
|
|
121
|
+
themes.dark.color.data['4']; // '#c98500'
|
|
94
122
|
```
|
|
95
123
|
|
|
96
124
|
Prefer the custom property wherever CSS can reach: a value read here is taken at build time, so it does not follow a theme the reader changes. The package's own suite compares every exported value against the declaration in `themes.css`, in both directions, because two spellings of one palette is the arrangement that drifts.
|
|
97
125
|
|
|
126
|
+
## Surfaces
|
|
127
|
+
|
|
128
|
+
Four opaque rungs, lowest first. Every ground a piece of text can land on is one of these, or a translucent overlay over one of them:
|
|
129
|
+
|
|
130
|
+
| Rung | Token | Dark | Light | What it is for |
|
|
131
|
+
| --- | --- | --- | --- | --- |
|
|
132
|
+
| Frame | `--color-surface-frame` | `#09090b` | `#ededea` | The application ground: the rail, and the body around the floating sheet. `@crewlethq/tokens/css/base` paints `body` with it. |
|
|
133
|
+
| Sheet | `--color-surface-background` | `#101013` | `#f9f9f8` | The page content sits on: the main column, its top bar, a canvas. It keeps its old name because everything that means "the ground content sits on" already reads it. |
|
|
134
|
+
| Card | `--color-surface-subtle` | `#141418` | `#ffffff` | A card, a popover, a dialog body, a table's header band. |
|
|
135
|
+
| Raised | `--color-surface-elevated` | `#1b1b20` | `#f3f3f1` | A chip, a segmented well, a key cap, a meter track, a lifted row. In light it is a step *down*: a chip on a white card reads by being greyer than it. |
|
|
136
|
+
|
|
137
|
+
Each step is held to what it has to do, in OKLab dE, by the palette suite:
|
|
138
|
+
|
|
139
|
+
- **The sheet lifts off the frame by dE 3** (3.37 dark, 3.67 light). That lift is the whole layering device. The approved dark sheet, `#0f0f12`, measured 2.91 and was lifted along lightness alone to `#101013`.
|
|
140
|
+
- **A card separates from the sheet by dE 1.5** (1.87 dark, 1.82 light), near-flat on purpose: a card is found by its **hairline**, and a page of cards each lifted by dE 3 is a relief map. So `--color-border-default` is held to dE 3 against both the card and the sheet (6.57 and 8.44 dark, 8.24 and 6.42 light), and `@crewlethq/ui`'s Card suite holds every card variant that stands on this rung to drawing it.
|
|
141
|
+
- **Raised separates from the card by dE 3** (3.13 dark, 3.65 light), because a chip or a well has no border of its own.
|
|
142
|
+
|
|
143
|
+
`--color-surface-hover`, `-pressed` and `-inset` stay **translucent**, so a hovered row is right on every rung. The approved design paints its hover as one opaque colour, which one alpha cannot be on four grounds, so the alpha is fitted: the value that moves the design least across the four rungs and the tertiary text step together (a stronger overlay takes contrast from that step where it lands on raised). It is held from below by a floor the suite measures, that a hovered row separates from every rung by dE 3 and a pressed row from a hovered one by dE 3 more: without it the fit's cheapest answer is an overlay nobody sees, and the design's own light hover sits dE 0.60 off the frame. The inset well is the design's own. `--color-surface-glass` is the card at 0.90, frosted by `--blur-sm`: the ground of what floats over CONTENT rather than over interface, a canvas's bars and a chart's reading, and the suite holds every text step over 4.5:1 on it with each data hue, the residual and the inverse ground behind it. And `--color-surface-veil`, the ground a dialog sits on, is each root's own **frame** at 0.65: the application recedes into its own ground rather than under a film of its page.
|
|
144
|
+
|
|
145
|
+
Where an approved value failed one of the suite's floors on the new rungs, the floor won and the value moved as little as it could along lightness, keeping its hue. Each token's own comment names the value it replaced, the distance it moved and the measurement that forced it:
|
|
146
|
+
|
|
147
|
+
| Token | Approved | Shipped | Moved (dE) | Because |
|
|
148
|
+
| --- | --- | --- | --- | --- |
|
|
149
|
+
| `--color-surface-background` dark | `#0f0f12` | `#101013` | 0.46 | dE 2.91 off the frame, under 3 |
|
|
150
|
+
| `--color-text-tertiary` dark | `#8c8c96` | `#9797a1` | 3.63 | 3.92:1 on a pressed row inside a raised surface |
|
|
151
|
+
| `--color-text-tertiary` light | `#696972` | `#5f5e67` | 3.79 | 3.84:1 on a pressed row on the frame |
|
|
152
|
+
| `--color-text-muted` light | `#a3a3aa` | `#8d8d94` | 7.20 | 2.51:1 on the card and 2.14:1 on the frame, under the decoration band |
|
|
153
|
+
| `--color-border-control` dark | `#6e6e78` | `#787983` | 3.70 | 2.59:1 on a pressed row inside a raised surface |
|
|
154
|
+
| `--color-border-control` light | `#8e8e96` | `#7a7a82` | 6.71 | 2.29:1 on a pressed row on the frame |
|
|
155
|
+
| `--color-data-other` light | `#8e8e96` | `#88888f` | 2.03 | 2.77:1 as a mark on the frame |
|
|
156
|
+
|
|
157
|
+
### Breaking change in 0.5.0: the four rungs
|
|
158
|
+
|
|
159
|
+
The surface ramp was three rungs (`background`, `subtle` and `elevated`) plus four names that repeated them: `muted` was byte-identical to `subtle` in both palettes, and the three `topbar` steps held the values of `background`, `subtle` and `elevated`. They are gone, and the frame is new.
|
|
160
|
+
|
|
161
|
+
| What changed | What to change |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `--color-surface-muted` (`color.surface.muted`) is removed. | `--color-surface-subtle`, which held the same value. |
|
|
164
|
+
| `--color-surface-topbar` (`color.surface.topbar`) is removed. | `--color-surface-frame` for the ground a rail stands on; `--color-surface-background` for a top bar inside the page, which is the value it held. |
|
|
165
|
+
| `--color-surface-topbar-lift` (`color.surface.topbarLift`) is removed. | `--color-surface-subtle`, which held the same value. |
|
|
166
|
+
| `--color-surface-topbar-active` (`color.surface.topbarActive`) is removed. | `--color-surface-elevated`, which held the same value. |
|
|
167
|
+
| `--color-surface-frame` (`color.surface.frame`) is new, and `@crewlethq/tokens/css/base` paints `body` with it rather than with `--color-surface-background`. | An application that floats its page on the body paints that page `--color-surface-background` itself; one with its own reset paints its application ground with the frame. |
|
|
168
|
+
| The four rungs, the overlays, the borders, the neutral text steps and `--color-data-other` take the values above: the approved ones, or the least move from them that clears the suite's floors. | Nothing. |
|
|
169
|
+
| `--color-surface-veil` is each root's own frame at 0.65. It was the root's `--color-surface-background` at 0.72. | An application that composed its own veil reads `--color-surface-veil`, or derives it from the frame. |
|
|
170
|
+
| In `@crewlethq/tokens/css/legacy`, `--bg-primary`, `--bg-secondary`, `--bg-tertiary` and `--bg-elevated` read `--color-surface-background`, `-subtle`, `-elevated` and `-elevated`. They read the three `topbar` steps. | Nothing under `@crewlethq/tokens/css/themes`, where those held the same values. With `tokens.css` alone, `--bg-primary` is now the marketing root's black rather than `#15171c`. |
|
|
171
|
+
| In `@crewlethq/tokens/test/palette`, `OPAQUE_SURFACES` is the four rungs and `RAIL_GROUND` is `--color-surface-frame`. The rule `the card differs from the page` is gone; `the sheet lifts off the frame`, `a card separates from the sheet`, `the hairline that finds a card is visible on both sides of it`, `raised separates from the card`, `a hovered row is visible on every rung` and `a pressed row is visible over a hovered one` are new; `the veil is this root at an alpha, not a colour of its own` is `the veil is this root's frame at an alpha, not a colour of its own`, and `VEIL_ALPHA` is 0.65. `VEIL_GROUND`, `RUNG_STEPS`, `CARD_HAIRLINE`, `HAIRLINE_DE`, `OVERLAY_STEPS` and `OVERLAY_DE` are new exports. | Rename a rule a suite filters on, and measure on `OPAQUE_SURFACES` rather than on a list of surface names of its own. |
|
|
172
|
+
|
|
98
173
|
## Density
|
|
99
174
|
|
|
100
175
|
`@crewlethq/tokens/css/density` sets `--density` from an attribute on the root element:
|
|
@@ -107,47 +182,249 @@ Prefer the custom property wherever CSS can reach: a value read here is taken at
|
|
|
107
182
|
|
|
108
183
|
Every spacing and size token is emitted as `calc(Npx * var(--density, 1))`, so a density setting is a real change to every gap, pad, row and control rather than to three font sizes, and every value is unchanged at density 1. The stylesheet is a separate import because a product with no density control should not ship the selectors that switch one; the tokens themselves work without it.
|
|
109
184
|
|
|
110
|
-
`--size-control-sm` and `--size-row-sm`
|
|
185
|
+
`--size-control-sm` is `max(24px, calc(26px * var(--density, 1)))` and `--size-row-sm` is `max(24px, calc(28px * var(--density, 1)))`. The floor is deliberate: 26 x 0.82 is 21.32px and 28 x 0.82 is 22.96px, and a target under 24px is one a finger cannot reliably hit. A test resolves both at all three densities.
|
|
186
|
+
|
|
187
|
+
The control steps are the approved design's control rhythm: `--size-control-md` is 30px, the height of a button, an icon button, a field in a toolbar and a segmented well, and `--size-control-sm` is 26px, a small button's. Every control on a line takes one of them, so a button beside a field beside a segmented row is one height at every density.
|
|
111
188
|
|
|
112
|
-
`--size-target-min` (24px) is that same floor, named, for a component that DERIVES a height from a control step rather than taking one. Subtracting from a step subtracts from its floor too, so `calc(var(--size-control-
|
|
189
|
+
`--size-target-min` (24px) is that same floor, named, for a component that DERIVES a height from a control step rather than taking one. Subtracting from a step subtracts from its floor too, so `calc(var(--size-control-md) - 6px)` is 18.6px at compact density; `max(var(--size-target-min), calc(var(--size-control-md) - 6px))` is the 24px a chip in a segmented well stands at density 1, and still a reachable target at every other.
|
|
113
190
|
|
|
114
191
|
`--radius-chip` is `calc(var(--radius-md) - 1px)`, the corner of a chip lifted inside a well drawn at `--radius-md`. It is derived rather than written as a step of its own, so it follows the well.
|
|
115
192
|
|
|
193
|
+
### Changed in 0.5.0: the control steps
|
|
194
|
+
|
|
195
|
+
No token is renamed; two values moved to the approved design.
|
|
196
|
+
|
|
197
|
+
| What changed | What to change |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| `--size-control-md` is 30px (was 32px) and `--size-control-sm` is 26px (was 28px), still floored at 24px. Every control that takes a step (a button, an icon button, a field, a select, a tab) is two pixels shorter. | Nothing, for a stylesheet that reads the tokens. A literal 32 or 28 written to line up with a control reads the token instead. |
|
|
200
|
+
|
|
116
201
|
## The document baseline
|
|
117
202
|
|
|
118
|
-
`@crewlethq/tokens/css/base` is the opt-in document baseline: the box-sizing reset, the document's own type and colour, headings, controls that inherit the font, `code` and `kbd` in mono with tabular figures, one `:focus-visible` ring, `::selection`, themed scrollbars and the global reduced-motion collapse. Every value comes from a token, so it follows the palette and the density without a rule of its own.
|
|
203
|
+
`@crewlethq/tokens/css/base` is the opt-in document baseline: the box-sizing reset, the document's own type and colour (the body on the frame, the lowest of the [four rungs](#surfaces), in Geist at [13px on the 1.45 leading](#type)), headings, controls that inherit the font, `code` and `kbd` in mono with tabular figures, one `:focus-visible` ring, `::selection`, themed scrollbars and the global reduced-motion collapse. Every value comes from a token, so it follows the palette and the density without a rule of its own.
|
|
119
204
|
|
|
120
205
|
The focus indicator is an **outline**, never a box-shadow alone. Forced-colors mode drops every box-shadow and keeps outlines, so a ring drawn as a shadow disappears for exactly the readers who most need one. `--shadow-focus` is still there for a component that wants a shadow as well, paired with a transparent outline so forced colors substitutes a system colour for it. `--size-focus-ring-inset-offset` is the offset for a focusable row inside a clipping scroller, where an outset ring is clipped by the scroller and painted over by the next row.
|
|
121
206
|
|
|
122
207
|
The body's height and overflow are deliberately not here. A page that can scroll as a whole moves an application's rail off the top of the window, so the rule that stops it belongs to the shell that owns the rail.
|
|
123
208
|
|
|
209
|
+
## Type
|
|
210
|
+
|
|
211
|
+
The body is **13px on a 1.45 leading**, the approved design's dense base: a board is read by scanning rows rather than paragraphs. `@crewlethq/tokens/css/base` sets it on `body`; everything that does not set a size of its own inherits it.
|
|
212
|
+
|
|
213
|
+
| Token | px | What it is for |
|
|
214
|
+
| --- | --- | --- |
|
|
215
|
+
| `--font-size-2xs` | 11 | Micro labels, table column heads, a keycap |
|
|
216
|
+
| `--font-size-xs` | 12 | Dense secondary text, a badge, a group label |
|
|
217
|
+
| `--font-size-compact` | 13 | **The body**, and table cells and captions |
|
|
218
|
+
| `--font-size-sm` | 14 | A step over the body: a section heading, a page's title in the top bar |
|
|
219
|
+
| `--font-size-md` to `--font-size-2xl` | 16, 18, 20, 24 | Headings |
|
|
220
|
+
| `--font-size-display` | 28 | The one number on a stat tile, and a page's greeting |
|
|
221
|
+
| `--font-size-3xl` to `--font-size-5xl` | 32, 48, 64 | Large display headings |
|
|
222
|
+
|
|
223
|
+
Sizes are authored in px and emitted in rem, so a reader who has set a larger default text size gets one; the typed export (`font.size`) carries the pixels. `compact` and `display` are named for their jobs rather than squeezed onto the ramp, so a component reads the role and the step can move without a rename.
|
|
224
|
+
|
|
225
|
+
| Token | Value | What it is for |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `--font-line-height-tight` | 1.2 | Headings |
|
|
228
|
+
| `--font-line-height-snug` | 1.4 | A subtitle under a heading |
|
|
229
|
+
| `--font-line-height-body` | 1.45 | **The document's own**: the body's 18.85px line. A keycap, a monogram and the rail's lockup take it, because they are drawn inside a line of the document and have to stand on it |
|
|
230
|
+
| `--font-line-height-normal` | 1.5 | A component's own line: a card title, a rail row |
|
|
231
|
+
| `--font-line-height-relaxed` | 1.7 | Long-form prose |
|
|
232
|
+
|
|
233
|
+
## Radii
|
|
234
|
+
|
|
235
|
+
| Token | px | What it is for |
|
|
236
|
+
| --- | --- | --- |
|
|
237
|
+
| `--radius-xs` | 4 | A keycap, inline code, a tag's corner, the focus ring |
|
|
238
|
+
| `--radius-sm` | 6 | A row or a node inside a surface: a table row, an organisation chart's node |
|
|
239
|
+
| `--radius-chip` | 7 | A chip lifted inside a well at `--radius-md`, one hairline tighter (derived from `md`) |
|
|
240
|
+
| `--radius-md` | 8 | A control: a button, a field, a menu, a rail row |
|
|
241
|
+
| `--radius-lg` | 12 | A card, a popover, a stat tile |
|
|
242
|
+
| `--radius-sheet` | 14 | The sheet an application floats on the frame, one step rounder than every card inside it |
|
|
243
|
+
| `--radius-xl` | 16 | A dialog |
|
|
244
|
+
| `--radius-2xl` | 24 | Marketing surfaces |
|
|
245
|
+
| `--radius-pill`, `--radius-circle` | 999px, 50% | A pill, and a dot or an avatar |
|
|
246
|
+
|
|
247
|
+
## The application shell
|
|
248
|
+
|
|
249
|
+
The shell is a rail and a floating sheet on the frame (see [Surfaces](#surfaces) for the rungs they stand on):
|
|
250
|
+
|
|
251
|
+
| Token | Value | What it is |
|
|
252
|
+
| --- | --- | --- |
|
|
253
|
+
| `--size-shell-rail` | 236px | The rail, the sidebar column on the frame. Fixed at every density |
|
|
254
|
+
| `--size-shell-topbar` | 52px | The sheet's top bar, and the rail's head beside it. Fixed at every density |
|
|
255
|
+
| `--size-shell-inset` | 8px | The gap between the frame's edge and the sheet, on its top, its inline end and its bottom. Density-scaled like every gap: 6.56px compact, 9.12px comfortable |
|
|
256
|
+
| `--radius-sheet` | 14px | The sheet's corner |
|
|
257
|
+
| `--size-nav-row` | 30px | A row in the rail: a destination, a project, a pinned view. Density-scaled, and 24.6px at compact, so it needs no floor |
|
|
258
|
+
|
|
259
|
+
The rail and the bar do not follow the density because `--breakpoint-shell` is derived from the rail: a rail that grew with the density would move a breakpoint that a density setting cannot move.
|
|
260
|
+
|
|
124
261
|
## Breakpoints
|
|
125
262
|
|
|
126
263
|
A media query cannot read a custom property, so a stylesheet that switches layout at a breakpoint spells the number. `@crewlethq/tokens/css/breakpoint` is the generated partial that says which number, and `breakpoint` in the typed export carries the same values, so a check can compare a query's literal against them instead of trusting a comment.
|
|
127
264
|
|
|
128
|
-
`--breakpoint-shell` (
|
|
265
|
+
`--breakpoint-shell` (**1024px**) is where the application shell stops being a rail beside a sheet and the rail becomes a drawer. It is derived, at density 1:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
236px --size-shell-rail
|
|
269
|
+
+ 8px --size-shell-inset
|
|
270
|
+
+ 780px the sheet's floor: the two-pane Inbox, a 320px list beside a 460px detail
|
|
271
|
+
= 1024px --breakpoint-shell
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
At exactly 1024px the sheet is exactly its floor and the wide layout fits, so the switch is **strictly under** the step: `@media (width < 1024px)`, and `matchMedia('(width < 1024px)')` in script. `(max-width: 1024px)` would draw the drawer at the one width the arithmetic says the rail still fits. `@crewlethq/ui`'s `AppShell` switches there, and so does every component that changes shape with the shell (a side sheet going full width, a toolbar folding into its overflow, a toolbar's search field dropping its label, a stat row going to two columns); its suite holds all of them to the same query and asserts the sum. It shares its number with `--breakpoint-lg` by arithmetic, not by name: a change to the rail or the inset moves the shell step and leaves `lg` where it is.
|
|
275
|
+
|
|
276
|
+
`--breakpoint-phone` (**640px**) is the single-pane layout: under it a screen shows one pane at a time, a detail replacing its list with a way back, rather than the two side by side. It is the Inbox's 320px list twice: under it a detail beside the list would be narrower than the list itself. It switches strictly under the step too, `(width < 640px)`.
|
|
277
|
+
|
|
278
|
+
### Changed in 0.5.0: the body, the shell and its breakpoint
|
|
279
|
+
|
|
280
|
+
No token is renamed or removed here; values moved to the approved design, and seven tokens are new.
|
|
281
|
+
|
|
282
|
+
| What changed | What to change |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `@crewlethq/tokens/css/base` sets the body at `--font-size-compact` (13px) on `--font-line-height-body` (1.45). It was `--font-size-sm` (14px) on `--font-line-height-normal` (1.5). | Nothing, for text that inherits. A component of your own drawn inside a line of the document on `--font-line-height-normal` (a keycap, a monogram) takes `--font-line-height-body` to stay on it. |
|
|
285
|
+
| `--font-size-sm` is no longer the body step; its comment and role are "a step over the body". | A stylesheet that set body copy at `--font-size-sm` to match the document sets `--font-size-compact`. |
|
|
286
|
+
| `--size-shell-rail` is 236px (was 280px) and `--size-shell-topbar` is 52px (was 64px). | Nothing, for a stylesheet that reads them. A literal 280 or 64 written to line up with the shell reads the token instead. |
|
|
287
|
+
| `--breakpoint-shell` is 1024px (was 900px), derived as above, and the switch is strictly under it. | A query written to switch with the shell, `(max-width: 900px)`, becomes `(width < 1024px)`. |
|
|
288
|
+
| `--font-size-display`, `--font-line-height-body`, `--radius-sheet`, `--size-shell-inset`, `--breakpoint-phone` and `--motion-duration-breath` are new (`font.size.display`, `font.lineHeight.body`, `radius.sheet`, `size.shell.inset`, `breakpoint.phone`, `motion.duration.breath`). | Nothing. |
|
|
289
|
+
| `--size-nav-row` (`size.nav.row`, 30px) is new: the rail's own row step, which `@crewlethq/ui`'s `SidebarNav` draws its rows at in place of `--size-row-md` (36px). | Nothing. |
|
|
290
|
+
|
|
291
|
+
## Motion
|
|
292
|
+
|
|
293
|
+
| Token | Value | What it is for |
|
|
294
|
+
| --- | --- | --- |
|
|
295
|
+
| `--motion-duration-instant` | 75ms | A tooltip's flash, a dropdown indicator's nudge |
|
|
296
|
+
| `--motion-duration-fast` | 120ms | A hover or an active state |
|
|
297
|
+
| `--motion-duration-base` | 150ms | The default transition |
|
|
298
|
+
| `--motion-duration-moderate` | 200ms | A rail row's hover, a bar's background |
|
|
299
|
+
| `--motion-duration-slow` | 300ms | A dialog entering, a drawer sliding |
|
|
300
|
+
| `--motion-duration-breath` | 2200ms | The PERIOD of a mark saying work is still happening: one full round, out and back, 0.45 cycles a second. Not a transition. A pulsing `StatusDot` breathes one round of it |
|
|
301
|
+
|
|
302
|
+
A component that moves stops every motion it starts in the same stylesheet, naming the rule's selector again, after it, inside `@media (prefers-reduced-motion: reduce)`, which is what `crewlet-css-check` in `@crewlethq/ui` holds it to; and `@crewlethq/tokens/css/base` collapses motion for the whole document under `prefers-reduced-motion: reduce`.
|
|
129
303
|
|
|
130
304
|
## What colour means
|
|
131
305
|
|
|
132
|
-
|
|
306
|
+
Three families, and the meanings never move:
|
|
133
307
|
|
|
134
308
|
| Use | Meaning | Tokens |
|
|
135
309
|
| --- | --- | --- |
|
|
136
|
-
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
| Data | a chart series, and only inside a chart that carries a legend | `--color-data-1` to `-5`, `--color-data-other` |
|
|
310
|
+
| State | what a piece of work is doing: `info` is working, `warning` needs a person, `danger` is stopped, `success` is done. The names stay generic, because a badge, a callout and a toast mean the same four things | `--color-feedback-{success,warning,danger,info}` and their `-ink`, `-soft` and `-line` steps, and `--color-feedback-danger-hover` |
|
|
311
|
+
| Accent | what to act on, and what is chosen: the primary action's fill, the focus ring, the selected row, the filter that is on, the count of what is waiting on the reader | `--color-brand-accent`, `-hover`, `-active`, `-ink`, `-soft`, `-soft-strong`, `-rgb`, and `--color-focus` |
|
|
312
|
+
| Data | a series, and only inside a figure that names it: a legend for two series or more, the label for one | `--color-data-1` to `-4`, in that order, and `--color-data-other` for the rest |
|
|
140
313
|
|
|
141
314
|
Two rules travel with them:
|
|
142
315
|
|
|
143
316
|
- **A fill step is never text, and an `-ink` step is never a background.** A fill clears 3:1 as a mark; an ink clears 4.5:1 as text, including on its own soft tint.
|
|
144
317
|
- **Everything else is neutral.** A category with no state in it takes neutral colour, and its identity is carried by its name, its glyph and its position.
|
|
145
318
|
|
|
319
|
+
A phase is a category. The agent's phases (onboarding, execute, review, and whatever an engine adds) are drawn in the neutral colour with their word: a neutral tag, or the word itself, beside the state badge that says what the work is doing. There is no phase hue: a hue per phase spends the budget the four states and the four series are held apart in, and asks a reader to learn a second colour vocabulary that says nothing the word does not. Inside a figure a phase is a series like any other: the application declares which phase takes which series (`--color-data-1` to `-4`), keeps that mapping in every figure it draws, and the legend names each one.
|
|
320
|
+
|
|
146
321
|
A `-soft` step is its own fill at alpha 0.12 and a `-line` step is the same fill at alpha 0.30. They are derived by the build from the fill, per palette, so a tint cannot come to belong to a hue the fill no longer is.
|
|
147
322
|
|
|
323
|
+
### The accent
|
|
324
|
+
|
|
325
|
+
The accent is a violet, and it is **per palette**:
|
|
326
|
+
|
|
327
|
+
| Token | Dark (and the marketing root) | Light | What it is |
|
|
328
|
+
| --- | --- | --- | --- |
|
|
329
|
+
| `--color-brand-accent` | `#7c56ff` | `#6b45f0` | The fill of the primary action, a selected card's ring, a selected row's rail, the attention count. A fill and a mark, never text. |
|
|
330
|
+
| `--color-brand-accent-hover` | `#744bf4` | `#633ae5` | The primary action under the pointer. |
|
|
331
|
+
| `--color-brand-accent-active` | `#6c40e9` | `#5b2eda` | The primary action pressed. |
|
|
332
|
+
| `--color-brand-accent-ink` | `#b3a1ff` | `#5a33de` | The accent as text: a link, an outline button's label. |
|
|
333
|
+
| `--color-brand-accent-soft` | the accent at 0.16 | the accent at 0.108 | The tint behind a selected row or a toggle that is on. |
|
|
334
|
+
| `--color-brand-accent-soft-strong` | the accent at 0.32 | the accent at 0.32 | The border paired with the soft tint. |
|
|
335
|
+
| `--color-brand-accent-rgb` | `124, 86, 255` | `107, 69, 240` | The accent as an `r, g, b` triple, for a translucent tint of a stylesheet's own. `--shadow-glow` is composed from it. |
|
|
336
|
+
| `--color-focus` | `#7e5bff` | `#6b45f0` | The focus ring. |
|
|
337
|
+
| `--color-text-on-accent` | `#ffffff` | `#ffffff` | The label on the accent's three fills, and on the danger fill. |
|
|
338
|
+
|
|
339
|
+
`-rgb` and `-soft-strong` are derived from the accent by the build, per palette, and the build suite holds the written `-soft` to the accent's own channels.
|
|
340
|
+
|
|
341
|
+
- **The primary action is the accent.** The monochrome `--color-brand-primary` (white on dark, black on light) is gone: a white primary beside a light-grey secondary was one pair of greys. White clears 4.53:1 on the dark accent and 5.62:1 on the light one.
|
|
342
|
+
- **Its hover and its press are darker, a step each, never brighter.** The label is white, so a brighter fill is a step toward it: the approved design's `brightness(1.08)` hover took the dark accent's label to 4.18:1, under the text floor at the moment the reader is about to press. Each step is the least move along lightness a reader can see, dE 3, and the palette suite holds both halves: `a hovered primary action is a visible step away from its label` and `a pressed primary action is a visible step past a hovered one`.
|
|
343
|
+
- **The focus ring is the accent**, except where the floor forbids it. The dark accent measured 2.88:1 on a pressed row inside a raised surface, under the 3:1 a ring clears, and it cannot lighten itself without taking the primary action's label under 4.5:1; so the dark ring is the accent lifted dE 1.06, the least lift that clears it.
|
|
344
|
+
- **Where the reader is in a rail is not the accent.** The rail's current row stands on raised with the plain border round it (`RAIL_CURRENT_ROW`), and the rail's one hue is the attention count, the accent's fill with the on-accent label: the one thing in the chrome that asks the reader to act.
|
|
345
|
+
|
|
346
|
+
Where an approved value failed a floor, the floor won and the value moved the least it could, keeping its hue. Each token's comment names the value it replaced, the distance and the measurement:
|
|
347
|
+
|
|
348
|
+
| Token | Was | Shipped | Moved (dE) | Because |
|
|
349
|
+
| --- | --- | --- | --- | --- |
|
|
350
|
+
| `--color-brand-accent-hover` dark | `#865dff`, the approved `brightness(1.08)` | `#744bf4` | | 4.18:1 under the white label; the shipped step is darker by dE 3.08 |
|
|
351
|
+
| `--color-brand-accent-hover` light | `#744bff`, the approved `brightness(1.08)` | `#633ae5` | | it moved toward the label; the shipped step is darker by dE 3.03 |
|
|
352
|
+
| `--color-focus` dark | `#7c56ff`, the accent | `#7e5bff` | 1.06 | 2.88:1 on a pressed row inside a raised surface |
|
|
353
|
+
| `--color-brand-accent-soft` light | 0.10 | 0.108 | | a selected row sat dE 2.67 from a hovered one on the frame, under 3 |
|
|
354
|
+
|
|
355
|
+
### Breaking change in 0.5.0: the accent is the primary action
|
|
356
|
+
|
|
357
|
+
| What changed | What to change |
|
|
358
|
+
| --- | --- |
|
|
359
|
+
| `--color-brand-primary`, `--color-brand-primary-hover` and `--color-brand-primary-active` (`color.brand.primary`, `.primaryHover`, `.primaryActive`) are removed. | `--color-brand-accent`, `-hover` and `-active`, which is what the primary `Button` already painted. A surface that wanted the monochrome fill itself takes `--color-surface-inverse`. |
|
|
360
|
+
| `--color-text-on-brand` (`color.text.onBrand`) is removed. | `--color-text-on-accent` on an accent fill, or `--color-text-inverse` on the inverse ground. |
|
|
361
|
+
| The accent is per palette: `#7c56ff` on the dark root and the marketing root, `#6b45f0` in light. It was `#5469d4` in every palette, and its hover, press, ink, soft tint, triple and `--color-focus` move with it (the table above). | Nothing, for a stylesheet that reads the tokens. A literal `#5469d4` or `84, 105, 212` spelled in an application is the old accent: read the token instead. |
|
|
362
|
+
| The accent is a THEMED slot now, declared by all three blocks of `@crewlethq/tokens/css/themes`. It was declared by `tokens.css` alone. | An application that rebinds the accent on `:root` alone is beaten by `:root[data-theme="light"]` in light: mirror the three selectors, as for any themed token (see [Theming](#theming)), and rebind `-rgb`, `-soft` and `-soft-strong` with it. |
|
|
363
|
+
| `--color-brand-accent-rgb` and `--color-brand-accent-soft-strong` are derived from the accent by the build. | Nothing. |
|
|
364
|
+
| `--shadow-glow` is `rgba(var(--color-brand-accent-rgb), …)` rather than a literal indigo. | Nothing; it follows a rebound accent now. |
|
|
365
|
+
| In `@crewlethq/tokens/css/legacy`, `--accent`, `--primary-blue` and `--accent-purple` read `--color-brand-accent`. They read `--color-brand-primary`. | Nothing, unless a stylesheet relied on them being white on dark. |
|
|
366
|
+
| In `@crewlethq/tokens/test/palette`, `LABEL_ON_FILL` no longer carries the three `--color-text-on-brand` pairs; the rail's current row is `RAIL_CURRENT_ROW` (raised, with the plain border) and the attention count is the on-accent label on the accent in `RAIL_PAINTS`; `text on glass clears 4.5:1` measures the primary, secondary and tertiary steps over the inverse ground, the accent fill and each data hue and the residual, rather than over the brand-primary fill. `a hovered primary action is a visible step away from its label`, `a pressed primary action is a visible step past a hovered one`, `the rail's current row lifts off the rail` and `the hairline round the rail's current row is visible on it` are new, with the exports `ACTION_STEPS`, `ACTION_LABEL`, `ACTION_DE`, `RAIL_CURRENT_ROW` and `RAIL_CURRENT_LIFT`. | Rename a subject a suite filters on. |
|
|
367
|
+
| In `@crewlethq/tokens/test/palette`, `SCRIM_GROUNDS` is the ten avatar tints and no longer carries `--color-brand-accent`, so `a label on the scrim clears 4.5:1` measures over the opaque rungs and the tints alone: `@crewlethq/ui`'s `Avatar` has no accent-filled tone any more (a selected badge is a ring round a neutral one), so no upload overlay lands on the accent. | Nothing, unless a suite of your own reads `SCRIM_GROUNDS` for an accent-filled badge; measure that pair yourself. |
|
|
368
|
+
|
|
369
|
+
### The states and the chart series
|
|
370
|
+
|
|
371
|
+
The four states say what a piece of work is doing, and nothing else: **info is working, warning needs a person, danger is stopped, success is done**. Done stays a green in both palettes. The token names stay generic (`success`, `warning`, `danger`, `info`), because a badge, a callout and a toast mean the same four things wherever they are drawn.
|
|
372
|
+
|
|
373
|
+
The chart ramp is **four series**, blue, orange, aqua and yellow in that order, and `--color-data-other` for everything past the fourth. A series hue is legal only inside a figure that names it: a legend when it carries two series or more, the label when it carries one, which is what a `Meter` is.
|
|
374
|
+
|
|
375
|
+
Both families are the approved design's hues, or the least move from them that the palette suite's floors forced, found by [the fit](#the-approved-palette-and-the-fit). A state or a series moves inside its own hue family, 15 degrees either way, and each moved token's comment names the design value, the distance and the measurement:
|
|
376
|
+
|
|
377
|
+
| Token | Palette | Design | Shipped | Moved (dE) | Because |
|
|
378
|
+
| --- | --- | --- | --- | --- | --- |
|
|
379
|
+
| `--color-feedback-danger` | dark | `#f26d6d` | `#e50055` | 13.35 | white on it 2.92:1; dE 5.7 from done under deuteranopia; dE 7.8 from the chart orange under normal vision. It turns toward crimson as well as darkening, which is what lets the orange stay within dE 3.22 of the design |
|
|
380
|
+
| `--color-feedback-danger` | light | `#e5484d` | `#c01d32` | 10.40 | white on it 3.91:1; dE 8.4 from done under protanopia; dE 6.3 from the chart orange under normal vision |
|
|
381
|
+
| `--color-feedback-warning` | dark | `#f2b33d` | `#f4b221` | 1.32 | dE 8.5 from done under protanopia |
|
|
382
|
+
| `--color-feedback-warning` | light | `#e09a12` | `#c57600` | 10.32 | 2.03:1 as a mark on the frame |
|
|
383
|
+
| `--color-feedback-success` | dark | `#3fcf8e` | `#3ecf8f` | 0.13 | dE 9.9 from warning under protanopia |
|
|
384
|
+
| `--color-feedback-success` | light | `#23a26d` | `#009b74` | 2.80 | 2.77:1 as a mark on the frame |
|
|
385
|
+
| `--color-feedback-info` | light | `#2f7fe0` | `#3081de` | 0.67 | dE 7.4 from the accent under deuteranopia |
|
|
386
|
+
| `--color-feedback-info-ink` | light | `#1d63b8` | `#1a5eb3` | 1.56 | 4.21:1 on a pressed row on the frame |
|
|
387
|
+
| `--color-feedback-warning-ink` | light | `#935600` | `#8e5000` | 1.89 | 4.16:1 on a pressed row on the frame |
|
|
388
|
+
| `--color-feedback-danger-ink` | light | `#bf282e` | `#b61426` | 3.05 | 4.17:1 on a pressed row on the frame, and 3.94:1 on a code chip over the fitted fill's soft tint |
|
|
389
|
+
| `--color-feedback-success-ink` | light | `#17744c` | `#106d46` | 2.28 | 4.08:1 on a pressed row on the frame |
|
|
390
|
+
| `--color-data-1` | dark | `#3987e5` | `#458adc` | 1.95 | dE 6.0 from the accent under deuteranopia |
|
|
391
|
+
| `--color-data-1` | light | `#2a78d6` | `#3879ce` | 1.58 | dE 6.5 from the accent under deuteranopia |
|
|
392
|
+
| `--color-data-2` | dark | `#d95926` | `#e4631f` | 3.22 | dE 12.0 from danger under normal vision |
|
|
393
|
+
| `--color-data-2` | light | `#eb6834` | `#e45f28` | 2.39 | 2.73:1 as a mark on the frame |
|
|
394
|
+
| `--color-data-3` | dark | `#199e70` | `#009c79` | 1.62 | dE 6.3 from danger under deuteranopia |
|
|
395
|
+
| `--color-data-3` | light | `#1baf7a` | `#009b74` | 6.04 | 2.40:1 as a mark on the frame |
|
|
396
|
+
| `--color-data-4` | light | `#eda100` | `#c57600` | 13.09 | 1.85:1 as a mark on the frame: a yellow dark enough to be seen on a light page is an ochre |
|
|
397
|
+
|
|
398
|
+
Every other state and series step ships as designed: the dark inks, dark info, and the dark yellow. The design's soft tints are drawn at their own alphas; the kit derives every `-soft` step at 0.12 and every `-line` step at 0.30 from the fill, per palette.
|
|
399
|
+
|
|
400
|
+
In light, two series land on a state's own value: the aqua on done's green (`#009b74`) and the yellow on the warning ochre (`#c57600`). The design drew each pair a few steps apart, and each was darkened onto the same floor, a mark that clears 3:1 on the frame, where the two meet at the edge of what a screen can show. 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.
|
|
401
|
+
|
|
402
|
+
- **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.71:1 dark, 6.06:1 light), and its hover, `--color-feedback-danger-hover`, is a step darker again, the least a reader can see (dE 3.14 dark, 3.04 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`.
|
|
403
|
+
- **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 aqua sat dE 0.73 from the approved red under protanopia.
|
|
404
|
+
- **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.
|
|
405
|
+
|
|
406
|
+
The marketing root takes the dark palette's state and series values.
|
|
407
|
+
|
|
408
|
+
### Breaking change in 0.5.0: the state and chart hues
|
|
409
|
+
|
|
410
|
+
| What changed | What to change |
|
|
411
|
+
| --- | --- |
|
|
412
|
+
| `--color-data-5` (`color.data.5`) is removed: the ramp is four series. | A fifth series is the residual, `--color-data-other`; a figure with more than four series folds the rest into it or is split. |
|
|
413
|
+
| `--color-data-1` to `-4` are the approved blue, orange, aqua and yellow (the table above). They were an indigo, a yellow, a sky blue and a green in dark, and an indigo, an amber, a blue and a green in light. | Nothing, for a figure that reads the tokens and names its series. A figure that relied on a series' colour meaning something (a green series for "passed") takes a state token instead, which is what a meaning is for. |
|
|
414
|
+
| The four state fills, their inks and `--color-feedback-danger-hover` take the values above, and so do their derived `-soft` and `-line` steps. The dark fills were a teal, an amber, a red and a sky blue, and the light ones a dark teal, a brown, a dark red and a dark blue. | Nothing, for a stylesheet that reads the tokens. |
|
|
415
|
+
| Nine neutral and accent steps move by one 8-bit step or one thousandth of alpha, because the fit places them jointly with the state inks drawn on them: in light, `--color-surface-hover` 0.045 to 0.044, `--color-surface-pressed` 0.089 to 0.088, `--color-surface-inset` 0.025 to 0.026, `--color-text-tertiary` `#5e5e67` to `#5f5e67`, `--color-data-other` `#87878f` to `#88888f`, `--color-brand-accent-soft` 0.109 to 0.108 and `--color-brand-accent-active` `#5b2dda` to `#5b2eda`; in dark and on the marketing root, `--color-focus` `#805bff` to `#7e5bff`; in dark, `--color-border-control` `#797983` to `#787983`. | Nothing. |
|
|
416
|
+
| In `@crewlethq/tokens/test/palette`, `DATA` is the four series, and `adjacent data hues stay separable` holds a neighbouring pair to dE 15 under normal vision as well as 9 under every vision (the new exports `DATA_ADJACENT_DE` and `DATA_ADJACENT_NORMAL_DE`). `ACTION_STEPS` gains `a hovered destructive action is a visible step away from its label`. | Rename a subject a suite filters on. |
|
|
417
|
+
|
|
418
|
+
### Breaking change in 0.5.0: the phase family is removed
|
|
419
|
+
|
|
420
|
+
| What changed | What to change |
|
|
421
|
+
| --- | --- |
|
|
422
|
+
| `--color-phase-onboarding`, `--color-phase-execute` and `--color-phase-review`, with their `-ink` and `-soft` steps, are removed from every palette, and so is `color.phase` (and `themes.*.color.phase`) in the typed export. | A phase is drawn in the neutral colour with its word: `--color-text-secondary` or `--color-text-tertiary` for the word, on a neutral ground, never a hue of its own. Inside a figure, map each phase to a series, `--color-data-1` to `-4`, and name it in the legend. |
|
|
423
|
+
| In `@crewlethq/tokens/test/palette`, the `PHASE` export is removed, `INK_STEPS` and `FILL_STEPS` no longer carry the phase steps, and the rules `phase hues stay separable` and `a phase hue clears the status family` are removed. | Drop a suite's filter on those rule names. |
|
|
424
|
+
|
|
148
425
|
## The palette suite
|
|
149
426
|
|
|
150
|
-
`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
|
|
427
|
+
`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 and the hairline that finds a card, 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.
|
|
151
428
|
|
|
152
429
|
The module is **published**, as `@crewlethq/tokens/test/palette`, so a consumer runs the same rules over the version it installed:
|
|
153
430
|
|
|
@@ -165,7 +442,45 @@ expect(failures.map(describeFailure)).toEqual([]);
|
|
|
165
442
|
|
|
166
443
|
One implementation, two gates. The constraint is that the floors are kept, not only that the measurement moves, and a tokens bump that lowered a ratio would otherwise reach a consumer through an auto-merged dependency update with nothing measuring a ratio again.
|
|
167
444
|
|
|
168
|
-
The tarball ships `test/color.mjs`, `test/palette.mjs
|
|
445
|
+
The tarball ships `test/color.mjs`, `test/palette.mjs`, `test/palette.d.mts` and `test/palette.test.mjs`, so the whole table runs over the installed version with no configuration at all, from the application's root:
|
|
446
|
+
|
|
447
|
+
```sh
|
|
448
|
+
node node_modules/@crewlethq/tokens/test/palette.test.mjs
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
or with the package's own `node --test "test/*.test.mjs"` from inside `node_modules/@crewlethq/tokens`. Not with `node --test` over a path into `node_modules` from outside it: the test runner skips `node_modules`, a glob included, runs no test at all and exits 0. `test/tarball.test.mjs` packs this package with npm, installs the tarball into an empty directory and runs both commands there, counting the tests that ran, and imports the suite by its package name to hold `runPalette`, `paletteStates`, `describeFailure` and `tightest` to the shapes `test/palette.d.mts` declares, which is how an application's own suite reaches them.
|
|
452
|
+
|
|
453
|
+
`test/build.test.mjs` stays out of the tarball: it reads the token source the build compiles from, which a tarball does not carry. So do `test/intent.test.mjs`, which reads the design record below, `test/tarball.test.mjs`, which packs the package from outside it, and `test/fonts.test.mjs`, which decodes every file in `fonts/` and holds [`fonts/README.md`](./fonts/README.md), the built `@font-face` rules and [`fonts/OFL.txt`](./fonts/OFL.txt) to what the files say about themselves: a replaced file is caught in this repository, before it is published, not in an installed copy.
|
|
454
|
+
|
|
455
|
+
### The approved palette, and the fit
|
|
456
|
+
|
|
457
|
+
[`tokens/intent.json`](./tokens/intent.json) is the approved design's palette: the two token blocks every artboard declares (`.app` for dark, `.app.light` for light), declaration for declaration, each with the custom property that ships it, and the colours the artboards spell outside those blocks. It is a record rather than a token file: the build skips it and nothing is emitted from it. `test/intent.test.mjs` holds it to naming only tokens the palettes declare, the same names for the same tokens in both palettes, and values that parse.
|
|
458
|
+
|
|
459
|
+
Every colour the record names ships the value recorded there, or the least move from it that one of the palette suite's floors forced: the surfaces, the neutral text and border steps, the accent, the four states and their inks, and the four chart series. [`scripts/fit-palette.mjs`](./scripts/fit-palette.mjs) is what finds those moves:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
npm run build --workspace @crewlethq/tokens
|
|
463
|
+
node packages/tokens/scripts/fit-palette.mjs # both palettes, about three minutes
|
|
464
|
+
node packages/tokens/scripts/fit-palette.mjs light # one of them
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
From the recorded values it searches for the palette that moves the design least in all, in OKLab dE, while clearing every rule, each candidate measured by `runPalette` itself over the candidate stylesheets:
|
|
468
|
+
|
|
469
|
+
- a neutral step and the accent's family move along lightness alone, as every move before the fit was made; a state or chart hue moves inside a hue family 15 degrees either side of the design's; a translucent step moves its alpha;
|
|
470
|
+
- only a value that fails a floor of its own moves, never the ground it was measured on, and never a value that clears every floor it has to spare another;
|
|
471
|
+
- the four rungs keep the design's order, and so does the text ramp, because the suite holds a step to a distance and not to a direction (in light, raised is below the card);
|
|
472
|
+
- a token the design never drew (the focus ring, a pressed row, the primary action's hover and press, the destructive action's hover, a control's boundary) is fitted from the value it is a step of, and only after the design's own values: it never buys a design value back by moving itself. A step is seated first, where its own rule holds, because at the value it is a step of that rule refuses it, and the design is then repaired around it;
|
|
473
|
+
- a value's own floors are repaired before a separation between two values, because where a hue's floor sends it does not depend on anything else;
|
|
474
|
+
- a hue is looked for along lightness first, keeping its hue and chroma, the move every hand fit made, and then in drawn directions inside its family; and a seat found along a drawn direction is then turned toward the design, the direction rotated in halving angles until no turn brings the seat nearer, because a seat on a drawn direction is only as near as that direction lets it be;
|
|
475
|
+
- a value that another is measured ON moves together with it: a state's ink is held to 4.5:1 on its own fill's soft tint and on a code chip over that tint, so a fill walked back toward the design takes its ink with it, the ink repaired at every step. Moved one at a time, neither could go nearer, and the light danger red ended as a muted maroon dE 15.19 from the design where the red and its ink moved together need 10.40 and 3.05.
|
|
476
|
+
|
|
477
|
+
It is seeded (mulberry32 at seed 1). The search spends a fixed budget of palette evaluations repairing and improving the palette, and the finish then runs until no move it knows pays, so the same built stylesheets give the same fit, to the byte. The fit is the best it finds, not a proof that nothing moves the design less; its first line says how many evaluations the search and the finish took, and says so plainly if the finish ever stopped at its ceiling with moves still paying. For each value it fits it prints the design value, the value the fit ships, the dE between them and the binding rule, which is what fails when that value alone goes back to the design; then every token whose built value is not the fit. A token the floors moved says the same three things in its own comment in `tokens/themes/*.json`: the design value, the dE of the move and the measurement that forced it.
|
|
478
|
+
|
|
479
|
+
The fit is a development tool. It is not published, and no test runs it or reads what it prints: the gate is the palette suite, and the fit is how a value that passes it is chosen.
|
|
480
|
+
|
|
481
|
+
The fit is held to every rule in the table, and the shipped palette is exactly what it prints: run over this release's stylesheets, it reports that every value it fits ships its fitted value, in both palettes.
|
|
482
|
+
|
|
483
|
+
The Storybook shows both halves under **Foundations / Palette**. `Tightest Per Rule` is `tightest()` over the whole table: under each rule, the pair that would break first. `The Fit` is every colour `tokens/intent.json` declares, per palette, with what ships for it, the dE between them, and what fails when that value alone goes back to the design: the suite run again over the built `themes.css` with that one declaration put back (an overlay the design drew opaque at the alpha nearest it, and the steps the build derives from a fill following it), reduced to the tightest failure under each rule it breaks. It is measured every time the story renders rather than copied from a comment, so a move that stopped being forced says so there first. Its logic is `apps/storybook/src/paletteFit.ts`, and `apps/storybook/test/paletteFit.test.mjs` holds it.
|
|
169
484
|
|
|
170
485
|
## Fonts
|
|
171
486
|
|
|
@@ -173,16 +488,29 @@ The design system uses two type families, both self-hosted inside this package a
|
|
|
173
488
|
|
|
174
489
|
| Token | Family |
|
|
175
490
|
| ----- | ------ |
|
|
176
|
-
| `--font-family-sans` |
|
|
491
|
+
| `--font-family-sans` | Geist |
|
|
177
492
|
| `--font-family-display` | An alias of `--font-family-sans` (see the note below) |
|
|
178
|
-
| `--font-family-mono` |
|
|
493
|
+
| `--font-family-mono` | Geist Mono |
|
|
179
494
|
|
|
180
495
|
`--font-family-display` is declared on `:root` as `var(--font-family-sans)`. A custom property that holds `var()` is resolved on the element that declares it and inherited as a finished value, so the alias follows an override of `--font-family-sans` declared on `:root` and no other. An app that overrides the sans family on `body`, a theme class or any narrower selector sets `--font-family-display` in the same rule.
|
|
181
496
|
|
|
182
|
-
`@crewlethq/tokens/css/fonts` declares `@font-face` rules whose URLs point at the woff2 files in `fonts/`, relative to the stylesheet. A bundler (Vite, webpack, Rollup, esbuild) resolves those URLs and emits the files with the application's other assets, so the fonts render on a closed network and no request leaves for a third-party host. The files are also exported individually (for example `@crewlethq/tokens/fonts/
|
|
497
|
+
`@crewlethq/tokens/css/fonts` declares `@font-face` rules whose URLs point at the woff2 files in `fonts/`, relative to the stylesheet. A bundler (Vite, webpack, Rollup, esbuild) resolves those URLs and emits the files with the application's other assets, so the fonts render on a closed network and no request leaves for a third-party host. The files are also exported individually (for example `@crewlethq/tokens/fonts/geist-latin.woff2`) for apps that preload them or serve them from their own static directory.
|
|
498
|
+
|
|
499
|
+
The four files are 83,736 bytes in all, and a page that renders only basic Latin downloads the two `latin` files, 52,528 bytes. The sans `latin` file keeps `tnum`, so the tabular figures the kit sets on every number are Geist's own; Geist Mono needs none, being fixed-pitch.
|
|
183
500
|
|
|
184
501
|
Versions, subsets and the replacement procedure are in [`fonts/README.md`](./fonts/README.md). The license text and copyright notices are in [`fonts/OFL.txt`](./fonts/OFL.txt), which must accompany the files wherever they are redistributed. An application that bundles the fonts ships it by importing `@crewlethq/tokens/fonts/OFL.txt` as an asset, so the bundler emits it beside the font files, or by copying its text into the application's third-party notices.
|
|
185
502
|
|
|
503
|
+
### Breaking change in 0.5.0: Geist and Geist Mono
|
|
504
|
+
|
|
505
|
+
The families were Inter 4.001 and JetBrains Mono 2.211. The fallback stacks after the first name are unchanged, and so is every weight a stylesheet asks for: both new families carry the whole `wght` axis from 100 to 900 (JetBrains Mono stopped at 800).
|
|
506
|
+
|
|
507
|
+
| What changed | What to change |
|
|
508
|
+
| --- | --- |
|
|
509
|
+
| `@crewlethq/tokens/css/fonts` declares `@font-face` families `'Geist'` and `'Geist Mono'`. It declared `'Inter'` and `'JetBrains Mono'`. | A stylesheet that names a family itself, rather than reading `--font-family-sans` or `--font-family-mono`, renames it; one that names the old family falls back to the system font. |
|
|
510
|
+
| `--font-family-sans` (`font.family.sans`) leads with `'Geist'` and `--font-family-mono` (`font.family.mono`) with `'Geist Mono'`. | Nothing, for a stylesheet that reads the tokens. |
|
|
511
|
+
| `fonts/inter-latin.woff2`, `fonts/inter-latin-ext.woff2`, `fonts/jetbrains-mono-latin.woff2` and `fonts/jetbrains-mono-latin-ext.woff2` are gone, from the package and from its `./fonts/*` export. | `fonts/geist-latin.woff2`, `fonts/geist-latin-ext.woff2`, `fonts/geist-mono-latin.woff2` and `fonts/geist-mono-latin-ext.woff2` in a preload or a copy of your own. |
|
|
512
|
+
| `fonts/OFL.txt` carries the Geist Project Authors' copyright notices rather than Inter's and JetBrains'. | Regenerate an application's third-party notices from the file, or re-copy its text. |
|
|
513
|
+
|
|
186
514
|
## Material Symbols
|
|
187
515
|
|
|
188
516
|
`@crewlethq/tokens/css/material-symbols` is an opt-in stylesheet that loads the Material Symbols Outlined icon font from Google Fonts: it requests `fonts.googleapis.com`, which serves the `@font-face` rule and the `material-symbols-outlined` class, and the browser then downloads the font from `fonts.gstatic.com`.
|
|
@@ -195,7 +523,7 @@ It is the one stylesheet in this package that contacts a third-party host, which
|
|
|
195
523
|
|
|
196
524
|
## Add or change a token
|
|
197
525
|
|
|
198
|
-
1. Edit a JSON file in `tokens/`, or `tokens/themes/light.json` and `tokens/themes/dark.json` for a slot that changes with the palette. A themed slot needs an entry in **both** theme files and a value in `tokens/color.json`, which the palette suite checks.
|
|
526
|
+
1. Edit a JSON file in `tokens/`, or `tokens/themes/light.json` and `tokens/themes/dark.json` for a slot that changes with the palette. A themed slot needs an entry in **both** theme files and a value in `tokens/color.json`, which the palette suite checks. A colour the design draws starts from its value in `tokens/intent.json`, which changes first when the design does, and `scripts/fit-palette.mjs` gives the least move from it that clears the suite (see [The approved palette, and the fit](#the-approved-palette-and-the-fit)).
|
|
199
527
|
2. Run `npm run build` (or `npm run dev` for watch).
|
|
200
528
|
3. Run `npm test`. A colour change that lowers a measured floor fails here.
|
|
201
529
|
4. The generated `dist/` and `src/index.ts` are regenerated.
|
package/dist/css/base.css
CHANGED
|
@@ -35,12 +35,18 @@ html {
|
|
|
35
35
|
|
|
36
36
|
body {
|
|
37
37
|
margin: 0;
|
|
38
|
-
|
|
38
|
+
/* The FRAME, the lowest of the four rungs: what an application floats on
|
|
39
|
+
the body (a sheet, a rail) brings its own ground, and the body is the
|
|
40
|
+
application ground rather than a page. */
|
|
41
|
+
background: var(--color-surface-frame);
|
|
39
42
|
color: var(--color-text-primary);
|
|
40
43
|
font-family: var(--font-family-sans);
|
|
41
|
-
|
|
44
|
+
/* The approved design's dense base, 13px on an 18.85px line: a board is
|
|
45
|
+
read by scanning rows, not paragraphs. The size is the compact step,
|
|
46
|
+
which is the body now, and the leading is the body's own step. */
|
|
47
|
+
font-size: var(--font-size-compact);
|
|
42
48
|
font-weight: var(--font-weight-regular);
|
|
43
|
-
line-height: var(--font-line-height-
|
|
49
|
+
line-height: var(--font-line-height-body);
|
|
44
50
|
-webkit-font-smoothing: antialiased;
|
|
45
51
|
-moz-osx-font-smoothing: grayscale;
|
|
46
52
|
}
|
package/dist/css/breakpoint.css
CHANGED