@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 +1250 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/axes.css +324 -0
- package/dist/base.json +364 -0
- package/dist/primitives.tokens.json +27 -0
- package/dist/registry/token-layer.json +340 -0
- package/dist/tailwind.css +20 -0
- package/dist/theme.css +12 -0
- package/dist/tokens.d.ts +114 -0
- package/dist/tokens.js +240 -0
- package/package.json +51 -0
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.
|