@cueplusplus/ui 0.1.0 → 0.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +684 -0
  2. package/dist/brand/_chassis.d.ts +54 -0
  3. package/dist/brand/_chassis.js +81 -0
  4. package/dist/brand/_geometry.js +41 -0
  5. package/dist/brand/cue-logotype.d.ts +29 -0
  6. package/dist/brand/cue-logotype.js +60 -0
  7. package/dist/brand/cue-mark.d.ts +30 -0
  8. package/dist/brand/cue-mark.js +61 -0
  9. package/dist/brand/index.d.ts +5 -0
  10. package/dist/brand/plussie.d.ts +67 -0
  11. package/dist/brand/plussie.js +125 -0
  12. package/dist/chat/agent-pile.js +1 -1
  13. package/dist/chat/ask-box.js +2 -2
  14. package/dist/chat/delegation-card.js +1 -1
  15. package/dist/chat/message.js +2 -2
  16. package/dist/chrome/_edge-scroller.d.ts +45 -0
  17. package/dist/chrome/_edge-scroller.js +138 -0
  18. package/dist/chrome/_glyphs.js +19 -1
  19. package/dist/chrome/_tabs-scroll.d.ts +12 -7
  20. package/dist/chrome/_tabs-scroll.js +12 -7
  21. package/dist/chrome/app-bar.d.ts +103 -0
  22. package/dist/chrome/app-bar.js +175 -0
  23. package/dist/chrome/app-shell.d.ts +125 -0
  24. package/dist/chrome/app-shell.js +167 -0
  25. package/dist/chrome/footer.d.ts +75 -0
  26. package/dist/chrome/footer.js +68 -0
  27. package/dist/chrome/index.d.ts +5 -1
  28. package/dist/chrome/index.js +4 -1
  29. package/dist/chrome/navigation-menu.js +1 -1
  30. package/dist/chrome/page-shell.d.ts +1 -1
  31. package/dist/chrome/page-shell.js +15 -5
  32. package/dist/chrome/segmented-control.d.ts +19 -0
  33. package/dist/chrome/segmented-control.js +40 -21
  34. package/dist/chrome/tabs.js +21 -65
  35. package/dist/chrome/toolbar.d.ts +22 -0
  36. package/dist/chrome/toolbar.js +26 -5
  37. package/dist/configurator/_overrides.js +4 -2
  38. package/dist/configurator/configurator.js +1 -1
  39. package/dist/configurator/panel-sections.js +10 -4
  40. package/dist/dmx/channel-matrix.js +1 -1
  41. package/dist/forms/otp-field.js +1 -1
  42. package/dist/index.d.ts +15 -6
  43. package/dist/index.js +15 -9
  44. package/dist/instruments/data-table.js +1 -1
  45. package/dist/layout/carousel.js +1 -1
  46. package/dist/layout/container.d.ts +1 -1
  47. package/dist/layout/container.js +6 -3
  48. package/dist/layout/index.d.ts +3 -3
  49. package/dist/layout/index.js +3 -3
  50. package/dist/layout/pagination.js +1 -1
  51. package/dist/layout/sidebar.d.ts +23 -3
  52. package/dist/layout/sidebar.js +18 -7
  53. package/dist/midi/spectrum-visualizer.js +1 -1
  54. package/dist/midi/timeline-ruler.js +1 -1
  55. package/dist/overlays/command-palette.js +1 -1
  56. package/dist/overlays/dialog.js +1 -1
  57. package/dist/overlays/dropdown-menu.js +2 -2
  58. package/dist/overlays/hover-card.js +1 -1
  59. package/dist/overlays/index.js +1 -1
  60. package/dist/overlays/popover.js +1 -1
  61. package/dist/overlays/sheet.js +1 -1
  62. package/dist/overlays/toast.js +1 -1
  63. package/dist/primitives/index.d.ts +1 -1
  64. package/dist/primitives/index.js +1 -1
  65. package/dist/primitives/status-dot.d.ts +7 -0
  66. package/dist/primitives/status-dot.js +8 -1
  67. package/dist/styles.css +47 -1
  68. package/dist/system/density.d.ts +4 -1
  69. package/dist/system/density.js +12 -4
  70. package/dist/system/index.d.ts +2 -2
  71. package/dist/system/portal.d.ts +5 -5
  72. package/dist/system/portal.js +11 -6
  73. package/dist/system/prepaint.d.ts +15 -8
  74. package/dist/system/prepaint.js +26 -9
  75. package/dist/system/theme-provider.d.ts +36 -10
  76. package/dist/system/theme-provider.js +56 -18
  77. package/dist/system/use-density.d.ts +3 -3
  78. package/dist/system/use-density.js +16 -6
  79. package/dist/system/use-theme.d.ts +4 -2
  80. package/dist/system/use-theme.js +4 -2
  81. package/dist/theming/_presets.js +80 -5
  82. package/dist/theming/create-theme.js +13 -4
  83. package/package.json +4 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,684 @@
