@cueplusplus/tokens 0.13.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/CHANGELOG.md ADDED
@@ -0,0 +1,1250 @@
1
+ # @cueplusplus/tokens
2
+
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 31a7567: Six new tokens on `:root`: `--cue-leading-prose` and `--cue-leading-ui` (both 1.625), and `--cue-measure-narrow` / `--cue-measure-wide` / `--cue-measure-broad` / `--cue-measure-prose` (42rem / 54rem / 84rem / 68ch). Line height and reading measure had no token at all until now — 186 `leading-*` call sites and 176 arbitrary `max-w-[…]` across this repository against nothing in the token layer to point them at.
8
+
9
+ They are declared once, on `:root`, and they are not part of the geometry contract: a measure is a property of a page rather than of how tightly its controls are packed, so it does not get a rung per density, and no theme package has to author one. Override them in your own `:root` or under your own block and every component that spends them follows.
10
+
11
+ The build's per-group token contract is now exact in both directions rather than a floor: a group whose sources emit a name its contract does not list fails the build, the same way a missing name already did. The style group needs it most — it is deliberately outside `dist/base.json` and outside the geometry contract, so nothing downstream would have noticed a seventh `--cue-measure-*` arriving from a source-file edit — and the geometry groups satisfy it as they stand, so it is one rule for both. No emitted CSS changes.
12
+
13
+ The build also discovers its style sources now, rather than opening `leading.tokens.json` and `measure.tokens.json` by name: every `src/style/*.tokens.json` on disk is read. A new style source is picked up the moment it lands, and the exact-contract rule above is what refuses a name that source was not planned to declare — the fail-closed shape every other source group on this branch already has. No emitted CSS changes here either: today's directory holds exactly the two files the build used to name.
14
+
15
+ - 5b40f07: **A fourth composable axis: `data-style`, for page composition.**
16
+
17
+ Theme, density and font already answer "what colour", "how tight" and "which
18
+ typeface". `data-style` answers the question none of them could: how a _page_ is
19
+ composed — reading measure, prose line height, and the rhythm of a band. Three
20
+ sets ship. `console` is the current look made explicit and provably unchanged,
21
+ `editorial` widens the measure and opens the leading for long-form reading, and
22
+ `marketing` narrows the measure and spends the vertical space a landing page
23
+ wants.
24
+
25
+ Each set declares `--cue-measure-prose`, `--cue-leading-prose`, `--cue-band-y`,
26
+ `--cue-band-rule` and `--cue-band-ground`; `editorial` and `marketing` add
27
+ `--cue-style-display` for a display type scale. Only the display rung composes
28
+ with `--cue-font-scale`, and a name added to the set vocabulary without an
29
+ answer is a build error rather than a silent guess.
30
+
31
+ **Driving it.** `<ThemeProvider styleSet="editorial">` stamps the attribute on
32
+ its own element and on `<html>`, `useTheme().styleSet` and `setStyleSet()` read
33
+ and drive it, and it persists beside theme, density, font and mode under a
34
+ `style` key — written only when there is a set. A pre-paint script applies a
35
+ stored set before the first paint, so turning one on _or off_ lands on the first
36
+ frame rather than the first commit. The set is carried into overlay portals,
37
+ `<Density styleSet>` islands and `<Preview styleSet>` stages, so an overlay
38
+ follows the subtree it was opened from and three sets can be shown side by side
39
+ on one page.
40
+
41
+ `useTheme().styleSet` is **the set in force, not the set named here**. A
42
+ provider that names no set of its own reports the one stamped around it —
43
+ including a `<Density styleSet>` island sitting between it and an outer provider
44
+ — exactly as the DOM resolves it. That is deliberate: it makes the context value
45
+ and the set a portal or a stage carries out of that DOM one value rather than
46
+ two that can disagree. `null` means nothing anywhere has chosen a set, which is
47
+ not the same fact as `console`.
48
+
49
+ `setStyleSet(null)` is the same distinction from the writing side: it clears the
50
+ set this provider **names**, not the set that applies to it. A nested provider
51
+ that clears its own goes back to inheriting whatever is stamped around it, and
52
+ `styleSet` reports that inherited set, because it is what the cascade resolves
53
+ to. At the root the two coincide, which is why only a nested provider shows the
54
+ difference. To positively leave an inherited set, pass `DEFAULT_STYLE_SET`: it
55
+ stamps an attribute whose block restates the `:root` defaults token for token,
56
+ and an attribute is the only thing that out-specifies an ancestor's.
57
+
58
+ Restating a set on a `<Density>` island is what re-resolves its band rhythm at
59
+ that island's rung: `--cue-band-y` is built from `--cue-space-7`, and a custom
60
+ property's `var()` references are substituted where they are declared, so an
61
+ island that only changes density inherits the rhythm already resolved above it.
62
+
63
+ `STYLE_SETS`, `DEFAULT_STYLE_SET`, `STYLE_CONTRACT`, `STYLE_SET_CONTRACT` and
64
+ the `StyleSet` type are exported from `@cueplusplus/tokens`, so an application
65
+ can type a prop and validate a stored preference against the same closed list
66
+ the stylesheet was built from.
67
+
68
+ **What it costs.**
69
+
70
+ - **Every consumer carries 144 more bytes of blocking inline pre-paint script**,
71
+ whether or not the application uses the axis — a typical script grows from 995
72
+ bytes to 1139. It has to read the stored set and stamp or clear the attribute
73
+ before the first paint, and an application that never touches the axis cannot
74
+ opt out of the branch that decides there is nothing to stamp.
75
+ - **The attribute is the generic `data-style`, not `data-cue-style`.** An
76
+ application already using `data-style` on `<html>` for its own purposes will
77
+ have it overwritten while a `<ThemeProvider>` is mounted, and cleared when the
78
+ provider holds no set; the original value is restored on unmount. The generic
79
+ name was chosen for readability, and this collision is the price. If you use
80
+ `data-style` on `<html>` yourself, move it before upgrading.
81
+
82
+ **What is unchanged.** A page that names no set is untouched: no attribute is
83
+ stamped anywhere, no stored preference gains a key, and a stored `"style": null`
84
+ is refused and scrubbed on the next load like every other value nothing
85
+ declares. `console`'s declarations are asserted at build time to be exactly the
86
+ `:root` defaults, and a browser test compares every `--cue-*` on two identical
87
+ subtrees — one bare, one under `console` — and requires them to compute
88
+ identically. Switching to the default set changes nothing, as a checked fact
89
+ rather than a promise.
90
+
91
+ One note on `editorial`'s hairline: its 0.5px `--cue-band-rule` paints a
92
+ measured 1px on Chromium at either device pixel ratio and 0.5px only on retina
93
+ WebKit, so the separation it actually ships is the doubled `--cue-space-7`
94
+ rhythm with a hairline under it.
95
+
96
+ ### Patch Changes
97
+
98
+ - b1c2f25: **Every package now ships the licence it declares.** All sixteen have said `"license": "MIT"` in
99
+ their manifest since they were first published, and none of them carried the text. Installing one
100
+ gave you a licence field and nothing to read; anyone auditing their dependency tree found a package
101
+ that claimed a licence it did not include. Each package root now holds the MIT notice, and npm packs
102
+ a root `LICENSE` whether or not `files` lists it, so it is in the tarball you install.
103
+
104
+ `@cueplusplus/ui` additionally ships `NOTICE.md`, the third-party notice for the assistant-ui code
105
+ vendored into its elements. That one is not automatic — `files` had to name it — and it is byte-for-byte
106
+ the notice that already sat beside the source it covers, because a notice that travels without the
107
+ bytes it describes is not a notice.
108
+
109
+ Nothing else about these packages changed: same exports, same versions of every dependency, same
110
+ registry. If you are upgrading only for this, the diff you receive is licence text.
111
+
112
+ ## 0.12.0
113
+
114
+ ## 0.11.1
115
+
116
+ ## 0.11.0
117
+
118
+ ## 0.10.0
119
+
120
+ ### Patch Changes
121
+
122
+ - 64b4bab: Every package manifest's `repository` field now names `cueplusplus/cue-ui`, following the
123
+ repository's rename from an earlier working title.
124
+
125
+ **Nothing to do.** No code, no exports, no types, and no package name: the scope is the
126
+ organisation, not the repository, so every specifier you already import is the one you keep
127
+ importing. What moves is metadata — GitHub Packages matches a package to a repository through that
128
+ field, so from this release the "source" link on each package's registry page, and the repository
129
+ the next publish is attributed to, name `cue-ui`. The `directory` beside it is unchanged, so each
130
+ link still lands on that package rather than on the monorepo root.
131
+
132
+ If you have a bookmark or a script pointing at the old URL, GitHub's rename redirect answers it;
133
+ updating it to `https://github.com/cueplusplus/cue-ui` is tidiness rather than repair.
134
+
135
+ - c2238cc: The README now states the removal window for `dist/theme.css`, the deprecated alias of
136
+ `axes.css`: it is kept through 0.10 and removed in the minor after. Nothing in the tarball
137
+ changes; if you still import `@cueplusplus/tokens/theme.css`, switch to `axes.css` before then.
138
+
139
+ ## 0.9.0
140
+
141
+ ### Minor Changes
142
+
143
+ - 5eb0da2: The ten colour presets leave `@cueplusplus/tokens` and become packages of their
144
+ own. `@cueplusplus/ui/styles.css` now ships the blank base instead of a palette,
145
+ so a theme is something you install, import and register — the same way a
146
+ third-party theme always should have been, and now is.
147
+
148
+ `@cueplusplus/tokens` keeps the two axes it actually owns: the five density
149
+ rungs and the eight font pairings, plus the contracts, `base.json` and the
150
+ primitives a theme source aliases. It emits `axes.css` and no colour at all.
151
+ Each preset is `@cueplusplus/theme-<name>` — `cue`, `dusk`, `hivehub`, `luma`,
152
+ `quotamate`, `requestport`, `signal`, `snuffle`, `terminal`, `venu` — carrying
153
+ the stylesheet it always carried, byte for byte: nothing about any shipped
154
+ palette moved, and a frozen snapshot of what `tokens@0.8.0` emitted is committed
155
+ so that stays checkable rather than merely asserted.
156
+
157
+ ## Migrating
158
+
159
+ **The unattributed default stops being `cue`.** An app that imports
160
+ `@cueplusplus/ui/styles.css` and registers nothing now paints the blank base.
161
+ Nothing errors and nothing falls back: a `data-theme` value is styled by
162
+ whichever stylesheet declares it, and with no theme package imported, none does.
163
+
164
+ Ten custom properties move in dark and seven in light. Measured on a root
165
+ stamped `data-theme="cue"`, which is what every `ThemeProvider` stamped before
166
+ this release and still stamps, so this is the delta for an app that changes
167
+ nothing:
168
+
169
+ | token | dark: `cue` → blank base | light: `cue` → blank base |
170
+ | ---------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------ |
171
+ | `--cue-accent` | `#ffffff` → `#8a93a6` | `#0a0a0a` → `#3f4759` |
172
+ | `--cue-accent-hover` | `rgba(255,255,255,0.85)` → `#9aa3b6` | `rgba(0,0,0,0.85)` → `#333a4a` |
173
+ | `--cue-accent-soft` | follows `--cue-accent`: it is a 14% `color-mix` of it | same |
174
+ | `--cue-focus` | `#ffffff` → `#8a93a6` | `#0a0a0a` → `#3f4759` |
175
+ | `--cue-font-theme-mono` | JetBrains Mono → nothing; the base declares no theme mono | same as dark |
176
+ | `--cue-font-mono` | resolves through the line above, so JetBrains Mono → the platform stack | same as dark |
177
+ | `--cue-fg` | `#f5f5f7` → `#f5f5f5` | unchanged |
178
+ | `--cue-hair`, `--cue-hair-strong`, `--cue-row-hover` | follow `--cue-fg`: each is a `color-mix` of it (6%, 12%, 3.5%) | unchanged |
179
+ | `--cue-accent-fg` | unchanged | `#fcfcfc` → `#ffffff` |
180
+
181
+ The last four rows are the ones an eye catches and a list of "the accent and the
182
+ focus ring" would not have mentioned: four of the ten dark changes and the one
183
+ light change are tokens no theme file names directly, because they are mixes of
184
+ two that do.
185
+
186
+ To get `cue` back, exactly as it was:
187
+
188
+ ```bash
189
+ pnpm add @cueplusplus/theme-cue
190
+ ```
191
+
192
+ ```css
193
+ @import "@cueplusplus/ui/styles.css";
194
+ @import "@cueplusplus/theme-cue/theme.css";
195
+ ```
196
+
197
+ ```tsx
198
+ import cue from "@cueplusplus/theme-cue";
199
+
200
+ <ThemeProvider themes={[cue]} theme="cue">
201
+ {children}
202
+ </ThemeProvider>;
203
+ ```
204
+
205
+ The import order is load-bearing: a `[data-theme]` block and the base's bare
206
+ `:root` tie at (0,1,0), so the one declared later wins. Importing the theme
207
+ before `styles.css` paints the blank base over it. Importing the package from
208
+ its root, rather than the `manifest.json` subpath, also closes `ThemeName` to
209
+ the themes you installed, so a misspelt `theme=` is a type error.
210
+
211
+ Then the rest:
212
+
213
+ - **`@cueplusplus/tokens/theme.css` is a deprecated alias** of `axes.css` for
214
+ one minor. It still resolves and still delivers the geometry and type axes, so
215
+ a stylesheet naming it keeps building — it simply carries no palette. Change
216
+ the line to `@cueplusplus/tokens/axes.css`, or drop it: `ui/styles.css` already
217
+ brings the axes in through `@cueplusplus/theme-base/base.css`.
218
+ - **Gone from `@cueplusplus/tokens`:** the `themes/*.css` and
219
+ `registry/theme-*.json` subpaths, and the `THEMES`, `ThemeName`,
220
+ `THEME_SUPPORTS_LIGHT` and `DEFAULT_THEME` exports. `ThemeName` now comes from
221
+ `@cueplusplus/theme-base` (re-exported by `@cueplusplus/ui`) and widens with
222
+ each theme package installed, rather than naming the ten that happened to be
223
+ compiled in; whether a theme ships a light block is `supportsLight` on its own
224
+ `manifest.json`; and there is no default palette to name, because the library
225
+ contains none.
226
+ - **`@cueplusplus/tokens`' root entry is now `dist/tokens.js`**, with
227
+ `dist/tokens.d.ts` beside it, where it used to be the TypeScript source
228
+ `dist/tokens.ts`. The import specifier, the exported names and their values
229
+ are unchanged — `import { DENSITIES } from "@cueplusplus/tokens"` is the same
230
+ line it was — but the file behind it is one Node can execute. It could not
231
+ before: Node refuses to strip types for anything under `node_modules`, so the
232
+ package worked in a workspace and failed from a registry install, taking
233
+ `@cueplusplus/theme-base` and the `cue-theme` CLI with it. If you added
234
+ `@cueplusplus/tokens` to your bundler's transpile list to work around that
235
+ (Next.js' `transpilePackages`, or the equivalent), take it out.
236
+ - **`@cueplusplus/tokens` gains `base.json`'s `fonts.handshake`**: the three
237
+ literals of the monospace resolution (`font-pairing-mono`, `font-theme-mono`,
238
+ and the `var()` that resolves them), published as data so a theme built
239
+ anywhere spells them the same way this build does.
240
+ - **`@cueplusplus/ui`'s `THEME_NAME_PATTERN` and `RESOLVED_MONO` are now
241
+ re-exports** of `@cueplusplus/theme-base`'s. Same names, same values, one
242
+ definition instead of three.
243
+ - **`/r/tokens.json`'s `themes` are objects**, not strings:
244
+ `{ name, package, supportsLight, densities }`. A consumer reading the registry
245
+ for a theme list wants to know which package delivers it.
246
+ - **Every published tarball now carries a `README.md`.** On a registry the
247
+ package page _is_ the README, and `tokens`, `ui` and `brand-tokens` shipped
248
+ without one; `packages/release/test/publishable-packages.test.mjs` holds all
249
+ fifteen to it now, beside the same check for `CHANGELOG.md`.
250
+ - **`@cueplusplus/ui` depends on `@cueplusplus/theme-base@^1`**, a range rather
251
+ than a pin, deliberately: two copies of `theme-base` in one tree would break
252
+ the type registry, because a theme's `declare module` augments one copy while
253
+ `ui`'s re-exported `ThemeName` resolves against the other, and neither errors.
254
+
255
+ - 2737aec: `@cueplusplus/tokens` exports the contracts and the axes as data, so a theme
256
+ built outside this repository can resolve over the same base: `COLOR_CONTRACT`,
257
+ `GEOMETRY_CONTRACT` and `FONT_TOKENS` as typed tuples, `base.json` (every
258
+ density's geometry, the pairings' stacks, the derived templates, the defaults),
259
+ `primitives.tokens.json` (the tier-1 file a theme source aliases), and
260
+ `axes.css` — `theme.css` with the theme axis removed. Nothing that exists today
261
+ moves; `theme.css` is unchanged.
262
+
263
+ `@cueplusplus/ui/theming` keeps every export it had. The contrast arithmetic now
264
+ lives in `@cueplusplus/theme-base` and is re-exported here; `ui` gains that
265
+ package as a dependency. No behaviour changes.
266
+
267
+ ### Patch Changes
268
+
269
+ - cf80a87: No published behaviour changes. These packages gain a `lint` script and its two
270
+ dev dependencies, because `pnpm lint` now runs ESLint over every package that
271
+ holds source rather than resolving to an empty task graph, and a package with no
272
+ script is a package the gate is silently green about.
273
+
274
+ ## 0.8.0
275
+
276
+ ## 0.7.0
277
+
278
+ ## 0.6.0
279
+
280
+ ### Patch Changes
281
+
282
+ - 778374d: The keyboard focus ring is now visible in the luma, venu and hivehub light
283
+ themes. Each set `--cue-focus` to its brand accent, and on paper that accent
284
+ read as low as 2.45:1 against the recessed fill a focused field paints it
285
+ over — under the 3:1 WCAG 1.4.11 asks of a focus indicator. Each light block
286
+ now carries its own focus colour: the same accent hue, stepped down in
287
+ lightness until it clears every surface a field can sit on. `--cue-accent` is
288
+ unchanged, so no button, chip or link moves.
289
+
290
+ ## 0.5.0
291
+
292
+ ### Minor Changes
293
+
294
+ - 51e5046: ## The amber carries its own ink too
295
+
296
+ This is the amber half of the argument `the-red-carries-its-own-ink` already
297
+ made, and it is the same argument: the tone a control is _filled_ with decides
298
+ what can be read on it, and no other token in the palette knows that.
299
+
300
+ `TwoStepButton` is the one control in the library whose whole reason for
301
+ existing is that the press is consequential — arm, then confirm. Its `warn`
302
+ confirm painted `bg-warn text-black`. Black on an amber is a good guess, and on
303
+ the ten dark blocks it is right by a distance: 7.11:1 to 12.41:1. It is wrong on
304
+ the light ones, because a light theme darkens its amber to clear the paper and a
305
+ darkened amber wants paper back. **3.58:1** on requestport, **3.74:1** on dusk,
306
+ **4.22:1** on cue's own light block — three of the twenty preset x mode blocks
307
+ under AA, on the half of the control that does the thing.
308
+
309
+ It could not be closed before: there was no `--cue-warn-fg` to close it with,
310
+ and the queued note for the red said so in as many words. There is one now.
311
+
312
+ ### `--cue-warn-fg`
313
+
314
+ A new authored anchor, per preset, per mode, in the colour contract beside the
315
+ tone it is chosen against, with `text-warn-fg` / `bg-warn-fg` bridged into
316
+ Tailwind like every other colour token:
317
+
318
+ ```css
319
+ [data-theme="cue"] {
320
+ --cue-warn: oklch(0.8 0.17 75);
321
+ --cue-warn-fg: #000000;
322
+ }
323
+ [data-theme="cue"][data-mode="light"] {
324
+ --cue-warn: oklch(0.55 0.115 70);
325
+ --cue-warn-fg: #fcfcfc;
326
+ }
327
+ ```
328
+
329
+ Not one value is hand-picked. Each is the same `pickInk` `createTheme()` already
330
+ uses for `accent-fg` and `danger-fg`, run over the extremes the theme itself
331
+ owns — its page, its deepest well, its type — falling to pure black or white
332
+ only where none of the three reaches AA. Run over the reds instead, it
333
+ reproduces all twenty shipped `danger-fg` values exactly, spelling included,
334
+ which is the check that this is the same derivation rather than a second one
335
+ wearing its name.
336
+
337
+ The amber does not answer the way the red did, and that is the argument for
338
+ deriving against each tone instead of writing one rule about ink on a status
339
+ colour. All ten dark blocks take their deepest ground. The light ten split three
340
+ ways: cue, dusk and requestport take their paper; hivehub, luma, signal and
341
+ terminal take the theme's own type; quotamate, snuffle and venu take plain black
342
+ because nothing they own reaches AA on their amber. The worst amber pair in the
343
+ system is **4.77:1** (luma, light). The worst pair anywhere is still the red's
344
+ 4.59:1.
345
+
346
+ `CONTRAST_REQUIREMENTS` gains an eleventh pair, `warn-fg/warn` at 4.5:1, so a
347
+ generated palette is judged on it too, and `createTheme()` derives it for one.
348
+ The configurator shows it beside the amber in the Status group. The assistant-ui
349
+ island declares its own, because that block re-tints `--cue-warn` and an ink
350
+ chosen against the app's amber is an ink chosen against a colour nobody is
351
+ looking at in there.
352
+
353
+ ### The sweep covers the amber now
354
+
355
+ `test/destructive-ink.test.ts` reads the source and measures what the components
356
+ _spend_ rather than what the palette _offers_. It swept the red and said in its
357
+ own prose that it did not sweep the amber, because sweeping a defect nobody
358
+ could fix turns a missing token into a red suite instead of into a decision. The
359
+ token exists, so the amber is in: every class string that makes `bg-warn` a
360
+ ground and puts an ink on it, every rule in the package's own stylesheets that
361
+ does the same, measured on all twenty blocks at 4.5:1, with the fills pinned by
362
+ name per tone so a new one has to choose its ink in the commit that paints it. A
363
+ tint keeps its exemption — `bg-warn/10 text-warn` and the search highlight's
364
+ `bg-warn/35 text-fg` are read against the page, and the report already owns
365
+ those pairs — and the file says out loud what that exemption does not cover.
366
+
367
+ One number beside it got more honest. Both that sweep and
368
+ `everywhere-the-red-is-spent` said the right ink "falls to 3.95:1 on quotamate's
369
+ dark" under `bg-danger/90` without naming the ground the alpha let through. It
370
+ is **3.94:1** over `--cue-sunken` and **3.97:1** over `--cue-bg`. That the two
371
+ grounds disagree at all is the reason an alpha is refused rather than measured.
372
+
373
+ ### And the amber's pair is swept on its own, the way the red's is
374
+
375
+ `destructive-ink.test.ts` covers `warn-fg` on `warn` across all twenty blocks
376
+ for exactly as long as `TwoStepButton` keeps painting that string, because it
377
+ measures the call site. The red does not depend on that: it has a second,
378
+ unconditional sweep of the token pair beside `RevertDock` in
379
+ `elements/canon.test.tsx`. The amber had no equivalent, and the gap is not
380
+ theoretical — with the confirm moved off the amber in the way a later refactor
381
+ legitimately might, and that file's pinned list updated to match, a `warn-fg`
382
+ re-tinted down to **1.62:1** passed every contrast guard in the package.
383
+
384
+ So `instruments/two-step-button.test.tsx` gains the sweep the red has, asking
385
+ nothing about what any component currently paints, plus the spot check that says
386
+ this confirm takes the amber at full strength in the amber's own ink rather than
387
+ the `text-black` it used to.
388
+
389
+ ### If you ship your own theme block
390
+
391
+ A hand-written `[data-theme="acme"]` block should declare `--cue-warn-fg`
392
+ alongside `--cue-warn`, the same way it now declares `--cue-danger-fg` alongside
393
+ `--cue-danger`. Without it, `text-warn-fg` resolves to nothing on your theme.
394
+ Themes generated by `createTheme()` and the configurator's exports already carry
395
+ it.
396
+
397
+ - 06eb4ac: ## The flagship answers in colour
398
+
399
+ `cue` is a monochrome brand, and until this release it carried the monochrome
400
+ all the way through to the one vocabulary that exists to be told apart at a
401
+ glance. On the default theme `--cue-ok`, `--cue-busy`, `--cue-warn` and
402
+ `--cue-info` were all `#ffffff` on dark and all `#0a0a0a` on light: four
403
+ different meanings, one colour, distinguishable only by the glyph or the word
404
+ beside them. It read as restraint on a settings page. It reads as a defect on a
405
+ transcript where three tool calls are in three different states and the only
406
+ question is which of them is still moving.
407
+
408
+ They are chromatic now.
409
+
410
+ | token | before (dark / light) | after (dark / light) |
411
+ | -------------- | --------------------- | ----------------------------------------------- |
412
+ | `--cue-ok` | `#ffffff` / `#0a0a0a` | `oklch(0.72 0.17 149)` / `oklch(0.53 0.14 149)` |
413
+ | `--cue-busy` | `#ffffff` / `#0a0a0a` | `oklch(0.78 0.13 220)` / `oklch(0.54 0.1 225)` |
414
+ | `--cue-warn` | `#ffffff` / `#0a0a0a` | `oklch(0.8 0.17 75)` / `oklch(0.55 0.115 70)` |
415
+ | `--cue-info` | `#ffffff` / `#0a0a0a` | `oklch(0.75 0.12 252)` / `oklch(0.55 0.16 258)` |
416
+ | `--cue-danger` | `#ff5f57` / `#d03030` | unchanged |
417
+
418
+ `ok` and `warn` are assistant-ui's own semantic pair; `busy` is a console cyan
419
+ for in-flight and `info` a calmer blue for news. `danger` does not move — it was
420
+ the one tone already chromatic, and it is the one nobody should have to
421
+ re-learn.
422
+
423
+ The light row is darker than these hues usually run, and that is the point: a
424
+ tone is not only a dot here. `Chip`, `StatusDot`, `Toast` and the rest of the
425
+ tone family draw `text-<tone>` at 11px uppercase — text, judged at 4.5:1, not at
426
+ the 3:1 a signal is held to. Every light value above clears 4.5:1 on `--cue-bg`
427
+ (4.79 to 4.85, beside `danger`'s 4.94) and every one of them is inside sRGB, so
428
+ the measured ratio is the painted one. Dark was never in question: it runs 8.5
429
+ to 10.3:1.
430
+
431
+ The chrome is untouched. Ink, ground, surfaces, borders, `accent`, `focus` and
432
+ `selection` are the same monochrome they have always been, on both modes. This
433
+ change is the status vocabulary and the live hue below it, and nothing else.
434
+
435
+ The other nine presets do not move at all: `terminal`, `signal`, `venu`,
436
+ `hivehub`, `dusk`, `luma`, `snuffle`, `quotamate` and `requestport` were already
437
+ chromatic here, and an app on any of them sees nothing new in this section.
438
+
439
+ ### Migrating
440
+
441
+ **A console on the default theme that relied on monochrome statuses will now
442
+ see colour.** No API moved and nothing needs editing to keep working, but
443
+ pixels change, and they change in more places than a status pill: `Chip`,
444
+ `StatusDot`, `Meter`, `Toast`, `Stat`, `StatusBar`, `LogViewer`, `Sparkline`,
445
+ `GroupBar`, `TwoStepButton`, `Tree`, `TerminalFrame`, `AppWindowFrame`,
446
+ `UsageChart`, `FileUpload`, `EnvVarInput`, `AskBox`, `DelegationCard` and
447
+ `ToolCallCard` all spend these four tokens, along with the MIDI and DMX
448
+ instruments. Anywhere a `tone` prop was chosen and then quietly rendered white,
449
+ it now renders the tone. Screenshot tests on `data-theme="cue"` will need new
450
+ baselines; a design that used `tone="info"` as a way of saying "plain" will now
451
+ say "blue".
452
+
453
+ If a product deliberately wants the monochrome back, it is four declarations —
454
+ the soft fill and the rim derive from the base with `color-mix`, so overriding
455
+ the base moves the whole family with it:
456
+
457
+ ```css
458
+ @import "tailwindcss";
459
+ @import "@cueplusplus/ui/styles.css";
460
+
461
+ [data-theme="cue"] {
462
+ --cue-ok: #ffffff;
463
+ --cue-busy: #ffffff;
464
+ --cue-warn: #ffffff;
465
+ --cue-info: #ffffff;
466
+ }
467
+ [data-theme="cue"][data-mode="light"] {
468
+ --cue-ok: #0a0a0a;
469
+ --cue-busy: #0a0a0a;
470
+ --cue-warn: #0a0a0a;
471
+ --cue-info: #0a0a0a;
472
+ }
473
+ ```
474
+
475
+ Two selectors, not one, and the block goes _after_ the import — the preset's own
476
+ rules are `[data-theme="cue"]` and `[data-theme="cue"][data-mode="light"]`, so
477
+ an override has to match their specificity and win on source order. An app that
478
+ runs `mode="system"` mirrors these onto
479
+ `[data-theme="cue"][data-mode="system"]` inside the two `prefers-color-scheme`
480
+ queries, exactly as the generated sheet does.
481
+
482
+ ## `--cue-stream`, the live hue
483
+
484
+ The colour contract gains one token and its two derivations —
485
+ `--cue-stream`, `--cue-stream-soft` and `--cue-stream-border` — on all ten
486
+ presets, in both modes, at the same 12% and 35% mixes the statuses use. The
487
+ colour contract is 42 custom properties where it was 39.
488
+
489
+ It is the colour of something still arriving: a token mid-stream, a tool still
490
+ running, a cursor that has not stopped. It is deliberately **not** a sixth
491
+ status. A status is something a `Chip` or a `StatusDot` can be _told_ to be, and
492
+ "streaming" is not a tone a caller picks — it is a property of a surface while
493
+ the model is talking. There is no `tone="stream"` and there is not going to be
494
+ one. What it shares with the statuses is the shape of its family, which is why
495
+ it is derived beside them and bridged beside them:
496
+
497
+ ```
498
+ --color-stream → text-stream bg-stream/15 border-stream/30
499
+ ```
500
+
501
+ The bridge carries the tone at full strength and lets Tailwind's alpha modifier
502
+ do the washes, the way `Chip` already writes `bg-busy/10`.
503
+
504
+ The flagship pair is assistant-ui's `--aui-live` — `blue-400` on ink verbatim,
505
+ and on paper `blue-500`'s hue taken down to `oklch(0.55 0.21 259.815)`, because
506
+ the elements draw the live hue as text and `blue-500` itself lands at 3.6:1
507
+ there. Every other preset answers in its own blue, except `terminal`, which
508
+ already owned a live hue and keeps it.
509
+
510
+ Three things that hold a second copy of the contract moved with it, so nothing
511
+ needs to be told about the new token twice: `createTheme()` resolves and derives
512
+ `stream` like any other tone and darkens an invented light one to the non-text
513
+ AA floor; the contrast report checks `stream/bg` against that same floor — every
514
+ preset clears it with room to spare, the tightest being `terminal`'s light block
515
+ at 4.89:1; and the configurator edits and exports it beside the statuses rather
516
+ than in the miscellany. A theme somebody generated before this release and pasted into their
517
+ own CSS keeps working: `stream` falls back through the same resolution order
518
+ every other invented token does.
519
+
520
+ ## `@cueplusplus/ui/elements`
521
+
522
+ A new group, and the largest one this library has added at once: the agent's own
523
+ surface. Fifteen elements, plus the island and the two label primitives the
524
+ family is built out of, at `/docs/components/elements`.
525
+
526
+ | element | what it is |
527
+ | ----------------- | ----------------------------------------------------------- |
528
+ | `ToolCall` | one invocation, request and result behind a disclosure |
529
+ | `ToolGroup` | calls that went out together, collapsed to one row |
530
+ | `ToolError` | one call failed, with the error legible |
531
+ | `TerminalBlock` | output streaming line by line, ending in an exit status |
532
+ | `CodeDiff` | a unified diff, tinted, sized for a chat column |
533
+ | `ReviewableDiff` | the same diff where each hunk is a decision |
534
+ | `FileTree` | everything a run touched, with the churn per file |
535
+ | `ApprovalCard` | the agent asking before it does something with side effects |
536
+ | `PermissionGrant` | granting a capability rather than approving an action |
537
+ | `AgentPlan` | a checklist the agent works through |
538
+ | `TodoList` | the agent's working list, rewritten mid-run |
539
+ | `AgentStatus` | one pill: what it is doing, and for how long |
540
+ | `SubagentList` | parallel workers, their models and their progress |
541
+ | `TraceWaterfall` | every span in a run on one nested time axis |
542
+ | `ChatPanel` | the whole family working together, with its five parts |
543
+
544
+ Beside them: `ShimmerLabel` and `SwapLabel`, the eighteen surface recipes the
545
+ family shares as plain class strings (`paper`, `floating`, `field`, `mono`,
546
+ `live`, `codeSurface`, `collapsePanel`, `pressable`, the swap pairs and the
547
+ rest), and `range`'s six functions — `at`, `clamp`, `indexIn`, `pct`,
548
+ `progressOf`, `take` — which are the arithmetic a replayed transcript is made
549
+ of.
550
+
551
+ Two conventions run through the group and both are worth knowing before you read
552
+ the source:
553
+
554
+ **State is a prop, and time is the caller's.** Nothing here holds a run.
555
+ `ToolCall` is _told_ `running` and `open`; `TerminalBlock` is told how many of
556
+ its lines are visible. Replaying a conversation is arithmetic on props, which is
557
+ what makes every one of these benchable at every state.
558
+
559
+ **These components are written upstream's way, not this library's.** Plain
560
+ exported functions with no `forwardRef`, a `data-slot` attribute on every root,
561
+ `className` merged last through `cn`. That is deliberate rather than sloppy: it
562
+ keeps a re-synced diff readable, and `data-slot` is what lets an app restyle a
563
+ part from the outside without a prop for it. `AgentSurface` — cue's own — is the
564
+ one component in the group written in the house style.
565
+
566
+ `lucide-react` is an **optional peer** of this entry, which is why the group is
567
+ its own subpath instead of part of the root barrel: the family draws its own
568
+ icons rather than taking them from a prop. Import nothing from `/elements` and
569
+ it costs nothing — no install, no bundle, no resolution error.
570
+
571
+ `@cueplusplus/ui/elements.css` ships beside it and is **optional even for this
572
+ subpath**, because the family is painted by the token layer already:
573
+
574
+ ```css
575
+ @import "tailwindcss";
576
+ @import "@cueplusplus/ui/styles.css";
577
+ @import "@cueplusplus/ui/elements.css"; /* motion, and the supersede island */
578
+ ```
579
+
580
+ It carries the two vocabularies upstream never shipped as anything but
581
+ dependencies — the shimmer sweep and the nine entrance utilities, both stilled
582
+ under `prefers-reduced-motion` — and the island values below. Skip it and the
583
+ elements still render in cue's palette; you lose the motion and `skin="aui"`
584
+ becomes a no-op.
585
+
586
+ **Every class it declares is namespaced `cue-elements-`**, and that is a
587
+ compatibility promise rather than a house style. The entrance utilities are
588
+ `tw-animate-css`'s by name — `animate-in`, `fade-in`, `slide-in-from-*` — and
589
+ `tw-animate-css` is what shadcn installs by default, so a stylesheet declaring
590
+ them again would give such an app two `.animate-in` rules with import order
591
+ picking the winner, silently. The vendoring codemod renames them at emit, the
592
+ lockfile records the table as `classRenames`, and a test fails on any class in
593
+ this stylesheet that a third-party utility package could also own.
594
+
595
+ ## The island: one DOM, two value-sets
596
+
597
+ ```tsx
598
+ import { AgentSurface, ToolCall } from "@cueplusplus/ui/elements";
599
+
600
+ <AgentSurface skin="aui" fidelity="upstream">
601
+ <ToolCall
602
+ label="Searched"
603
+ activeLabel="Searching"
604
+ query="composer"
605
+ request={request}
606
+ result={result}
607
+ running
608
+ open={open}
609
+ onOpenChange={setOpen}
610
+ />
611
+ </AgentSurface>;
612
+ ```
613
+
614
+ `AgentSurface` renders a plain `div` and stamps `data-cue-skin` and
615
+ `data-cue-fidelity` on it. `elements.css` re-declares, under those attributes,
616
+ the `--cue-*` tokens the family reads, and custom properties inherit — so the
617
+ whole subtree repaints with no prop threading and no branch inside any
618
+ component.
619
+
620
+ The two axes are independent, which is the switchable fidelity the spec asked
621
+ for:
622
+
623
+ - **`skin`** — `cue` (the default) or `aui`, which installs assistant-ui's four
624
+ shadcn variables and four literal hues as cue tokens, in light and dark.
625
+ - **`fidelity`** — `cue-metrics` (the default) keeps cue's type scale and
626
+ declares exactly one metric of its own:
627
+ `--spacing: calc(0.25rem * var(--cue-density, 1))`. That is the lever that
628
+ carries the density ladder into five hundred-odd vendored geometry utilities,
629
+ none of which names a `--cue-space-*` token. `upstream` pins assistant-ui's
630
+ radii, their 13.5/12/11px type and a flat `--spacing: 0.25rem` instead, so
631
+ **density stops at the island edge** and the subtree keeps upstream's
632
+ proportions whatever the console around it is set to.
633
+
634
+ It is not a theme. `data-theme`, `data-mode` and `data-density` are untouched,
635
+ and a light app gets a light island.
636
+
637
+ Overlays follow. The portal stamp gains both axes, so a popover or dialog opened
638
+ from inside an island — which mounts on `<body>`, far from the attributes —
639
+ paints like the transcript it came from rather than like the page. Outside an
640
+ island the two attributes are _absent_ rather than defaulted, so nothing that
641
+ does not use this pays for it.
642
+
643
+ ## Where this code came from
644
+
645
+ Elements are not a package. `@assistant-ui/ui` is private at `0.0.0` and never
646
+ published; the only channel is a shadcn registry whose index carries no version,
647
+ no date and no hash, rebuilt from upstream `main` on every deploy. Copy-in is
648
+ the intended use — so this release also ships the only thing that can answer
649
+ "which Elements do we have".
650
+
651
+ `scripts/elements-sync/` is pinned to upstream commit
652
+ `31a049fcfa846a76da7c8e2c0bcd62960825dbd4` (resolved 2026-08-22). It fetches
653
+ all 96 `elements-*` registry items, verifies every allowlisted file against
654
+ `raw.githubusercontent.com` at that pin, records every byte it read in
655
+ `elements-sync.lock.json`, and runs a committed codemod that rewrites the fetched
656
+ TSX before it lands: the ink ramp onto cue's four stops, the four literal
657
+ Tailwind hues onto `stream`/`ok`/`danger`/`warn` with their alphas intact,
658
+ literal pixel sizes onto the type scale, radii onto the three rungs, and every
659
+ `dark:` variant resolved away because cue's mode axis is `[data-mode]` rather
660
+ than `prefers-color-scheme`. Geometry is deliberately left alone: it all compiles
661
+ through `--spacing`, which is the island's lever. Upstream's own 679 documented
662
+ prop rows are merged into JSDoc on the way out, so the props table on each page
663
+ is upstream's description of upstream's prop.
664
+
665
+ What that buys a consumer: the elements arrive as source this library owns and
666
+ can fix, not as a dependency that can move underneath you; the "version" you have
667
+ is a SHA you can read in `NOTICE.md` and diff against; and the licence obligation
668
+ travels with the package. Upstream's MIT notice — Copyright (c) 2025 AgentbaseAI
669
+ Inc. — is preserved verbatim in `packages/ui/src/elements/NOTICE.md` and named in
670
+ the group's own documentation.
671
+
672
+ Two consequences worth stating plainly. **The directory is generated.** Every
673
+ file in `src/elements/` except `surfaces.tsx` and `agent-surface.tsx` is output;
674
+ a bug there is fixed by changing a codemod rule and re-running the sync, never by
675
+ editing the file, because the next re-sync deletes a hand edit without telling
676
+ anyone. And **six upstream export names are renamed at emit**, because docgen
677
+ slugs are globally unique and the `Chat` family stays authoritative:
678
+ `Composer → ElementsComposer`, `CommandPalette → ElementsCommandPalette`,
679
+ `DataTable → ElementsDataTable`, `EmptyState → ChatEmptyState`,
680
+ `Timeline → ElementsTimeline`, and `Source → CitationSource` (that one collides
681
+ inside upstream's own family). None of the six is in this release — they land
682
+ with the rest of the catalogue — but the table is generated by the sync, ships in
683
+ the group barrel, and is the answer to "why is the import name not the one on
684
+ assistant-ui's site".
685
+
686
+ ## What did not change
687
+
688
+ Outside the `cue` preset's four status values and its light `stream`, no colour
689
+ moved. No component's
690
+ props, structure or rendering changed, with two additions that are worth naming
691
+ because they are additions rather than nothing: the portal stamp — both
692
+ `useCuePortalProps` and `CuePortalFrame` — now carries `data-cue-skin` and
693
+ `data-cue-fidelity` alongside the four axes it already carried, and both are
694
+ _absent_ outside an island rather than defaulted; and the configurator's
695
+ `TokenEditor` lists `stream` in its Status group, so a generated theme edits it
696
+ where you would look for it.
697
+
698
+ The root barrel is untouched, so an app that never imports `/elements` gets no
699
+ new peer, no new stylesheet and no new bytes, and `tw-shimmer` and
700
+ `tw-animate-css` were reimplemented as CSS in this package rather than taken as
701
+ dependencies. The `Chat` group is unchanged and stays authoritative on every
702
+ name the two families both wanted.
703
+
704
+ Text is still text. Nothing in this group parses model output as markdown or
705
+ HTML; the elements render what they are given, for the same reason `Message`
706
+ does. Sanitized markdown is a documented, opt-in surface arriving later, with its
707
+ own peer and its own entry.
708
+
709
+ `schemaVersion` is still `1`.
710
+
711
+ - 4e510e7: ## The red carries its own ink
712
+
713
+ `RevertDock`'s confirm is the one solid destructive fill in the elements family
714
+ and the one control in it that does something irreversible. It painted
715
+ `bg-danger/90 text-accent-fg`, and both halves of that were wrong.
716
+
717
+ `--cue-accent-fg` is the ink authored against `--cue-accent`. It has no
718
+ relationship to `--cue-danger`, and on the ten reds this system ships it lands
719
+ anywhere between **3.58:1** (venu, dark) and **6.67:1** (requestport, light).
720
+ The `/90` then let the paper through and took roughly another 0.65 off the
721
+ ratio. In cue's own light theme the label on that button measured **4.29:1** at
722
+ the 12px this row is set in — under AA, on the press that cannot be undone.
723
+ Inside an `<AgentSurface skin="aui">` in light mode it was **3.82:1**.
724
+
725
+ There is no existing token that fixes it. Every candidate the palette already
726
+ owns fails somewhere: `accent-fg` fails terminal and venu in dark, `bg` fails
727
+ terminal and hivehub in dark, the theme's own `fg` fails every light block. The
728
+ ink a red can carry is a property of _that red_, not of the mode — so the red
729
+ now has one.
730
+
731
+ ### `--cue-danger-fg`
732
+
733
+ A new authored anchor, per preset, per mode, in the colour contract beside the
734
+ tone it is chosen against:
735
+
736
+ ```css
737
+ [data-theme="cue"] {
738
+ --cue-danger: #ff5f57;
739
+ --cue-danger-fg: #000000;
740
+ }
741
+ [data-theme="cue"][data-mode="light"] {
742
+ --cue-danger: #d03030;
743
+ --cue-danger-fg: #fcfcfc;
744
+ }
745
+ ```
746
+
747
+ with `text-danger-fg` / `bg-danger-fg` bridged into Tailwind like every other
748
+ colour token. `ok`, `busy`, `info` and `stream` have no such ink, and that is
749
+ deliberate: they are read as signals — a dot, a rim, a 12% wash — and the text
750
+ near them sits on a surface. The red is a tone this system fills a control with,
751
+ which is what makes text on it text. (The amber turns out to be the other one —
752
+ _The amber carries its own ink too_, later in this same release.)
753
+
754
+ Each preset's value is the extreme it already owns, measured: every light block
755
+ takes its paper, seven dark blocks take their deepest ground, and the two whose
756
+ red sits in the middle of the range (terminal, hivehub) take white, because
757
+ nothing else reaches AA on it. The worst pair in the system is now **4.59:1**;
758
+ it was **3.58:1**.
759
+
760
+ `createTheme()` derives the same token for a generated theme the way it derives
761
+ `accent-fg` — best of the theme's ground, its deepest well and its ink, with
762
+ pure black or white only when none of the three can be read — and
763
+ `CONTRAST_REQUIREMENTS` gained a tenth pair, `danger-fg/danger` at 4.5:1, so a
764
+ generated palette is judged on it too. The configurator shows it next to the red
765
+ in the Status group.
766
+
767
+ A sweep in `@cueplusplus/ui`'s own suite now measures the shipped pairing across
768
+ all ten presets in both modes and fails under 4.5:1, and a second one measures
769
+ it inside the assistant-ui island, where the two halves legitimately disagree
770
+ about which ink their red wants.
771
+
772
+ ### If you ship your own theme block
773
+
774
+ A hand-written `[data-theme="acme"]` block should declare `--cue-danger-fg`
775
+ alongside `--cue-danger`. Without it, `text-danger-fg` resolves to nothing on
776
+ your theme. Themes generated by `createTheme()` and the configurator's exports
777
+ already carry it.
778
+
779
+ ## 0.4.0
780
+
781
+ ### Minor Changes
782
+
783
+ - 85448d8: ## Three hairlines, derived from the foreground
784
+
785
+ A list is not a stack of boxes, and until now the token vocabulary had no way to
786
+ say the difference.
787
+
788
+ `--cue-border` is an authored anchor: every preset picks one, and what every
789
+ preset picks is a **box edge** — the line around a card, an input, a panel. It is
790
+ the right weight for exactly that. Repeat it down thirty rows of a table and the
791
+ page stops reading as a list and starts reading as thirty boxes stacked on each
792
+ other. The line _between_ things wants to be lighter than the line _around_ a
793
+ thing, and there was no token for it, so every surface that needed one either
794
+ borrowed `--cue-border` and looked heavy, or invented a `color-mix()` of its own
795
+ and stopped being themeable.
796
+
797
+ Three new tokens, in every preset, in both modes:
798
+
799
+ | Token | Value | What it is |
800
+ | ------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
801
+ | `--cue-hair` | `color-mix(in oklch, var(--cue-fg) 6%, transparent)` | **Hairline rule** — between rows, between sections, under a caption |
802
+ | `--cue-hair-strong` | `color-mix(in oklch, var(--cue-fg) 12%, transparent)` | **Strong hairline** — the same line where it carries weight: a table head, a quiet tag's edge |
803
+ | `--cue-row-hover` | `color-mix(in oklch, var(--cue-fg) 3.5%, transparent)` | **Row hover wash** — the faintest fill that still reads as one row picked out of a list |
804
+
805
+ They are mixed from `--cue-fg`, not authored per preset, and that is the whole
806
+ point of them. A share of the foreground keeps its relationship to the type it
807
+ separates whatever the palette turned out to be — the rules lighten with the text
808
+ in a light block, they hold at `terminal`'s cyan-on-black and at `luma`'s warm
809
+ paper, and a preset added tomorrow gets all three for free without authoring a
810
+ thing. Asking ten presets to hand-pick a hairline would have produced ten
811
+ slightly different answers to a question nobody should have to answer.
812
+
813
+ They are emitted **inside** every theme block rather than once on `:root`, like
814
+ every other derived colour here: a custom property's `var()` references are
815
+ substituted at the element that declares it, so a `:root`-level mix would freeze
816
+ the default preset's foreground into every nested theme.
817
+
818
+ ## Spending them
819
+
820
+ They are not Tailwind utilities, on the same ruling as `--cue-accent-soft`'s
821
+ status siblings — arbitrary-property syntax reads the token directly:
822
+
823
+ ```tsx
824
+ <div className="border-t border-(--cue-hair) hover:bg-(--cue-row-hover)" />
825
+ ```
826
+
827
+ ## `--cue-row-hover` does not replace the surface ramp
828
+
829
+ Worth being exact, because there are now two answers to "how does a row show
830
+ hover" and only one of them is new. A row that sits **on a surface** still lifts
831
+ to the next rung — `hover:bg-surface-2` — and that is what this library's own
832
+ rows do: `Sidebar.Item`, `Tree`, `Pagination`, `Toggle`, `NumberField`. None of
833
+ them change, and none of them should.
834
+
835
+ `--cue-row-hover` is for rows drawn **directly on `--cue-bg`**, where the next
836
+ rung of the ramp is a step you can see from across the room and the row you are
837
+ pointing at ends up looking selected rather than hovered. If the ramp fits your
838
+ row, use the ramp.
839
+
840
+ ## `createTheme()` derives them too — and now it cannot forget to
841
+
842
+ `@cueplusplus/ui` carries its own copy of the derivation table, because
843
+ `createTheme()` runs in a browser over a palette somebody just typed while the
844
+ token build runs at publish time over DTCG files under Style Dictionary, and
845
+ neither package can import the other's. So a preset you generate gets the three
846
+ hairlines exactly as a shipped preset does.
847
+
848
+ The copy was the real hazard here, and it was already sitting in the tree: **no
849
+ test compared the two lists.** A derivation added to the build alone would have
850
+ been missing from every theme a consumer generated, and the only symptom would
851
+ have been an unstyled rule in somebody else's app weeks later. There is now a
852
+ test that compares `DERIVED_TOKEN_TEMPLATES` against the compiled stylesheet in
853
+ both directions, so a one-sided edit fails in this repository instead.
854
+
855
+ **Nothing changes for existing consumers.** Both packages gain tokens and neither
856
+ loses or moves one; no component's rendered output differs.
857
+
858
+ ## 0.3.0
859
+
860
+ ### Minor Changes
861
+
862
+ - 31e3607: Ship the linked RequestPort preset release across tokens, UI, and brand tokens.
863
+
864
+ The release adds the typed RequestPort preset and its generated dark, light, and
865
+ system selectors; system SSR, prepaint, provider, and portal behavior; the
866
+ `persistPreferences={false}` and authoritative prepaint options contracts; the
867
+ typed `fontFamilies` provider/portal override; the deterministic package
868
+ manifest and companion checksums; and the accessible `TableScrollRegion`.
869
+
870
+ All three linked tarballs receive the same minor version and changelog entry.
871
+ The Version Packages path regenerates the package manifest after applying those
872
+ versions and rejects stale or nondeterministic output before publication.
873
+
874
+ ## 0.2.0
875
+
876
+ ### Minor Changes
877
+
878
+ - 33a95c4: Typeface is a third axis, beside theme and density.
879
+
880
+ `data-font` selects one of eight font pairings — a sans and a monospace — the
881
+ same way `data-theme` selects a palette and `data-density` selects a geometry.
882
+ `<ThemeProvider font>`, `useTheme().font` / `setFont`, persistence beside the
883
+ other two, the pre-paint script, and every portal root carry it.
884
+
885
+ ```tsx
886
+ <ThemeProvider theme="cue" density="normal" font="plex">
887
+ <App />
888
+ </ThemeProvider>
889
+ ```
890
+
891
+ | id | sans | mono | what your app has to do |
892
+ | -------- | ---------------------- | ------------------- | ---------------------------------- |
893
+ | `system` | the platform's UI sans | **the theme's own** | nothing — this is the default |
894
+ | `geist` | Geist | JetBrains Mono | deliver two faces |
895
+ | `inter` | Inter | IBM Plex Mono | deliver two faces |
896
+ | `plex` | IBM Plex Sans | IBM Plex Mono | deliver two faces |
897
+ | `roboto` | Roboto | Roboto Mono | deliver two faces |
898
+ | `source` | Source Sans 3 | Source Code Pro | deliver two faces |
899
+ | `apple` | SF Pro Text | SF Mono | nothing — and nothing you _may_ do |
900
+ | `office` | Calibri (Carlito) | Cascadia Mono | nothing — and nothing you _may_ do |
901
+
902
+ New exports from `@cueplusplus/tokens`: `FONTS`, `FontName`, `DEFAULT_FONT`,
903
+ `FONT_PAIRINGS`, `FONT_FACES`, `FontDelivery`, `FontPairing`, `faceProperty`.
904
+ `prepaintScript()` takes a fourth argument, the default pairing.
905
+
906
+ ## You have to load the fonts. We will not.
907
+
908
+ **This package ships no font file and no `@font-face`, and that is deliberate.**
909
+ A library that injected one would be choosing your network requests, your
910
+ content-security policy and your font licensing, in a stylesheet you imported
911
+ for its colours. What a pairing emits instead is a hook with the family name as
912
+ its fallback:
913
+
914
+ ```css
915
+ [data-font="inter"] {
916
+ --cue-font-sans: var(--cue-face-inter, Inter), ui-sans-serif, system-ui, …;
917
+ --cue-font-mono:
918
+ var(--cue-face-ibm-plex-mono, "IBM Plex Mono"), ui-monospace, …;
919
+ }
920
+ ```
921
+
922
+ Assign nothing and the stack still resolves — a local install first, then the
923
+ platform — so no pairing ever renders as nothing. Assign the property and
924
+ everything painted from the token follows. With `next/font`, the property name
925
+ _is_ the option:
926
+
927
+ ```tsx
928
+ const inter = Inter({ subsets: ["latin"], display: "swap", preload: false, variable: "--cue-face-inter" });
929
+ <html className={inter.variable}>
930
+ ```
931
+
932
+ `preload: false` matters: with preloading on, a site offering five webfont
933
+ pairings makes every visitor download all of them to render in one.
934
+ `FONT_PAIRINGS[name].faces` is the list of properties one pairing needs;
935
+ `FONT_FACES` is every face at once, as `property → family`.
936
+
937
+ **`apple` and `office` are stacks and must stay stacks.** SF Pro and SF Mono are
938
+ Apple-licensed and may not be served as webfonts; Calibri is Microsoft's and
939
+ ships with Windows and Office rather than with anybody's site (Carlito is its
940
+ metric-compatible libre twin, and is in the stack). They light up where
941
+ installed and fall through cleanly where they are not. Do not bundle them.
942
+
943
+ ## A theme's monospace still applies — until somebody picks a pairing
944
+
945
+ Seven presets name a monospace of their own, and seven pairings name one, so
946
+ one token was claimed twice.
947
+ Neither axis writes it now: a theme publishes `--cue-font-theme-mono`, a pairing
948
+ publishes `--cue-font-pairing-mono`, and `--cue-font-mono` resolves the two,
949
+ pairing first. **A chosen pairing wins**, at whatever element each was stamped
950
+ on — a pairing is what a person picked out of a menu and a preset's mono is what
951
+ its author chose in the absence of a person. The default pairing, `system`,
952
+ declines the question, so an app that never touches the axis keeps exactly the
953
+ monospace it has today.
954
+
955
+ ## Two things that change without you asking
956
+
957
+ **The base sans no longer names `Geist`.** It named a font this package has
958
+ never delivered, so for almost every visitor it resolved to the next entry
959
+ anyway. `--cue-font-sans` and `--cue-font-display` are now the platform stack;
960
+ Geist is the head of the `geist` pairing, which your app can actually deliver.
961
+ If you want it back, deliver it: `font="geist"` plus the two `--cue-face-*`
962
+ assignments.
963
+
964
+ **A generated theme's mono moves with it.** `createTheme({ fonts: { mono } })`
965
+ now writes `--cue-font-theme-mono`. Nothing to change unless you read the
966
+ generated `tokens` map by key.
967
+
968
+ ## Migrating
969
+
970
+ Nothing is required. Two things are worth doing:
971
+
972
+ 1. **Pass the pairing to the pre-paint script**, as its fourth argument, if your
973
+ app's default is not `system`:
974
+ `prepaintScript(DEFAULT_STORAGE_KEY, "cue", "normal", "geist")`. A theme that
975
+ arrives one frame late is a flash of the wrong colour; a _family_ that
976
+ arrives one frame late reflows every line under the reader.
977
+ 2. **If your own CSS overrides `--cue-font-mono`** under a `[data-theme]` block
978
+ of your own, move it to `--cue-font-theme-mono` — otherwise your override
979
+ beats the typeface picker and yours is the one theme it cannot move.
980
+
981
+ `font` is a root-level prop: there is no font island, and a nested provider
982
+ forwards the axis (and `setFont`) to the root. A specimen that genuinely wants
983
+ one face beside another needs nothing from this library but `data-font` on a
984
+ `<div>`.
985
+
986
+ - b86f482: The density ladder is five rungs, and the two names in the middle have moved.
987
+
988
+ `ultra-compact` · `normal` · `large` was never a ladder anyone could reason
989
+ about: the rung called `normal` is what every other design system calls
990
+ _compact_, the rung called `large` is what everyone else calls _normal_, and
991
+ above that there was nothing at all — no level for a touch panel, a stage
992
+ monitor, or a reader who needs the whole screen a size up. This renames the two
993
+ misnamed rungs to what they always were and adds the two that were missing.
994
+
995
+ **Minor rather than major because the package is pre-1.0** — 0.x's breaking rung
996
+ is the minor one, and this is breaking.
997
+
998
+ **Read the migration before you upgrade.** One of its four steps is a prop
999
+ rename a compiler catches. The other three are not: CSS of your own keyed on
1000
+ `[data-density]`, a label map with one entry per level, and a persisted
1001
+ preference all keep working after this release and quietly mean something else.
1002
+
1003
+ ## The mapping
1004
+
1005
+ | you wrote | you now write | geometry |
1006
+ | --------------- | ----------------- | ---------------------------------------------------- |
1007
+ | `ultra-compact` | `ultra-compact` | unchanged |
1008
+ | `normal` | **`compact`** | **unchanged** — the name moved, not a pixel |
1009
+ | `large` | **`normal`** | **unchanged** — the name moved, not a pixel |
1010
+ | — | **`large`** | new: a rung above what the ladder used to top out at |
1011
+ | — | **`ultra-large`** | new: the top, and the mirror of `ultra-compact` |
1012
+
1013
+ Nothing that existed changed size. `<ThemeProvider density="normal">` used to
1014
+ render 24px controls and now renders 32px ones — not because the geometry moved
1015
+ but because `normal` now names the rung above the one it used to name.
1016
+
1017
+ ## Migrating
1018
+
1019
+ Four things move. Only the first and the last are visible to a compiler; the two
1020
+ in the middle are the ones that ship a wrong-looking screen with a green build.
1021
+
1022
+ ### 1. Rename every level you name, one step down the ladder
1023
+
1024
+ Everywhere you write a density — the provider, a `<Density>` island, a prop you pass
1025
+ through, a test:
1026
+
1027
+ ```diff
1028
+ -<ThemeProvider theme="cue" density="normal">
1029
+ +<ThemeProvider theme="cue" density="compact">
1030
+ ```
1031
+
1032
+ ```diff
1033
+ -<Density density="large">
1034
+ +<Density density="normal">
1035
+ ```
1036
+
1037
+ `ultra-compact` is unchanged and needs no edit. Do the two renames in that
1038
+ order, or in one pass — `normal` → `compact` and `large` → `normal` — and your
1039
+ screens render exactly as they did before.
1040
+
1041
+ **If you never passed a `density`, this step is a no-op.** The default moved
1042
+ from `normal` to `compact`, which is the same geometry it always was: an app
1043
+ that never chose a level renders identically before and after.
1044
+
1045
+ ### 2. Rename your own `[data-density]` CSS — and add blocks for the two new rungs
1046
+
1047
+ **This is the half that fails silently.** If your app keys any of its own CSS on
1048
+ `data-density` — a per-rung scalar you multiply your lengths by, a per-rung
1049
+ override, anything — those selectors still match a valid attribute value after
1050
+ this release. They just match a _different rung_. Nothing throws, nothing logs,
1051
+ no build fails: your rows simply stop matching the controls inside them.
1052
+
1053
+ ```diff
1054
+ :root { --app-type: 1; --app-space: 1; }
1055
+ -[data-density="normal"] { --app-type: 1; --app-space: 1; }
1056
+ -[data-density="large"] { --app-type: 1.111; --app-space: 1.333; }
1057
+ +[data-density="compact"] { --app-type: 1; --app-space: 1; }
1058
+ +[data-density="normal"] { --app-type: 1.111; --app-space: 1.333; }
1059
+ +/* and two blocks that did not exist before — continue your own step, or read
1060
+ + the ratio off the library's ladder: --cue-density is 0.85 / 1 / 1.3 / 1.6 / 1.9
1061
+ + and the spacing sequence shifts one place per rung. */
1062
+ +[data-density="large"] { --app-type: …; --app-space: …; }
1063
+ +[data-density="ultra-large"] { --app-type: …; --app-space: …; }
1064
+ ```
1065
+
1066
+ What each rung does if you rename the props and stop there:
1067
+
1068
+ | rung | if you only renamed the props | what the reader sees |
1069
+ | --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- |
1070
+ | `ultra-compact` | your `ultra-compact` block still matches | correct |
1071
+ | `compact` | **no block matches** — falls through to `:root` | your scalars sit at their base values while the library's geometry is a rung up |
1072
+ | `normal` | your old `normal` block matches | your scalars are a rung behind the library's |
1073
+ | `large` | your old `large` block matches | your scalars are two rungs behind |
1074
+ | `ultra-large` | **no block matches** — falls through to `:root` | base scalars against the largest geometry the ladder has |
1075
+
1076
+ A rung with no block of yours does not fall back to the nearest one. It falls
1077
+ through to whatever `:root` declares, which is almost always the smallest value
1078
+ you own. Grep your stylesheets for `data-density` before you upgrade:
1079
+
1080
+ ```sh
1081
+ rg 'data-density' --glob '!node_modules'
1082
+ ```
1083
+
1084
+ ### 3. Fix anything that enumerates the ladder
1085
+
1086
+ A label map, a switcher, a segmented control, a settings screen, a persisted
1087
+ preference — anything with one entry per density level is now missing two and
1088
+ mis-naming two. A map keyed by the old three names does not error; it returns
1089
+ `undefined`, and most switchers render the raw slug:
1090
+
1091
+ ```diff
1092
+ const DENSITY_LABELS = {
1093
+ "ultra-compact": "Ultra compact",
1094
+ - normal: "Normal",
1095
+ - large: "Large",
1096
+ + compact: "Compact",
1097
+ + normal: "Normal",
1098
+ + large: "Large",
1099
+ + "ultra-large": "Ultra large",
1100
+ };
1101
+ ```
1102
+
1103
+ `DENSITIES` from `@cueplusplus/tokens` is the list to derive from rather than
1104
+ restate — it is five entries now and it will be right next time too. If your map
1105
+ is typed `Record<Density, string>` TypeScript will fail the build for you; if it
1106
+ is a plain object literal, nothing will.
1107
+
1108
+ **A persisted preference that says `"normal"` is now a bigger screen.** The
1109
+ provider stores the level under `localStorage` and validates it against the
1110
+ ladder, so `"normal"` is still a valid value — it just means a different rung.
1111
+ Migrate the stored value the same way you migrate the code, or accept that
1112
+ returning users move up one rung once.
1113
+
1114
+ ### 4. `density-ultra:` is now `density-ultra-compact:`
1115
+
1116
+ The stylesheet registers one Tailwind variant per rung, each named after the
1117
+ rung it matches: `density-ultra-compact:`, `density-compact:`,
1118
+ `density-normal:`, `density-large:`, `density-ultra-large:`. `density-ultra:` is
1119
+ gone, because with an `ultra-large` rung on the ladder "ultra" names neither end
1120
+ of it — and an unknown variant is a build error, so this one tells you.
1121
+ `density-large:` keeps its name and changes meaning with its rung, which does
1122
+ not.
1123
+
1124
+ ## The two new rungs
1125
+
1126
+ Every axis continues the step the three existing rungs already walk, rather than
1127
+ scaling anything by a factor:
1128
+
1129
+ | | ultra-compact | compact | normal | **large** | **ultra-large** |
1130
+ | ---------------- | ------------- | ------- | -------- | ------------ | --------------- |
1131
+ | `control-md` | 20px | 24px | 32px | **40px** | **48px** |
1132
+ | `chip-h` | 16px | 18px | 22px | **26px** | **30px** |
1133
+ | `icon-md` | 10px | 12px | 16px | **20px** | **24px** |
1134
+ | `text-body` | 10px | 11px | 13px | **15px** | **17px** |
1135
+ | `pad-row` | 2 × 6px | 4 × 8px | 6 × 12px | **8 × 16px** | **10 × 20px** |
1136
+ | `chrome-toolbar` | 32px | 36px | 44px | **52px** | **60px** |
1137
+ | `--cue-density` | 0.85 | 1 | 1.3 | **1.6** | **1.9** |
1138
+
1139
+ `large` is a touch and presentation rung: 40px controls clear the WCAG 2.2
1140
+ SC 2.5.8 target floor with room to spare. `ultra-large` is for a wall display, a
1141
+ kiosk, a stage monitor read from two metres, and for a low-vision reader who
1142
+ wants the whole interface a size up rather than the browser's zoom.
1143
+
1144
+ The `@media (pointer: coarse)` re-raise still applies to `ultra-compact` alone,
1145
+ and still raises it to exactly the same heights it always did — `compact`'s.
1146
+ `compact` is the rung the raise lands on, so raising it too would mean promoting
1147
+ it to `normal` and moving today's default geometry on every touch device.
1148
+
1149
+ ## The type ladder got a top
1150
+
1151
+ `text-title` and `text-emphasis` are bigger at every rung. `text-micro`,
1152
+ `text-label`, `text-ui` and `text-body` are untouched — consoles are built out
1153
+ of those four and none of them moves.
1154
+
1155
+ | | ultra-compact | compact | normal | large | ultra-large |
1156
+ | --------------- | ------------- | ------------- | ------------- | -------- | ----------- |
1157
+ | `text-emphasis` | 11 → **13px** | 12 → **14px** | 14 → **17px** | **20px** | **23px** |
1158
+ | `text-title` | 12 → **16px** | 13 → **18px** | 16 → **22px** | **26px** | **30px** |
1159
+
1160
+ A page heading was 1.18× a paragraph, which is not a hierarchy — it is six sizes
1161
+ within four points of each other, and every screen built on it came out flat. It
1162
+ is now 1.6–1.76×, and the whole ladder spans 2× to 2.7× from `micro` to `title`
1163
+ depending on the rung. If you set a page title in `text-title` it will look like
1164
+ one now; if you were compensating with a hand-written size, delete the
1165
+ compensation.
1166
+
1167
+ ## Also
1168
+
1169
+ `prepaintScript()` takes a third argument, `defaultDensity`, alongside
1170
+ `defaultTheme`. An app whose provider names a level other than `compact` should
1171
+ pass the same level here, or the first frame paints at one rung and every frame
1172
+ after it at another:
1173
+
1174
+ ```tsx
1175
+ prepaintScript(DEFAULT_STORAGE_KEY, "cue", "normal");
1176
+ ```
1177
+
1178
+ - a382b68: A ninth theme preset, `quotamate`, with a light pair.
1179
+
1180
+ `THEMES` gains `"quotamate"` and `ThemeName` widens with it, so a consumer can write
1181
+ `theme="quotamate"` without a cast. `dist/themes/quotamate.css` is built alongside the other
1182
+ eight, and `THEME_PRESETS.quotamate` is available to `createTheme()` and the configurator.
1183
+
1184
+ ```tsx
1185
+ <ThemeProvider theme="quotamate" density="ultra-compact" mode="dark">
1186
+ ```
1187
+
1188
+ A blue-black ramp at hue 240 under a cyan accent (`#00e5e5`). Not a new design — quotamate is
1189
+ a shipping product whose palette was already fixed in a private stylesheet, so the values are
1190
+ the same colours in hex that it authored in HSL. Adopting the preset is a no-op for the
1191
+ running product rather than a restyle, which is the only reason it can be adopted at all.
1192
+
1193
+ **Its status hues are deliberately not the accent**, which is where this preset differs from
1194
+ `cue`. quotamate's whole job is to say whether an account is usable right now, so `ok`, `warn`
1195
+ and `danger` have to stay separable at a glance from the colour that means "interactive" — a
1196
+ dashboard where "healthy" and "clickable" are the same green cannot be read quickly.
1197
+
1198
+ Light drops the accent to a deepened teal (`#0d7d7d`) rather than the near-black the product
1199
+ itself used. `#00e5e5` on `#fcfcfc` fails contrast for text and for a hairline rim alike, so
1200
+ the product's instinct was right; an accent that is merely dark, though, loses the brand.
1201
+ Same hue, enough chroma to read as the product's colour, dark enough to carry white text on a
1202
+ filled control.
1203
+
1204
+ Two tests encoded "eight presets" and were corrected rather than bumped:
1205
+
1206
+ - `build.test.mjs` listed the theme names, which is the point of that assertion.
1207
+ - `disjoint.test.mjs` asserted `sources === 16` as a stand-in for "each preset has a dark and
1208
+ a light source". A count is a poor proxy — eight darks and eight lights is 16, and so is
1209
+ nine darks and seven lights — and it failed a geometry test for a reason unrelated to
1210
+ geometry. It now asserts the pairing directly and will not go stale on the tenth preset.
1211
+
1212
+ ### Patch Changes
1213
+
1214
+ - 6ce0c7d: The release notes now ship inside the package.
1215
+
1216
+ `files` was `dist` alone, so the `CHANGELOG.md` this repository writes on every
1217
+ release stayed in a private git repository and reached nobody: `npm pack` left it
1218
+ out, and the tarball a consumer installs from GitHub Packages carried the code
1219
+ with no record of what had changed in it. Anyone asking "what moved between the
1220
+ version I have and the version I am upgrading to" had to have access to the
1221
+ source repository to find out.
1222
+
1223
+ `CHANGELOG.md` is now on the `files` allowlist of all three published packages,
1224
+ so it lands in `node_modules/@cueplusplus/<package>/CHANGELOG.md` beside the code
1225
+ it describes. `packages/release` has the test that keeps it there.
1226
+
1227
+ No code changes and no API changes — the same `dist`, plus one file.
1228
+
1229
+ The same notes are published, alongside the CUE++ agent skills' own, at
1230
+ <https://skills.cueplusplus.com/releases>.
1231
+
1232
+ ## 0.1.0
1233
+
1234
+ ### Minor Changes
1235
+
1236
+ - First release of the CUE++ design system.
1237
+
1238
+ `@cueplusplus/tokens` ships eight theme presets (`cue`, `terminal`, `signal`, `venu`, `hivehub`,
1239
+ `dusk`, `luma`, `snuffle`) in dark and light, three density levels (`ultra-compact`, `normal`,
1240
+ `large`), and the Tailwind v4 `@theme inline` mapping — colour scoped to `[data-theme]`, geometry to
1241
+ `[data-density]`, never mixed.
1242
+
1243
+ `@cueplusplus/ui` ships 146 components on Base UI: the core entry plus the `/layout`, `/forms`,
1244
+ `/overlays`, `/chrome`, `/instruments`, `/chat`, `/theming`, `/configurator`, `/color`, `/date`
1245
+ subpaths and the four opt-in ones (`/charts`, `/dmx`, `/midi`, `/flow`) whose heavy dependencies stay
1246
+ optional peers. `ThemeProvider`, the density islands, `createTheme()` with its contrast report and the
1247
+ floating `ThemeConfigurator` come with it.
1248
+
1249
+ `@cueplusplus/brand-tokens` ships the CUE++ brand layer — the white-alpha ramp, the mono stack, the
1250
+ eyebrow and motion treatments, and the wordmark and Plussie marks.