1
+ # @cueplusplus/ui
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 33a95c4: Typeface is a third axis, beside theme and density.
8
+
9
+ `data-font` selects one of eight font pairings — a sans and a monospace — the
10
+ same way `data-theme` selects a palette and `data-density` selects a geometry.
11
+ `<ThemeProvider font>`, `useTheme().font` / `setFont`, persistence beside the
12
+ other two, the pre-paint script, and every portal root carry it.
13
+
14
+ ```tsx
15
+ <ThemeProvider theme="cue" density="normal" font="plex">
16
+ <App />
17
+ </ThemeProvider>
18
+ ```
19
+
20
+ | id | sans | mono | what your app has to do |
21
+ | -------- | ---------------------- | ------------------- | ---------------------------------- |
22
+ | `system` | the platform's UI sans | **the theme's own** | nothing — this is the default |
23
+ | `geist` | Geist | JetBrains Mono | deliver two faces |
24
+ | `inter` | Inter | IBM Plex Mono | deliver two faces |
25
+ | `plex` | IBM Plex Sans | IBM Plex Mono | deliver two faces |
26
+ | `roboto` | Roboto | Roboto Mono | deliver two faces |
27
+ | `source` | Source Sans 3 | Source Code Pro | deliver two faces |
28
+ | `apple` | SF Pro Text | SF Mono | nothing — and nothing you _may_ do |
29
+ | `office` | Calibri (Carlito) | Cascadia Mono | nothing — and nothing you _may_ do |
30
+
31
+ New exports from `@cueplusplus/tokens`: `FONTS`, `FontName`, `DEFAULT_FONT`,
32
+ `FONT_PAIRINGS`, `FONT_FACES`, `FontDelivery`, `FontPairing`, `faceProperty`.
33
+ `prepaintScript()` takes a fourth argument, the default pairing.
34
+
35
+ ## You have to load the fonts. We will not.
36
+
37
+ **This package ships no font file and no `@font-face`, and that is deliberate.**
38
+ A library that injected one would be choosing your network requests, your
39
+ content-security policy and your font licensing, in a stylesheet you imported
40
+ for its colours. What a pairing emits instead is a hook with the family name as
41
+ its fallback:
42
+
43
+ ```css
44
+ [data-font="inter"] {
45
+ --cue-font-sans: var(--cue-face-inter, Inter), ui-sans-serif, system-ui, …;
46
+ --cue-font-mono:
47
+ var(--cue-face-ibm-plex-mono, "IBM Plex Mono"), ui-monospace, …;
48
+ }
49
+ ```
50
+
51
+ Assign nothing and the stack still resolves — a local install first, then the
52
+ platform — so no pairing ever renders as nothing. Assign the property and
53
+ everything painted from the token follows. With `next/font`, the property name
54
+ _is_ the option:
55
+
56
+ ```tsx
57
+ const inter = Inter({ subsets: ["latin"], display: "swap", preload: false, variable: "--cue-face-inter" });
58
+ <html className={inter.variable}>
59
+ ```
60
+
61
+ `preload: false` matters: with preloading on, a site offering five webfont
62
+ pairings makes every visitor download all of them to render in one.
63
+ `FONT_PAIRINGS[name].faces` is the list of properties one pairing needs;
64
+ `FONT_FACES` is every face at once, as `property → family`.
65
+
66
+ **`apple` and `office` are stacks and must stay stacks.** SF Pro and SF Mono are
67
+ Apple-licensed and may not be served as webfonts; Calibri is Microsoft's and
68
+ ships with Windows and Office rather than with anybody's site (Carlito is its
69
+ metric-compatible libre twin, and is in the stack). They light up where
70
+ installed and fall through cleanly where they are not. Do not bundle them.
71
+
72
+ ## A theme's monospace still applies — until somebody picks a pairing
73
+
74
+ Seven presets name a monospace of their own, and seven pairings name one, so
75
+ one token was claimed twice.
76
+ Neither axis writes it now: a theme publishes `--cue-font-theme-mono`, a pairing
77
+ publishes `--cue-font-pairing-mono`, and `--cue-font-mono` resolves the two,
78
+ pairing first. **A chosen pairing wins**, at whatever element each was stamped
79
+ on — a pairing is what a person picked out of a menu and a preset's mono is what
80
+ its author chose in the absence of a person. The default pairing, `system`,
81
+ declines the question, so an app that never touches the axis keeps exactly the
82
+ monospace it has today.
83
+
84
+ ## Two things that change without you asking
85
+
86
+ **The base sans no longer names `Geist`.** It named a font this package has
87
+ never delivered, so for almost every visitor it resolved to the next entry
88
+ anyway. `--cue-font-sans` and `--cue-font-display` are now the platform stack;
89
+ Geist is the head of the `geist` pairing, which your app can actually deliver.
90
+ If you want it back, deliver it: `font="geist"` plus the two `--cue-face-*`
91
+ assignments.
92
+
93
+ **A generated theme's mono moves with it.** `createTheme({ fonts: { mono } })`
94
+ now writes `--cue-font-theme-mono`. Nothing to change unless you read the
95
+ generated `tokens` map by key.
96
+
97
+ ## Migrating
98
+
99
+ Nothing is required. Two things are worth doing:
100
+
101
+ 1. **Pass the pairing to the pre-paint script**, as its fourth argument, if your
102
+ app's default is not `system`:
103
+ `prepaintScript(DEFAULT_STORAGE_KEY, "cue", "normal", "geist")`. A theme that
104
+ arrives one frame late is a flash of the wrong colour; a _family_ that
105
+ arrives one frame late reflows every line under the reader.
106
+ 2. **If your own CSS overrides `--cue-font-mono`** under a `[data-theme]` block
107
+ of your own, move it to `--cue-font-theme-mono` — otherwise your override
108
+ beats the typeface picker and yours is the one theme it cannot move.
109
+
110
+ `font` is a root-level prop: there is no font island, and a nested provider
111
+ forwards the axis (and `setFont`) to the root. A specimen that genuinely wants
112
+ one face beside another needs nothing from this library but `data-font` on a
113
+ `<div>`.
114
+
115
+ - 038837d: `AppShell`: the console frame, and the mobile navigation that goes with it.
116
+
117
+ Every console screen is the same five parts in the same arrangement — title bar,
118
+ navigation column, toolbar, one scrolling work area, status bar — and until now
119
+ each one arranged them by hand. `AppShell` is that arrangement, with the
120
+ arithmetic that actually holds it up: `h-dvh` rather than `min-h-dvh`, an
121
+ unbroken `min-h-0` chain from the frame down to the scroller, `min-w-0` on the
122
+ content column, and a scroller that is not the document, so the chrome stays put
123
+ with no `sticky` anywhere.
124
+
125
+ ```tsx
126
+ import {
127
+ AppShell,
128
+ Sidebar,
129
+ StatusBar,
130
+ TitleBar,
131
+ Toolbar,
132
+ } from "@cueplusplus/ui";
133
+
134
+ <AppShell.Root>
135
+ <TitleBar center="main-stage.cue">
136
+ <AppShell.NavTrigger />
137
+ </TitleBar>
138
+ <AppShell.Body>
139
+ <AppShell.Sidebar aria-label="Console" drawerTitle="Console">
140
+ <Sidebar.Section label="Show">
141
+ <Sidebar.Item icon={List} active>
142
+ Cues
143
+ </Sidebar.Item>
144
+ </Sidebar.Section>
145
+ </AppShell.Sidebar>
146
+ <AppShell.Content>
147
+ <Toolbar.Root aria-label="Playback">…</Toolbar.Root>
148
+ <AppShell.Scroller>…</AppShell.Scroller>
149
+ </AppShell.Content>
150
+ <AppShell.Aside aria-label="Inspector">…</AppShell.Aside>
151
+ </AppShell.Body>
152
+ <StatusBar>…</StatusBar>
153
+ </AppShell.Root>;
154
+ ```
155
+
156
+ Seven parts: `Root` (the frame), `Body` (the middle row), `Sidebar` (the
157
+ navigation), `NavTrigger` (the control that opens it on a phone), `Content` (the
158
+ `<main>` column — pass `main={false}` when the shell is embedded in a page that
159
+ already has one), `Scroller` (the only element that may scroll) and `Aside` (a
160
+ trailing column with a scroller of its own). `TitleBar` and `StatusBar` go in as
161
+ ordinary children: both are already `shrink-0`, so a wrapper would only repeat
162
+ that.
163
+
164
+ **`AppShell` is the one component in this library with a viewport breakpoint.**
165
+ `AppShell.Sidebar` renders the same children twice — as the 14rem column at
166
+ Tailwind's stock `md` (48rem) and up, and as a `Drawer` below it, opened by
167
+ `AppShell.NavTrigger` from the bar. It is a CSS-only dual render, both shapes in
168
+ the markup with one hidden per viewport: no `useMediaQuery`, nothing to measure,
169
+ and a first server-rendered frame that is already correct. The switch decides
170
+ navigation shape and nothing else; everything else in the library stays
171
+ intrinsically responsive, and a later component that wants a breakpoint is a new
172
+ decision rather than a precedent this one set.
173
+
174
+ `PageShell` and `Container` now offer the same four measures.
175
+
176
+ `narrow` (42rem) and `wide` (54rem) are unchanged. `broad` (84rem) is new on
177
+ both: the application measure, wide enough for a two-column documentation page
178
+ or a device frame. `full` is new on `PageShell`, which had no uncapped rung at
179
+ all while `Container` did. Nothing existing moves — `narrow` is still the
180
+ default on both.
181
+
182
+ ```tsx
183
+ <PageShell width="broad" title="Components">…</PageShell>
184
+ <PageShell width="full" title="Gallery">…</PageShell>
185
+ ```
186
+
187
+ - 7eeb1c4: New component group, `brand`: the CUE++ marks, as components rather than files.
188
+
189
+ `CueMark` is the circular mark, `CueLogotype` is the wordmark and mark locked up
190
+ at 778:367, and `Plussie` is the character — the same circle and the same two
191
+ `+` glyphs the mark draws, out of the same geometry module, with the eyes in
192
+ their own groups so a pose can move them. All three come from the root barrel
193
+ (`@cueplusplus/ui`), pull no optional peer, and are static markup, so a
194
+ server-rendered header costs no client bundle.
195
+
196
+ ```tsx
197
+ import { CueLogotype, CueMark, Plussie } from "@cueplusplus/ui";
198
+
199
+ <CueMark size="sm" />
200
+ <CueLogotype size="lg" title="CUE++" />
201
+ <Plussie size={96} expression="wink" />
202
+ ```
203
+
204
+ Props on all three: `size` (`"sm" | "md" | "lg"`, or a height in px), which sets
205
+ the height and lets the artwork's ratio decide the width; `tone`
206
+ (`"inherit" | "fg" | "accent"`, default `"inherit"` = `currentColor`); and an
207
+ optional `title`, which turns a decorative `aria-hidden` mark into a labelled
208
+ `role="img"`. `Plussie` adds `expression` (`default`, `blink`, `wink`,
209
+ `look-left`, `look-right`, `look-up`, `look-down` — each one a still from the
210
+ character's own animations) and `idle`, a CSS-only blink that is off by default
211
+ and switched off again under `prefers-reduced-motion`.
212
+
213
+ There is no light/dark pair to choose between. `@cueplusplus/brand-tokens` still
214
+ ships the four source SVGs as files for the places that need a URL — a favicon,
215
+ an `<img>`, an email — and those hard-code `white`, `black` and `#1E1E1E`.
216
+ These components take their ink from the token layer instead, so one element is
217
+ correct in all eight presets and both modes. The path data is the same artwork,
218
+ copied verbatim.
219
+
220
+ - b86f482: The density ladder is five rungs, and the two names in the middle have moved.
221
+
222
+ `ultra-compact` · `normal` · `large` was never a ladder anyone could reason
223
+ about: the rung called `normal` is what every other design system calls
224
+ _compact_, the rung called `large` is what everyone else calls _normal_, and
225
+ above that there was nothing at all — no level for a touch panel, a stage
226
+ monitor, or a reader who needs the whole screen a size up. This renames the two
227
+ misnamed rungs to what they always were and adds the two that were missing.
228
+
229
+ **Minor rather than major because the package is pre-1.0** — 0.x's breaking rung
230
+ is the minor one, and this is breaking.
231
+
232
+ **Read the migration before you upgrade.** One of its four steps is a prop
233
+ rename a compiler catches. The other three are not: CSS of your own keyed on
234
+ `[data-density]`, a label map with one entry per level, and a persisted
235
+ preference all keep working after this release and quietly mean something else.
236
+
237
+ ## The mapping
238
+
239
+ | you wrote | you now write | geometry |
240
+ | --------------- | ----------------- | ---------------------------------------------------- |
241
+ | `ultra-compact` | `ultra-compact` | unchanged |
242
+ | `normal` | **`compact`** | **unchanged** — the name moved, not a pixel |
243
+ | `large` | **`normal`** | **unchanged** — the name moved, not a pixel |
244
+ | — | **`large`** | new: a rung above what the ladder used to top out at |
245
+ | — | **`ultra-large`** | new: the top, and the mirror of `ultra-compact` |
246
+
247
+ Nothing that existed changed size. `<ThemeProvider density="normal">` used to
248
+ render 24px controls and now renders 32px ones — not because the geometry moved
249
+ but because `normal` now names the rung above the one it used to name.
250
+
251
+ ## Migrating
252
+
253
+ Four things move. Only the first and the last are visible to a compiler; the two
254
+ in the middle are the ones that ship a wrong-looking screen with a green build.
255
+
256
+ ### 1. Rename every level you name, one step down the ladder
257
+
258
+ Everywhere you write a density — the provider, a `<Density>` island, a prop you pass
259
+ through, a test:
260
+
261
+ ```diff
262
+ -<ThemeProvider theme="cue" density="normal">
263
+ +<ThemeProvider theme="cue" density="compact">
264
+ ```
265
+
266
+ ```diff
267
+ -<Density density="large">
268
+ +<Density density="normal">
269
+ ```
270
+
271
+ `ultra-compact` is unchanged and needs no edit. Do the two renames in that
272
+ order, or in one pass — `normal` → `compact` and `large` → `normal` — and your
273
+ screens render exactly as they did before.
274
+
275
+ **If you never passed a `density`, this step is a no-op.** The default moved
276
+ from `normal` to `compact`, which is the same geometry it always was: an app
277
+ that never chose a level renders identically before and after.
278
+
279
+ ### 2. Rename your own `[data-density]` CSS — and add blocks for the two new rungs
280
+
281
+ **This is the half that fails silently.** If your app keys any of its own CSS on
282
+ `data-density` — a per-rung scalar you multiply your lengths by, a per-rung
283
+ override, anything — those selectors still match a valid attribute value after
284
+ this release. They just match a _different rung_. Nothing throws, nothing logs,
285
+ no build fails: your rows simply stop matching the controls inside them.
286
+
287
+ ```diff
288
+ :root { --app-type: 1; --app-space: 1; }
289
+ -[data-density="normal"] { --app-type: 1; --app-space: 1; }
290
+ -[data-density="large"] { --app-type: 1.111; --app-space: 1.333; }
291
+ +[data-density="compact"] { --app-type: 1; --app-space: 1; }
292
+ +[data-density="normal"] { --app-type: 1.111; --app-space: 1.333; }
293
+ +/* and two blocks that did not exist before — continue your own step, or read
294
+ + the ratio off the library's ladder: --cue-density is 0.85 / 1 / 1.3 / 1.6 / 1.9
295
+ + and the spacing sequence shifts one place per rung. */
296
+ +[data-density="large"] { --app-type: …; --app-space: …; }
297
+ +[data-density="ultra-large"] { --app-type: …; --app-space: …; }
298
+ ```
299
+
300
+ What each rung does if you rename the props and stop there:
301
+
302
+ | rung | if you only renamed the props | what the reader sees |
303
+ | --------------- | ----------------------------------------------- | ------------------------------------------------------------------------------- |
304
+ | `ultra-compact` | your `ultra-compact` block still matches | correct |
305
+ | `compact` | **no block matches** — falls through to `:root` | your scalars sit at their base values while the library's geometry is a rung up |
306
+ | `normal` | your old `normal` block matches | your scalars are a rung behind the library's |
307
+ | `large` | your old `large` block matches | your scalars are two rungs behind |
308
+ | `ultra-large` | **no block matches** — falls through to `:root` | base scalars against the largest geometry the ladder has |
309
+
310
+ A rung with no block of yours does not fall back to the nearest one. It falls
311
+ through to whatever `:root` declares, which is almost always the smallest value
312
+ you own. Grep your stylesheets for `data-density` before you upgrade:
313
+
314
+ ```sh
315
+ rg 'data-density' --glob '!node_modules'
316
+ ```
317
+
318
+ ### 3. Fix anything that enumerates the ladder
319
+
320
+ A label map, a switcher, a segmented control, a settings screen, a persisted
321
+ preference — anything with one entry per density level is now missing two and
322
+ mis-naming two. A map keyed by the old three names does not error; it returns
323
+ `undefined`, and most switchers render the raw slug:
324
+
325
+ ```diff
326
+ const DENSITY_LABELS = {
327
+ "ultra-compact": "Ultra compact",
328
+ - normal: "Normal",
329
+ - large: "Large",
330
+ + compact: "Compact",
331
+ + normal: "Normal",
332
+ + large: "Large",
333
+ + "ultra-large": "Ultra large",
334
+ };
335
+ ```
336
+
337
+ `DENSITIES` from `@cueplusplus/tokens` is the list to derive from rather than
338
+ restate — it is five entries now and it will be right next time too. If your map
339
+ is typed `Record<Density, string>` TypeScript will fail the build for you; if it
340
+ is a plain object literal, nothing will.
341
+
342
+ **A persisted preference that says `"normal"` is now a bigger screen.** The
343
+ provider stores the level under `localStorage` and validates it against the
344
+ ladder, so `"normal"` is still a valid value — it just means a different rung.
345
+ Migrate the stored value the same way you migrate the code, or accept that
346
+ returning users move up one rung once.
347
+
348
+ ### 4. `density-ultra:` is now `density-ultra-compact:`
349
+
350
+ The stylesheet registers one Tailwind variant per rung, each named after the
351
+ rung it matches: `density-ultra-compact:`, `density-compact:`,
352
+ `density-normal:`, `density-large:`, `density-ultra-large:`. `density-ultra:` is
353
+ gone, because with an `ultra-large` rung on the ladder "ultra" names neither end
354
+ of it — and an unknown variant is a build error, so this one tells you.
355
+ `density-large:` keeps its name and changes meaning with its rung, which does
356
+ not.
357
+
358
+ ## The two new rungs
359
+
360
+ Every axis continues the step the three existing rungs already walk, rather than
361
+ scaling anything by a factor:
362
+
363
+ | | ultra-compact | compact | normal | **large** | **ultra-large** |
364
+ | ---------------- | ------------- | ------- | -------- | ------------ | --------------- |
365
+ | `control-md` | 20px | 24px | 32px | **40px** | **48px** |
366
+ | `chip-h` | 16px | 18px | 22px | **26px** | **30px** |
367
+ | `icon-md` | 10px | 12px | 16px | **20px** | **24px** |
368
+ | `text-body` | 10px | 11px | 13px | **15px** | **17px** |
369
+ | `pad-row` | 2 × 6px | 4 × 8px | 6 × 12px | **8 × 16px** | **10 × 20px** |
370
+ | `chrome-toolbar` | 32px | 36px | 44px | **52px** | **60px** |
371
+ | `--cue-density` | 0.85 | 1 | 1.3 | **1.6** | **1.9** |
372
+
373
+ `large` is a touch and presentation rung: 40px controls clear the WCAG 2.2
374
+ SC 2.5.8 target floor with room to spare. `ultra-large` is for a wall display, a
375
+ kiosk, a stage monitor read from two metres, and for a low-vision reader who
376
+ wants the whole interface a size up rather than the browser's zoom.
377
+
378
+ The `@media (pointer: coarse)` re-raise still applies to `ultra-compact` alone,
379
+ and still raises it to exactly the same heights it always did — `compact`'s.
380
+ `compact` is the rung the raise lands on, so raising it too would mean promoting
381
+ it to `normal` and moving today's default geometry on every touch device.
382
+
383
+ ## The type ladder got a top
384
+
385
+ `text-title` and `text-emphasis` are bigger at every rung. `text-micro`,
386
+ `text-label`, `text-ui` and `text-body` are untouched — consoles are built out
387
+ of those four and none of them moves.
388
+
389
+ | | ultra-compact | compact | normal | large | ultra-large |
390
+ | --------------- | ------------- | ------------- | ------------- | -------- | ----------- |
391
+ | `text-emphasis` | 11 → **13px** | 12 → **14px** | 14 → **17px** | **20px** | **23px** |
392
+ | `text-title` | 12 → **16px** | 13 → **18px** | 16 → **22px** | **26px** | **30px** |
393
+
394
+ A page heading was 1.18× a paragraph, which is not a hierarchy — it is six sizes
395
+ within four points of each other, and every screen built on it came out flat. It
396
+ is now 1.6–1.76×, and the whole ladder spans 2× to 2.7× from `micro` to `title`
397
+ depending on the rung. If you set a page title in `text-title` it will look like
398
+ one now; if you were compensating with a hand-written size, delete the
399
+ compensation.
400
+
401
+ ## Also
402
+
403
+ `prepaintScript()` takes a third argument, `defaultDensity`, alongside
404
+ `defaultTheme`. An app whose provider names a level other than `compact` should
405
+ pass the same level here, or the first frame paints at one rung and every frame
406
+ after it at another:
407
+
408
+ ```tsx
409
+ prepaintScript(DEFAULT_STORAGE_KEY, "cue", "normal");
410
+ ```
411
+
412
+ - a382b68: A ninth theme preset, `quotamate`, with a light pair.
413
+
414
+ `THEMES` gains `"quotamate"` and `ThemeName` widens with it, so a consumer can write
415
+ `theme="quotamate"` without a cast. `dist/themes/quotamate.css` is built alongside the other
416
+ eight, and `THEME_PRESETS.quotamate` is available to `createTheme()` and the configurator.
417
+
418
+ ```tsx
419
+ <ThemeProvider theme="quotamate" density="ultra-compact" mode="dark">
420
+ ```
421
+
422
+ A blue-black ramp at hue 240 under a cyan accent (`#00e5e5`). Not a new design — quotamate is
423
+ a shipping product whose palette was already fixed in a private stylesheet, so the values are
424
+ the same colours in hex that it authored in HSL. Adopting the preset is a no-op for the
425
+ running product rather than a restyle, which is the only reason it can be adopted at all.
426
+
427
+ **Its status hues are deliberately not the accent**, which is where this preset differs from
428
+ `cue`. quotamate's whole job is to say whether an account is usable right now, so `ok`, `warn`
429
+ and `danger` have to stay separable at a glance from the colour that means "interactive" — a
430
+ dashboard where "healthy" and "clickable" are the same green cannot be read quickly.
431
+
432
+ Light drops the accent to a deepened teal (`#0d7d7d`) rather than the near-black the product
433
+ itself used. `#00e5e5` on `#fcfcfc` fails contrast for text and for a hairline rim alike, so
434
+ the product's instinct was right; an accent that is merely dark, though, loses the brand.
435
+ Same hue, enough chroma to read as the product's colour, dark enough to carry white text on a
436
+ filled control.
437
+
438
+ Two tests encoded "eight presets" and were corrected rather than bumped:
439
+
440
+ - `build.test.mjs` listed the theme names, which is the point of that assertion.
441
+ - `disjoint.test.mjs` asserted `sources === 16` as a stand-in for "each preset has a dark and
442
+ a light source". A count is a poor proxy — eight darks and eight lights is 16, and so is
443
+ nine darks and seven lights — and it failed a geometry test for a reason unrelated to
444
+ geometry. It now asserts the pairing directly and will not go stale on the tenth preset.
445
+
446
+ - 236d09b: The bars: `AppBar`, `Footer`, and one overflow behaviour for every strip that can
447
+ outgrow its box.
448
+
449
+ ### One overflow behaviour, not three bespoke fixes
450
+
451
+ `Toolbar` had neither wrap nor scroll and its buttons are `shrink-0
452
+ whitespace-nowrap`, so a bar wider than its container ran off the edge with no
453
+ way to reach it. `SegmentedControl` did the same with five segments in a page
454
+ header. Both now scroll, from one module, with the affordance
455
+ `ScrollableTabsList` has shipped since the tab strip was written: a fade mask at
456
+ whichever edge still hides something, and a chevron there — because what sits at
457
+ an overflowing edge is usually the gap _between_ two controls, and fading empty
458
+ space produces nothing anyone can see.
459
+
460
+ ```tsx
461
+ <Toolbar.Root aria-label="Playback" /> // scrolls by default
462
+ <Toolbar.Root aria-label="Playback" overflow="clip" /> // the old behaviour
463
+ <SegmentedControl aria-label="View" items={items} overflow="clip" />
464
+ ```
465
+
466
+ Not wrapping: every bar here is one rung of the chrome ladder tall and sits in a
467
+ layout that has already subtracted that height, so a bar that quietly becomes
468
+ two rows steals it from the work area. Not a "more" menu: `Toolbar` takes
469
+ arbitrary children and there is no honest conversion from one into a menu item,
470
+ so it would need a parallel data model, a measuring pass on every resize, and it
471
+ would lift the surplus controls out of Base UI's roving-focus group. A bar whose
472
+ controls fit renders exactly as it did before — no mask, no chevrons, no
473
+ compositing layer.
474
+
475
+ `AppBar.Nav` and `ScrollableTabsList` use the same module, so the library has one
476
+ implementation of this and four consumers.
477
+
478
+ ### `AppBar` — the horizontal bar
479
+
480
+ ```tsx
481
+ <AppBar.Root>
482
+ <AppBar.Brand render={<NextLink href="/" />}>
483
+ <Eyebrow>@cueplusplus/ui</Eyebrow>
484
+ <Chip tone="accent" variant="outline">
485
+ 0.1.0
486
+ </Chip>
487
+ </AppBar.Brand>
488
+
489
+ <AppBar.Nav aria-label="Sections">
490
+ <AppBar.Link render={<NextLink href="/docs" />}>
491
+ Getting started
492
+ </AppBar.Link>
493
+ <AppBar.Separator />
494
+ <AppBar.Link render={<NextLink href="/gallery" />} register="strong">
495
+ Gallery
496
+ </AppBar.Link>
497
+ <AppBar.Link
498
+ external
499
+ render={<a href="https://skills.cueplusplus.com" />}
500
+ >
501
+ Skills
502
+ </AppBar.Link>
503
+ </AppBar.Nav>
504
+
505
+ <AppBar.Actions>
506
+ <ThemePicker />
507
+ </AppBar.Actions>
508
+ </AppBar.Root>
509
+ ```
510
+
511
+ Sticky, `--cue-chrome-toolbar` tall, brand and actions `shrink-0` with the
512
+ navigation between them giving up the width. `AppBar.Link` is set in one of two
513
+ registers — `muted` is furniture, `strong` is a destination — and `active` is a
514
+ third thing rather than a louder register: accent ink _and_ `aria-current="page"`.
515
+
516
+ This is a **bar**, not a menu: `NavigationMenu` goes inside `AppBar.Nav` when a
517
+ section has children. It is also not `TitleBar`, which is window furniture.
518
+
519
+ **It adds no viewport breakpoint.** `AppShell` still holds the library's only
520
+ one, for navigation shape. A horizontal bar never faces that question: at 390px
521
+ it is the same bar with a shorter window onto the same links.
522
+
523
+ `app-bar.tsx` carries no `"use client"` directive, so the bar renders from a
524
+ server component — which a site header has to, because that is where a framework
525
+ reads its data. The client boundary is around the navigation's scroller alone.
526
+
527
+ ### `Footer` — the landmark, the hairline, the measure
528
+
529
+ ```tsx
530
+ <Footer
531
+ meta={
532
+ <>
533
+ <span>© 2026 CUE++</span>
534
+ <Link href="/legal">Legal</Link>
535
+ </>
536
+ }
537
+ >
538
+ <Grid minItemWidth="12rem" gap={5}>
539
+ <Stack gap={2}>
540
+ <Eyebrow>Docs</Eyebrow>
541
+ <Link href="/docs">Getting started</Link>
542
+ </Stack>
543
+ </Grid>
544
+ </Footer>
545
+ ```
546
+
547
+ The `contentinfo` landmark, a full-bleed hairline, a centred column on the same
548
+ four measures `Container` and `PageShell` offer (`broad` by default), and a
549
+ `meta` slot whose fine print hangs under a rule of its own.
550
+
551
+ Deliberately **not** a link-list component. A complex footer is `Grid` + `Stack`
552
+ - `Link` inside this one — components that already exist, already read the
553
+ density ladder, and already handle a column that is a paragraph rather than a
554
+ list. `Grid`'s `minItemWidth` collapses the columns on a phone with no breakpoint
555
+ and no prop for how many to draw. Static markup, no `"use client"`.
556
+
557
+ - 2c6aca1: `AppShell` and `Sidebar`: the four defects the shell's first independent consumer found, and the landmark it was announcing twice.
558
+
559
+ `AppShell` was drawn from the console specimens. The documentation site is the
560
+ first thing outside them to wear it, and putting a real 30-row navigation and a
561
+ few hundred visually-hidden labels through it turned up four things the
562
+ specimens never could. All four are fixed in the library rather than at the call
563
+ site, which is the whole point of a shell.
564
+
565
+ **Minor rather than major because the package is pre-1.0** — 0.x's breaking rung
566
+ is the minor one, and two of these changes are breaking. The migration is at the
567
+ bottom.
568
+
569
+ **`AppShell.Root` keeps its own `documentScrolls: false` promise.** The frame is
570
+ now `relative overflow-clip`. The flex arithmetic it already had only governs
571
+ boxes in the flow; an absolutely positioned descendant is laid out against its
572
+ containing block, and a frame that is neither positioned nor clipping is not
573
+ one — so the _initial_ containing block was, and the descendant's scrollable
574
+ overflow landed on `<html>`. `.sr-only` is `position: absolute`, so this was an
575
+ everyday case rather than an exotic one: a page carrying 308 hidden labels
576
+ measured a 7,068px document inside a 900px frame with every visible box on
577
+ screen, and a browser scrollbar that scrolled nothing. `clip` rather than
578
+ `hidden`, so the frame is not a scroll container something can shove sideways
579
+ with no scrollbar to put it back. The emitters are fixed too — `StatusDot`,
580
+ `OTPField`, `Message`, `AskBox` and `Sidebar.Item` now position the box their
581
+ hidden label is laid out against, so the leak stops at source as well as at the
582
+ frame.
583
+
584
+ **One navigation, one landmark.** `Sidebar.Root` rendered `<aside>` while
585
+ `AppShell.Sidebar`'s drawer half rendered `<nav>`, so the same navigation
586
+ announced as two different landmarks depending on the viewport — and a call site
587
+ whose column is the site's primary navigation had to hand-patch
588
+ `role="navigation"` over the part. Both halves now take one `landmark` prop:
589
+
590
+ ```tsx
591
+ <AppShell.Sidebar aria-label="Console">…</AppShell.Sidebar> {/* <nav>, both shapes */}
592
+ <Sidebar.Root aria-label="Filters" landmark="complementary">…</Sidebar.Root> {/* <aside> */}
593
+ ```
594
+
595
+ `"navigation"` is the default, because a column of `Sidebar.Item`s with one of
596
+ them marked `aria-current="page"` is primary navigation and not complementary.
597
+
598
+ **The column scrolls, and the rail does not scroll with it.** `Sidebar.Root` had
599
+ no scroller at all, so a navigation taller than the frame simply lost its last
600
+ rows; the fix is an inner scroller rather than `overflow-y-auto` on the root,
601
+ because `Sidebar.Rail` is `absolute inset-y-0` and would otherwise slide away
602
+ with the third screenful. Its containing block is the root and the scroller is
603
+ the element inside it, so the rail spans the whole column and holds its edge
604
+ however far the sections are scrolled. Measured: a 828px column of navigation in
605
+ a 318px frame, scrolled 400px, rail unmoved and unclipped.
606
+
607
+ **`AppShell.NavTrigger` is a target a finger can hit.** It defaulted to
608
+ `size="sm"` — 20px at normal density — on a control that renders _only_ below
609
+ the `md` breakpoint, which is to say only on a touch screen, under the 24×24
610
+ floor WCAG 2.2 SC 2.5.8 sets. The default is `size="md"` now: 24px at normal and
611
+ large, and 24px at ultra-compact too, where the token layer's
612
+ `@media (pointer: coarse)` rule already lifts the control ladder. It is also
613
+ `shrink-0`, because every other slot in `AppBar` is and this was the one that
614
+ gave — measured at 14px wide in a crowded 390px bar.
615
+
616
+ `Footer` gained no code, but its guidance was wrong in a way that cost a site its
617
+ footer: it listed "inside `AppShell`" under _when not to use_. Inside a shell a
618
+ footer belongs to the content scroller, at the end of the work area — bounded by
619
+ the content column, scrolling with the page. What does not belong is a footer as
620
+ a sibling of the navigation column. `Footer` and `StatusBar` are different
621
+ objects and a shell can carry both.
622
+
623
+ ### Migrating
624
+ - **`Sidebar.Root` renders `<nav>`.** A stylesheet or a test selecting
625
+ `aside[data-slot="sidebar"]` needs `[data-slot="sidebar"]`, and a column that
626
+ really is complementary needs `landmark="complementary"`. A call site passing
627
+ `role="navigation"` to work around the old element can drop it.
628
+ - **`Sidebar.Root` has one more element inside it.** Its children are wrapped in
629
+ `[data-slot="sidebar-scroller"]`, which now carries the column's padding and
630
+ gap. A `> *` selector aimed at the sections needs re-aiming; `overflow-y-auto`
631
+ passed in at the call site can go.
632
+ - **`AppShell.NavTrigger` is 4px bigger.** Pass `size="sm"` back if a bar really
633
+ needs the old height, and read WCAG 2.5.8 first.
634
+
635
+ ### Patch Changes
636
+
637
+ - 6ce0c7d: The release notes now ship inside the package.
638
+
639
+ `files` was `dist` alone, so the `CHANGELOG.md` this repository writes on every
640
+ release stayed in a private git repository and reached nobody: `npm pack` left it
641
+ out, and the tarball a consumer installs from GitHub Packages carried the code
642
+ with no record of what had changed in it. Anyone asking "what moved between the
643
+ version I have and the version I am upgrading to" had to have access to the
644
+ source repository to find out.
645
+
646
+ `CHANGELOG.md` is now on the `files` allowlist of all three published packages,
647
+ so it lands in `node_modules/@cueplusplus/<package>/CHANGELOG.md` beside the code
648
+ it describes. `packages/release` has the test that keeps it there.
649
+
650
+ No code changes and no API changes — the same `dist`, plus one file.
651
+
652
+ The same notes are published, alongside the CUE++ agent skills' own, at
653
+ <https://skills.cueplusplus.com/releases>.
654
+
655
+ - Updated dependencies [33a95c4]
656
+ - Updated dependencies [b86f482]
657
+ - Updated dependencies [a382b68]
658
+ - Updated dependencies [6ce0c7d]
659
+ - @cueplusplus/tokens@0.2.0
660
+
661
+ ## 0.1.0
662
+
663
+ ### Minor Changes
664
+
665
+ - First release of the CUE++ design system.
666
+
667
+ `@cueplusplus/tokens` ships eight theme presets (`cue`, `terminal`, `signal`, `venu`, `hivehub`,
668
+ `dusk`, `luma`, `snuffle`) in dark and light, three density levels (`ultra-compact`, `normal`,
669
+ `large`), and the Tailwind v4 `@theme inline` mapping — colour scoped to `[data-theme]`, geometry to
670
+ `[data-density]`, never mixed.
671
+
672
+ `@cueplusplus/ui` ships 146 components on Base UI: the core entry plus the `/layout`, `/forms`,
673
+ `/overlays`, `/chrome`, `/instruments`, `/chat`, `/theming`, `/configurator`, `/color`, `/date`
674
+ subpaths and the four opt-in ones (`/charts`, `/dmx`, `/midi`, `/flow`) whose heavy dependencies stay
675
+ optional peers. `ThemeProvider`, the density islands, `createTheme()` with its contrast report and the
676
+ floating `ThemeConfigurator` come with it.
677
+
678
+ `@cueplusplus/brand-tokens` ships the CUE++ brand layer — the white-alpha ramp, the mono stack, the
679
+ eyebrow and motion treatments, and the wordmark and Plussie marks.
680
+
681
+ ### Patch Changes
682
+
683
+ - Updated dependencies
684
+ - @cueplusplus/tokens@0.1.0