@cueplusplus/ui 0.14.1 → 0.15.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 (105) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agent-runtime/tool-call-card.js +1 -1
  3. package/dist/chat/message.js +1 -1
  4. package/dist/chrome/band.js +10 -4
  5. package/dist/chrome/eyebrow.js +1 -1
  6. package/dist/chrome/footer.js +1 -1
  7. package/dist/chrome/section-header.js +1 -1
  8. package/dist/configurator/configurator.js +1 -1
  9. package/dist/configurator/export-dialog.js +1 -1
  10. package/dist/configurator/panel-sections.js +1 -1
  11. package/dist/configurator/token-editor.js +1 -1
  12. package/dist/date/calendar.js +2 -2
  13. package/dist/elements/context-usage.js +3 -3
  14. package/dist/elements/transcript-rules.js +1 -1
  15. package/dist/forms/field.js +1 -1
  16. package/dist/forms/slider.js +1 -1
  17. package/dist/instruments/code-block.js +1 -1
  18. package/dist/instruments/ledger.js +1 -1
  19. package/dist/instruments/progress.js +1 -1
  20. package/dist/instruments/stat.js +1 -1
  21. package/dist/instruments/table.js +2 -2
  22. package/dist/instruments/terminal-frame.js +1 -1
  23. package/dist/layout/_ground.js +1 -1
  24. package/dist/layout/collapsible.js +1 -1
  25. package/dist/layout/description-list.js +1 -1
  26. package/dist/layout/preview.d.ts +13 -1
  27. package/dist/layout/preview.js +16 -9
  28. package/dist/layout/sidebar.js +1 -1
  29. package/dist/layout/stepper.js +1 -1
  30. package/dist/overlays/_surface.d.ts +1 -1
  31. package/dist/overlays/_surface.js +1 -1
  32. package/dist/overlays/command-palette.js +1 -1
  33. package/dist/overlays/toast.js +1 -1
  34. package/dist/primitives/chip.js +2 -2
  35. package/dist/system/density.d.ts +11 -2
  36. package/dist/system/density.js +29 -9
  37. package/dist/system/portal.js +8 -2
  38. package/dist/system/prepaint.d.ts +18 -1
  39. package/dist/system/prepaint.js +8 -2
  40. package/dist/system/theme-provider.d.ts +17 -2
  41. package/dist/system/theme-provider.js +57 -17
  42. package/dist/system/vocabulary.d.ts +1 -1
  43. package/dist/system/vocabulary.js +14 -2
  44. package/manifest/components/alert-dialog.json +2 -1
  45. package/manifest/components/autocomplete.json +2 -1
  46. package/manifest/components/band.json +1 -1
  47. package/manifest/components/calendar.json +3 -1
  48. package/manifest/components/channel-matrix.json +2 -1
  49. package/manifest/components/chart-tooltip-content.json +2 -1
  50. package/manifest/components/chip.json +1 -0
  51. package/manifest/components/code-block.json +2 -1
  52. package/manifest/components/collapsible.json +2 -1
  53. package/manifest/components/colors-section.json +2 -1
  54. package/manifest/components/combobox.json +2 -1
  55. package/manifest/components/command-palette.json +2 -1
  56. package/manifest/components/context-menu.json +2 -1
  57. package/manifest/components/context-usage.json +1 -0
  58. package/manifest/components/density.json +13 -0
  59. package/manifest/components/description-list.json +2 -1
  60. package/manifest/components/dialog.json +2 -1
  61. package/manifest/components/disclosure.json +2 -1
  62. package/manifest/components/dmx-bar.json +2 -1
  63. package/manifest/components/dmx-strip.json +2 -1
  64. package/manifest/components/drawer.json +2 -1
  65. package/manifest/components/dropdown-menu.json +2 -1
  66. package/manifest/components/export-dialog.json +2 -1
  67. package/manifest/components/eyebrow.json +2 -1
  68. package/manifest/components/field-description.json +2 -1
  69. package/manifest/components/field-error.json +2 -1
  70. package/manifest/components/field-label.json +2 -1
  71. package/manifest/components/field.json +2 -1
  72. package/manifest/components/footer.json +1 -0
  73. package/manifest/components/frames.json +2 -1
  74. package/manifest/components/hover-card.json +2 -1
  75. package/manifest/components/ledger.json +2 -1
  76. package/manifest/components/menubar.json +2 -1
  77. package/manifest/components/message.json +2 -1
  78. package/manifest/components/multi-select.json +2 -1
  79. package/manifest/components/navigation-menu.json +2 -1
  80. package/manifest/components/popover.json +2 -1
  81. package/manifest/components/preset-section.json +2 -1
  82. package/manifest/components/preview.json +15 -1
  83. package/manifest/components/progress.json +2 -1
  84. package/manifest/components/section-header.json +2 -1
  85. package/manifest/components/shape-section.json +2 -1
  86. package/manifest/components/sheet.json +2 -1
  87. package/manifest/components/sidebar.json +2 -1
  88. package/manifest/components/slider.json +2 -1
  89. package/manifest/components/stat.json +1 -0
  90. package/manifest/components/stepper.json +2 -1
  91. package/manifest/components/table-scroll-region.json +2 -1
  92. package/manifest/components/table.json +2 -1
  93. package/manifest/components/tags-input.json +2 -1
  94. package/manifest/components/terminal-frame.json +1 -0
  95. package/manifest/components/theme-configurator.json +2 -1
  96. package/manifest/components/theme-provider.json +13 -0
  97. package/manifest/components/time-boundary.json +2 -1
  98. package/manifest/components/toast.json +2 -0
  99. package/manifest/components/token-editor.json +2 -1
  100. package/manifest/components/tool-call-card.json +2 -1
  101. package/manifest/components/tooltip.json +2 -1
  102. package/manifest/components/unread-divider.json +2 -1
  103. package/manifest/manifest.json +63 -63
  104. package/manifest/tokens.json +29 -2
  105. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,162 @@
1
1
  # @cueplusplus/ui
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5493c5a: A style set can now say what colour the line between two bands is.
8
+
9
+ `band-rule` was only ever its WIDTH. The colour was the literal string
10
+ `border-border`, spelled twice — once in `SEPARATION_PAINT` and once in
11
+ `Footer`'s own class list — so a set could say how thick the rule is and had no
12
+ way to say what it was made of.
13
+
14
+ `--cue-band-rule-color` joins the style-set contract, which becomes six required
15
+ names. It takes `var(--cue-border)`, `var(--cue-border-subtle)`,
16
+ `var(--cue-border-strong)` or `transparent` — structural inks only, because a
17
+ brand colour on a structural line belongs to a different axis. The `:root`
18
+ default and all three shipped sets declare `var(--cue-border)`, which is
19
+ byte-for-byte what both call sites drew before, so this is a no-op until a set
20
+ spends it.
21
+
22
+ Measured across thirteen reference sites on 2026-09-22: every one that separates
23
+ bands by a line picks a line lighter than its box edge. `--cue-border` is a box
24
+ edge, and repeating a box edge between sections is what `--cue-border-subtle`'s
25
+ own docblock argues against one layer down. Until now no axis could express the
26
+ difference.
27
+
28
+ - c6e26c0: `Density`, `Preview` and portals carry the paint axis
29
+
30
+ `<Density paint>` and `<Preview paint>` stamp `data-paint` on their subtree, and
31
+ a portal opened inside either follows it — the same three-publisher arrangement
32
+ the style axis already has, for the same reason: a portal mounts on `<body>` and
33
+ inherits nothing from the DOM it was written in, so it has to be told.
34
+
35
+ ```tsx
36
+ <Density density="compact" paint="none">
37
+ <Toolbar />
38
+ </Density>
39
+
40
+ <Preview paint="none">
41
+ <Card>…</Card>
42
+ </Preview>
43
+ ```
44
+
45
+ **Absent means inherit, and `none` is a value rather than an absence.** An island
46
+ that names no pack stamps no attribute and passes the ambient pack straight
47
+ through; `none` restates the `:root` defaults, which is what a subtree stamps to
48
+ opt _out_ of a pack its parent set. Collapsing the two would make every unpainted
49
+ subtree override its parent instead of inheriting from it.
50
+
51
+ New: `PaintContext`, and a `paint` prop on `DensityProps` and `PreviewProps`.
52
+ `CuePortalStamp` gains a `paint` field.
53
+
54
+ The provider half — a `paint` prop on `<ThemeProvider>`, persistence, and the
55
+ blocking prepaint script — is not here. This is the island path, which works on
56
+ its own: a subtree can re-scope the axis without a provider, exactly as
57
+ `<Density styleSet>` does.
58
+
59
+ - e7a2405: `ThemeProvider` carries the paint axis, and persists it
60
+
61
+ `<ThemeProvider paint>` stamps `data-paint`, `useTheme()` reports it and
62
+ `setPaint()` switches it, and the blocking pre-paint script puts it on `<html>`
63
+ before the first frame — so a page restored from storage does not flash an
64
+ unpainted frame before React commits.
65
+
66
+ ```tsx
67
+ <ThemeProvider theme="cue" paint="none">
68
+ …
69
+ </ThemeProvider>;
70
+
71
+ const { paint, setPaint } = useTheme();
72
+ ```
73
+
74
+ **An app that never touches the axis writes the object it always wrote, byte for
75
+ byte.** The pack is persisted only when there is one — the rule `styleSet`
76
+ already follows — because a `"paint": null` in storage would be a value the
77
+ pre-paint script has to hold an opinion about for ever, for a preference nobody
78
+ expressed.
79
+
80
+ **The root never promotes an ambient pack.** `useTheme().paint` reports the pack
81
+ in force, but the `data-paint` stamp stays on the provider's own state, so a
82
+ `<Density paint>` island cannot become a document-wide attribute and from there
83
+ reach storage and the next load.
84
+
85
+ New: `paint` on `ThemeProviderProps` and `PrepaintDefaults`, `paint` and
86
+ `setPaint` on the `useTheme()` value, `isPaint()` beside `isStyleSet()`, and
87
+ `paint` on `PersistedPreferences`.
88
+
89
+ The skills learn the axis too: `ui-theming` documents five axes rather than four,
90
+ and the generated token reference carries the eighteen paint names — so an agent
91
+ asked to compose a page can find `--cue-paint-glow-blur` instead of spelling a
92
+ shadow literal by hand.
93
+
94
+ - d5c4183: Letter spacing becomes a token a style set can retune.
95
+
96
+ The system had already decided this and never finished it. `leading-ui` is spent
97
+ 63 times through a token against 2 literals. The only tracking name that existed,
98
+ `tracking-label`, was spent **zero** times — while 34 hard-coded `tracking-[…]`
99
+ literals sat across 27 chassis files at six distinct values.
100
+
101
+ Three names join the style-set contract, at the values those literals already
102
+ held: `tracking-caps` (0.15em, 23 call sites), `tracking-caps-tight` (0.1em, 5)
103
+ and `tracking-caps-loose` (0.2em, 1 — `Eyebrow`, the one place the system spends
104
+ the accent on pure typography, so a set that wants a different kicker has
105
+ nowhere else to say so). Two optional names join beside `style-display`:
106
+ `style-tracking-display` and `style-leading-display`, the latter a second name
107
+ rather than a widened `leading-prose` because a display register routinely sits
108
+ under 1 and a paragraph must not.
109
+
110
+ 32 literals become token spends. Two survive deliberately: `tracking-[inherit]`
111
+ on `Stepper`, which is a pass-through rather than a value, and `tracking-[0.06em]`
112
+ on `Chip`'s tag, one call site at a value no other rung shares.
113
+
114
+ `tracking-label` stays in the frozen base and does **not** move. Its three call
115
+ sites now spend `--cue-tracking-label` instead of a literal, which finally makes
116
+ a declared-and-never-spent token do its job.
117
+
118
+ Measured across thirteen reference sites on 2026-09-22: a display tracking is
119
+ missing in twelve of them — more than any single paint device. Apple's curve is
120
+ non-monotone (-0.022em at 17px, +0.012em at 19px, 0 at 40px, -0.015em at 80px);
121
+ vercel wants -0.02/-0.04/-0.06em; stripe -0.025em. None of it was expressible.
122
+
123
+ Every value is unchanged. The defaults restate the literals they replace.
124
+
125
+ ### Patch Changes
126
+
127
+ - 9c7041a: The dot screen and the app bar's glass spend paint tokens
128
+
129
+ `Preview` and `Frames`' dots ground reads `--cue-paint-screen` and
130
+ `--cue-paint-screen-size` instead of spelling the gradient out.
131
+
132
+ **Nothing renders differently, and that is proved in a browser rather than
133
+ argued.** Each token's `:root` default is the value its literal held, so a page
134
+ that never stamps `data-paint` computes exactly what it computed before.
135
+ `tests/browser/paint-noop.spec.ts` measures it on the built site: the adopted
136
+ utility and the literal it replaced compute the same `background-image` and
137
+ `background-size`, and every value is required to be non-`none` first — because two absences would satisfy an equality, and "the dots
138
+ stopped painting" is the regression that guards against.
139
+
140
+ A pack can now retune the finish without touching a component.
141
+
142
+ **One duplicate collapsed.** `apps/docs`' ground picker carried a hand-copied
143
+ second spelling of the same gradient, with a comment saying it was "the same
144
+ gradient `Preview`'s own `dots` ground paints" — and nothing tied the two
145
+ together. Both spend the token now, so they are the same value rather than the
146
+ same text.
147
+
148
+ **`AppBar` keeps `backdrop-blur`.** Adopting `--cue-paint-glass-filter` there was
149
+ tried and reverted: Tailwind composes every backdrop utility into one
150
+ declaration, so a raw `[backdrop-filter:var(…)]` would stop a consumer's
151
+ `backdrop-brightness-50` from combining with the bar's blur. The glass slice
152
+ owns that decision.
153
+
154
+ - Updated dependencies [a20e98e]
155
+ - Updated dependencies [5493c5a]
156
+ - Updated dependencies [96f6f6b]
157
+ - Updated dependencies [d5c4183]
158
+ - @cueplusplus/tokens@0.15.0
159
+
3
160
  ## 0.14.1
4
161
 
5
162
  ### Patch Changes
@@ -25,7 +25,7 @@ const DANGER_TONES = {
25
25
  */
26
26
  const HEADER = "flex min-w-0 items-center gap-(--cue-space-2) border-0 bg-transparent p-0 text-start font-mono text-(length:--cue-text-ui) text-fg-muted outline-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-accent";
27
27
  /** The label over a block in the body, in the same eyebrow as a message header. */
28
- const EYEBROW = "font-mono text-(length:--cue-text-micro) tracking-[0.15em] uppercase text-fg-subtle";
28
+ const EYEBROW = "font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) uppercase text-fg-subtle";
29
29
  /**
30
30
  * An LLM's own tool call, shown while it runs and after.
31
31
  *
@@ -72,7 +72,7 @@ const Message = React.forwardRef(function Message({ className, role = "agent", a
72
72
  children: [
73
73
  hasHeader ? /* @__PURE__ */ jsxs("div", {
74
74
  "data-slot": "message-header",
75
- className: "flex min-w-0 items-baseline justify-between gap-(--cue-space-3) font-mono text-(length:--cue-text-micro) tracking-[0.15em] uppercase",
75
+ className: "flex min-w-0 items-baseline justify-between gap-(--cue-space-3) font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) uppercase",
76
76
  children: [name === void 0 ? null : /* @__PURE__ */ jsx("span", {
77
77
  "data-slot": "message-author",
78
78
  className: cn("min-w-0 truncate", role === "user" ? "text-accent-fg opacity-70" : "text-(--agent-color)"),
@@ -16,12 +16,18 @@ import { jsx } from "react/jsx-runtime";
16
16
  * the argument.
17
17
  *
18
18
  * `rule`'s entry is the colour and not the width: the width is
19
- * `--cue-band-rule` and belongs to the chassis, and both elements name
20
- * `border-border` in their own class list rather than inheriting it from the
21
- * other.
19
+ * `--cue-band-rule` and belongs to the chassis, and both elements name the ink
20
+ * in their own class list rather than inheriting it from the other.
21
+ *
22
+ * That ink is now a token. It was the literal `border-border`, spelled here and
23
+ * again in `Footer`, which meant a style set could say how THICK the line
24
+ * between two bands is and had no way to say what colour it was — and every
25
+ * site measured in the 2026-09-22 corpus that separates bands by a line picks
26
+ * one lighter than its box edge. `--cue-band-rule-color` defaults to
27
+ * `var(--cue-border)` on `:root`, which is byte-for-byte what this drew before.
22
28
  */
23
29
  const SEPARATION_PAINT = {
24
- rule: "border-border",
30
+ rule: "border-(--cue-band-rule-color)",
25
31
  ground: "bg-(--cue-band-ground)",
26
32
  bleed: null,
27
33
  none: null
@@ -22,7 +22,7 @@ const Eyebrow = React.forwardRef(function Eyebrow({ className, slashes = true, c
22
22
  return /* @__PURE__ */ jsxs("span", {
23
23
  ref,
24
24
  "data-slot": "eyebrow",
25
- className: cn("inline-flex items-center gap-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-[0.2em] text-accent uppercase", className),
25
+ className: cn("inline-flex items-center gap-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps-loose) text-accent uppercase", className),
26
26
  ...elementProps,
27
27
  children: [slashes ? /* @__PURE__ */ jsx("span", {
28
28
  "data-slot": "eyebrow-glyph",
@@ -66,7 +66,7 @@ const Footer = React.forwardRef(function Footer({ className, columnClassName, wi
66
66
  ref,
67
67
  "data-slot": "footer",
68
68
  "data-separation": "rule",
69
- className: cn(bandClasses("rule"), "border-border", className),
69
+ className: cn(bandClasses("rule"), "border-(--cue-band-rule-color)", className),
70
70
  ...elementProps,
71
71
  children: /* @__PURE__ */ jsxs(Container, {
72
72
  width,
@@ -35,7 +35,7 @@ const SectionHeader = React.forwardRef(function SectionHeader({ className, label
35
35
  }) : null,
36
36
  /* @__PURE__ */ jsx("span", {
37
37
  "data-slot": "section-header-label",
38
- className: "shrink-0 font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-muted uppercase",
38
+ className: "shrink-0 font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-muted uppercase",
39
39
  children: label
40
40
  }),
41
41
  count === void 0 ? null : /* @__PURE__ */ jsx("span", {
@@ -221,7 +221,7 @@ function ThemeConfigurator({ position: positionProp, defaultPosition = "floating
221
221
  }),
222
222
  /* @__PURE__ */ jsx("span", {
223
223
  id: titleId,
224
- className: "flex-1 truncate font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-muted uppercase",
224
+ className: "flex-1 truncate font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-muted uppercase",
225
225
  children: title
226
226
  }),
227
227
  moved === 0 ? null : /* @__PURE__ */ jsx(Chip, {
@@ -70,7 +70,7 @@ function ExportDialog({ open, onOpenChange, overrides, scope, defaultFormat = "c
70
70
  /* @__PURE__ */ jsxs(Field, {
71
71
  className: "gap-(--cue-space-1)",
72
72
  children: [/* @__PURE__ */ jsx(FieldLabel, {
73
- className: "font-mono text-(length:--cue-text-micro) tracking-[0.15em] uppercase",
73
+ className: "font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) uppercase",
74
74
  children: "Theme name"
75
75
  }), /* @__PURE__ */ jsx(Input, {
76
76
  size: "sm",
@@ -133,7 +133,7 @@ function TextTokenField({ label, token, edits, placeholder }) {
133
133
  return /* @__PURE__ */ jsxs(Field, {
134
134
  className: "gap-(--cue-space-1)",
135
135
  children: [/* @__PURE__ */ jsx(FieldLabel, {
136
- className: "font-mono text-(length:--cue-text-micro) tracking-[0.15em] uppercase",
136
+ className: "font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) uppercase",
137
137
  children: label
138
138
  }), /* @__PURE__ */ jsx(Input, {
139
139
  size: "sm",
@@ -133,7 +133,7 @@ function TokenEditor({ values, overridden, onTokenChange, onTokenReset, groups =
133
133
  "data-slot": "configurator-token-group",
134
134
  className: "flex flex-col gap-(--cue-space-2)",
135
135
  children: [/* @__PURE__ */ jsx("span", {
136
- className: "font-mono text-(length:--cue-text-micro) tracking-[0.15em] text-fg-subtle uppercase",
136
+ className: "font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
137
137
  children: group.label
138
138
  }), group.tokens.map((token) => {
139
139
  const property = tokenProperty(token);
@@ -23,14 +23,14 @@ const CALENDAR_CLASSES = {
23
23
  months: "relative flex flex-wrap items-start gap-(--cue-space-5)",
24
24
  month: "flex flex-col gap-(--cue-space-2)",
25
25
  month_caption: "flex h-control-sm items-center justify-center",
26
- caption_label: "font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase",
26
+ caption_label: "font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
27
27
  nav: "absolute inset-x-0 top-0 flex h-control-sm items-center justify-between",
28
28
  button_previous: "flex size-control-sm cursor-pointer items-center justify-center rounded-(--radius-control) border-0 bg-transparent p-0 text-fg-subtle outline-none transition-colors duration-150 hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none",
29
29
  button_next: "flex size-control-sm cursor-pointer items-center justify-center rounded-(--radius-control) border-0 bg-transparent p-0 text-fg-subtle outline-none transition-colors duration-150 hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none",
30
30
  chevron: "size-icon-sm fill-current",
31
31
  month_grid: "border-collapse border-spacing-0",
32
32
  weekdays: "",
33
- weekday: "size-control-sm p-0 text-center align-middle font-mono text-(length:--cue-text-micro) font-normal tracking-[0.1em] text-fg-subtle uppercase",
33
+ weekday: "size-control-sm p-0 text-center align-middle font-mono text-(length:--cue-text-micro) font-normal tracking-(--cue-tracking-caps-tight) text-fg-subtle uppercase",
34
34
  week: "",
35
35
  day: "p-0 text-center align-middle",
36
36
  day_button: "size-control-sm cursor-pointer rounded-(--radius-control) border-0 bg-transparent p-0 font-mono text-(length:--cue-text-ui) tabular-nums text-fg-muted outline-none transition-colors duration-150 hover:bg-surface-3 hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none",
@@ -136,17 +136,17 @@ const ContextUsage = React.forwardRef(function ContextUsage({ categories, limit,
136
136
  children: [
137
137
  /* @__PURE__ */ jsx("th", {
138
138
  scope: "col",
139
- className: "pb-1 text-start font-normal tracking-[0.12em]",
139
+ className: "pb-1 text-start font-normal tracking-(--cue-tracking-label)",
140
140
  children: copy.category
141
141
  }),
142
142
  /* @__PURE__ */ jsx("th", {
143
143
  scope: "col",
144
- className: "pb-1 text-end font-normal tracking-[0.12em]",
144
+ className: "pb-1 text-end font-normal tracking-(--cue-tracking-label)",
145
145
  children: copy.tokens
146
146
  }),
147
147
  /* @__PURE__ */ jsx("th", {
148
148
  scope: "col",
149
- className: "pb-1 text-end font-normal tracking-[0.12em]",
149
+ className: "pb-1 text-end font-normal tracking-(--cue-tracking-label)",
150
150
  children: copy.usage
151
151
  })
152
152
  ]
@@ -86,7 +86,7 @@ const UnreadDivider = React.forwardRef(function UnreadDivider({ label = "New", c
86
86
  className: "bg-accent/40 h-px flex-1"
87
87
  }), /* @__PURE__ */ jsx("span", {
88
88
  "aria-hidden": true,
89
- className: cn(mono, "text-accent shrink-0 tracking-[0.15em] uppercase"),
89
+ className: cn(mono, "text-accent shrink-0 tracking-(--cue-tracking-caps) uppercase"),
90
90
  children: label
91
91
  })]
92
92
  });
@@ -43,7 +43,7 @@ const FieldLabel = React.forwardRef(function FieldLabel({ className, ...elementP
43
43
  return /* @__PURE__ */ jsx(Field.Label, {
44
44
  ref,
45
45
  "data-slot": "field-label",
46
- className: cn("font-mono text-(length:--cue-text-label) leading-none tracking-[0.1em] text-fg-muted uppercase select-none data-[disabled]:opacity-40", className),
46
+ className: cn("font-mono text-(length:--cue-text-label) leading-none tracking-(--cue-tracking-caps-tight) text-fg-muted uppercase select-none data-[disabled]:opacity-40", className),
47
47
  ...elementProps
48
48
  });
49
49
  });
@@ -40,7 +40,7 @@ const Slider$1 = React.forwardRef(function Slider$2({ className, onCommit, label
40
40
  className: cn("flex w-full flex-col gap-(--cue-space-2)", className),
41
41
  ...elementProps,
42
42
  children: [label === void 0 && !showValue ? null : /* @__PURE__ */ jsxs("div", {
43
- className: "flex items-baseline justify-between gap-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-[0.1em] text-fg-muted uppercase",
43
+ className: "flex items-baseline justify-between gap-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps-tight) text-fg-muted uppercase",
44
44
  children: [label === void 0 ? /* @__PURE__ */ jsx("span", {}) : /* @__PURE__ */ jsx(Slider.Label, { children: label }), showValue ? /* @__PURE__ */ jsx(Slider.Value, { className: "tabular-nums text-fg normal-case tracking-normal" }) : null]
45
45
  }), /* @__PURE__ */ jsx(Slider.Control, {
46
46
  "data-slot": "slider-control",
@@ -159,7 +159,7 @@ const CodeBlock = React.forwardRef(function CodeBlock({ code, label, language =
159
159
  className: "flex items-center justify-between gap-(--cue-space-4) border-b border-border px-(--cue-pad-row-x) py-(--cue-pad-row-y)",
160
160
  children: [/* @__PURE__ */ jsx("span", {
161
161
  "data-slot": "code-block-label",
162
- className: "font-mono text-(length:--cue-text-micro) tracking-[0.15em] text-fg-subtle uppercase",
162
+ className: "font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
163
163
  children: label ?? language
164
164
  }), /* @__PURE__ */ jsx(CopyButton, { value: code })]
165
165
  }), /* @__PURE__ */ jsx("pre", {
@@ -13,7 +13,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
13
  * be said out loud, because `<h2>` and `<h3>` come bold by default and a bold
14
14
  * micro-label in a dense list reads as a second kind of emphasis.
15
15
  */
16
- const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] text-fg-subtle uppercase";
16
+ const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-(--cue-tracking-caps) text-fg-subtle uppercase";
17
17
  /**
18
18
  * The disclosure button a collapsible heading holds.
19
19
  *
@@ -58,7 +58,7 @@ const Progress$1 = React.forwardRef(function Progress$2({ className, trackClassN
58
58
  children: [label === void 0 ? null : /* @__PURE__ */ jsx(Progress.Label, {
59
59
  id: namedByCaption ? captionId : void 0,
60
60
  "data-slot": "progress-label",
61
- className: "truncate tracking-[0.15em] text-fg-subtle uppercase",
61
+ className: "truncate tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
62
62
  children: label
63
63
  }), showValue ? /* @__PURE__ */ jsx(Progress.Value, {
64
64
  "data-slot": "progress-value",
@@ -39,7 +39,7 @@ const Stat = React.forwardRef(function Stat({ className, label, value, detail, t
39
39
  children: [
40
40
  /* @__PURE__ */ jsx("span", {
41
41
  "data-slot": "stat-label",
42
- className: "truncate font-mono text-(length:--cue-text-micro) tracking-[0.15em] text-fg-subtle uppercase",
42
+ className: "truncate font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
43
43
  children: label
44
44
  }),
45
45
  /* @__PURE__ */ jsx("span", {
@@ -122,7 +122,7 @@ const TableHead = React.forwardRef(function TableHead({ className, numeric = fal
122
122
  scope,
123
123
  "data-slot": "table-head",
124
124
  "data-numeric": numeric ? "" : void 0,
125
- className: cn("border-b border-border bg-surface-1 px-(--cue-pad-row-x) py-(--cue-pad-row-y) text-left font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] whitespace-nowrap text-fg-subtle uppercase", sticky ? "sticky top-0 z-3 border-b-0 shadow-[inset_0_-1px_0_var(--cue-hair-strong)]" : null, className),
125
+ className: cn("border-b border-border bg-surface-1 px-(--cue-pad-row-x) py-(--cue-pad-row-y) text-left font-mono text-(length:--cue-text-label) font-normal tracking-(--cue-tracking-caps) whitespace-nowrap text-fg-subtle uppercase", sticky ? "sticky top-0 z-3 border-b-0 shadow-[inset_0_-1px_0_var(--cue-hair-strong)]" : null, className),
126
126
  ...elementProps
127
127
  });
128
128
  });
@@ -201,7 +201,7 @@ const Table = {
201
201
  colSpan: span,
202
202
  scope: "rowgroup",
203
203
  "data-slot": "table-group-cell",
204
- className: cn("bg-bg px-(--cue-pad-row-x) py-(--cue-pad-row-y) text-left font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] whitespace-nowrap text-fg-subtle uppercase", sticky ? "sticky z-2 [top:var(--cue-table-group-top,0px)]" : null, className),
204
+ className: cn("bg-bg px-(--cue-pad-row-x) py-(--cue-pad-row-y) text-left font-mono text-(length:--cue-text-label) font-normal tracking-(--cue-tracking-caps) whitespace-nowrap text-fg-subtle uppercase", sticky ? "sticky z-2 [top:var(--cue-table-group-top,0px)]" : null, className),
205
205
  children: [/* @__PURE__ */ jsx(Label, {
206
206
  "data-slot": "table-group-label",
207
207
  className: TABLE_GROUP_LABEL,
@@ -191,7 +191,7 @@ const TerminalFrame = React.forwardRef(function TerminalFrame({ className, title
191
191
  className: "flex h-control-md shrink-0 items-center justify-between gap-(--cue-space-3) border-b border-border px-(--cue-pad-row-x)",
192
192
  children: [/* @__PURE__ */ jsx("span", {
193
193
  "data-slot": "terminal-frame-title",
194
- className: "min-w-0 truncate text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase",
194
+ className: "min-w-0 truncate text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
195
195
  children: title
196
196
  }), copy ? /* @__PURE__ */ jsx("span", {
197
197
  "data-slot": "terminal-frame-copy",
@@ -2,7 +2,7 @@
2
2
  /** What each ground paints. `"none"` paints nothing, so whatever is behind shows through. */
3
3
  const GROUND_CLASSES = {
4
4
  plain: "bg-surface-1",
5
- dots: "bg-surface-1 bg-[radial-gradient(circle,var(--cue-hair-strong)_1px,transparent_1.3px)] bg-[length:14px_14px]",
5
+ dots: "bg-surface-1 bg-(image:--cue-paint-screen) bg-(length:--cue-paint-screen-size)",
6
6
  none: ""
7
7
  };
8
8
  //#endregion
@@ -94,7 +94,7 @@ const Disclosure = React.forwardRef(function Disclosure({ className, label = "ad
94
94
  children: [/* @__PURE__ */ jsxs(Collapsible.Trigger, {
95
95
  ref,
96
96
  "data-slot": "disclosure-trigger",
97
- className: cn("group/disclosure inline-flex cursor-pointer items-center gap-(--cue-space-2) self-start border-0 bg-transparent p-0 py-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase transition-colors duration-150 outline-none select-none hover:text-fg-muted focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-accent", className),
97
+ className: cn("group/disclosure inline-flex cursor-pointer items-center gap-(--cue-space-2) self-start border-0 bg-transparent p-0 py-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase transition-colors duration-150 outline-none select-none hover:text-fg-muted focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-accent", className),
98
98
  ...elementProps,
99
99
  children: [
100
100
  /* @__PURE__ */ jsx("span", {
@@ -20,7 +20,7 @@ const DescriptionList = {
20
20
  return /* @__PURE__ */ jsx("dt", {
21
21
  ref,
22
22
  "data-slot": "description-list-term",
23
- className: cn("font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase", className),
23
+ className: cn("font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase", className),
24
24
  ...elementProps
25
25
  });
26
26
  }),
@@ -1,6 +1,6 @@
1
1
  import { PreviewGround } from "./_ground.js";
2
2
  import * as React from "react";
3
- import { StyleSet } from "@cueplusplus/tokens";
3
+ import { Paint, StyleSet } from "@cueplusplus/tokens";
4
4
  import { Density as Density$1, FontName, Mode, ThemeName } from "@cueplusplus/theme-base";
5
5
  //#region src/layout/preview.d.ts
6
6
  /** Where the chrome sits and what the stage's ground is. */
@@ -71,6 +71,18 @@ interface PreviewProps extends Omit<React.ComponentPropsWithoutRef<"div">, "chil
71
71
  * `<body>` and inherits nothing from the stage's DOM.
72
72
  */
73
73
  styleSet?: StyleSet;
74
+ /**
75
+ * A look pack for the stage: the finish its surfaces wear.
76
+ *
77
+ * Stamped as `data-paint` on the stage element, and republished to React for
78
+ * the same reason `styleSet` is — a popover opened on the stage mounts on
79
+ * `<body>` and inherits nothing from the stage's DOM, so it has to be told.
80
+ *
81
+ * Absent means inherit. `none` is a value that opts OUT of an ambient pack,
82
+ * which is what a stage uses to show a component unpainted inside a page that
83
+ * is not.
84
+ */
85
+ paint?: Paint;
74
86
  /**
75
87
  * Resize the stage by dragging or with the keyboard. `true` gives both axes
76
88
  * inside the default bounds (160–1280 px wide, 96–960 px tall); an object
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { useIsomorphicLayoutEffect } from "../system/use-isomorphic-layout-effect.js";
4
- import { AmbientThemeContext, DensityContext, StyleSetContext } from "../system/density.js";
4
+ import { AmbientThemeContext, DensityContext, PaintContext, StyleSetContext } from "../system/density.js";
5
5
  import { OverridesContext } from "../system/overrides.js";
6
6
  import { ThemeContext } from "../system/theme-provider.js";
7
7
  import { GROUND_CLASSES } from "./_ground.js";
@@ -241,6 +241,7 @@ function stageAttributes(asked, page) {
241
241
  }
242
242
  if (asked.font !== void 0) attributes["data-font"] = asked.font;
243
243
  if (asked.styleSet !== void 0) attributes["data-style"] = asked.styleSet;
244
+ if (asked.paint !== void 0) attributes["data-paint"] = asked.paint;
244
245
  return attributes;
245
246
  }
246
247
  /**
@@ -258,12 +259,13 @@ function stageAttributes(asked, page) {
258
259
  * route: `useCuePortalProps()` stamps a portal container from `StyleSetContext`,
259
260
  * and a popup opened on the stage inherits nothing from the stage's DOM.
260
261
  */
261
- function StageScope({ theme, mode, density, styleSet, children }) {
262
+ function StageScope({ theme, mode, density, styleSet, paint, children }) {
262
263
  const outer = React.useContext(ThemeContext);
263
264
  const outerOverrides = React.useContext(OverridesContext);
264
265
  const outerDensity = React.useContext(DensityContext);
265
266
  const outerAmbient = React.useContext(AmbientThemeContext);
266
267
  const outerStyleSet = React.useContext(StyleSetContext);
268
+ const outerPaint = React.useContext(PaintContext);
267
269
  const value = React.useMemo(() => {
268
270
  if (outer === null || theme === void 0 && mode === void 0) return outer;
269
271
  const scoped = theme ?? outer.theme;
@@ -286,11 +288,14 @@ function StageScope({ theme, mode, density, styleSet, children }) {
286
288
  value: density ?? outerDensity,
287
289
  children: /* @__PURE__ */ jsx(StyleSetContext.Provider, {
288
290
  value: styleSet ?? outerStyleSet,
289
- children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
290
- value: stamped ? null : outerOverrides,
291
- children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
292
- value,
293
- children
291
+ children: /* @__PURE__ */ jsx(PaintContext.Provider, {
292
+ value: paint ?? outerPaint,
293
+ children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
294
+ value: stamped ? null : outerOverrides,
295
+ children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
296
+ value,
297
+ children
298
+ })
294
299
  })
295
300
  })
296
301
  })
@@ -505,7 +510,7 @@ function Grips({ label, options, size }) {
505
510
  * <Container className="max-w-(--cue-measure-prose)">…</Container>
506
511
  * </Preview>
507
512
  */
508
- const Preview = React.forwardRef(function Preview({ children, label, theme, mode, density, font, styleSet, resizable, tools, footer, presentation = "panel", name, count, ground, captions = true, className, ...elementProps }, ref) {
513
+ const Preview = React.forwardRef(function Preview({ children, label, theme, mode, density, font, styleSet, paint, resizable, tools, footer, presentation = "panel", name, count, ground, captions = true, className, ...elementProps }, ref) {
509
514
  const outer = React.useContext(ThemeContext);
510
515
  const bench = React.useRef(null);
511
516
  const given = resolveResize(resizable);
@@ -517,7 +522,8 @@ const Preview = React.forwardRef(function Preview({ children, label, theme, mode
517
522
  mode,
518
523
  density,
519
524
  font,
520
- styleSet
525
+ styleSet,
526
+ paint
521
527
  }, outer);
522
528
  const framed = presentation === "frame";
523
529
  const stage = /* @__PURE__ */ jsx("div", {
@@ -530,6 +536,7 @@ const Preview = React.forwardRef(function Preview({ children, label, theme, mode
530
536
  mode,
531
537
  density,
532
538
  styleSet,
539
+ paint,
533
540
  children
534
541
  })
535
542
  });
@@ -101,7 +101,7 @@ const Sidebar = {
101
101
  ...elementProps,
102
102
  children: [label === void 0 || collapsed ? null : /* @__PURE__ */ jsx("div", {
103
103
  "data-slot": "sidebar-section-label",
104
- className: "flex h-control-sm items-center px-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase",
104
+ className: "flex h-control-sm items-center px-(--cue-space-2) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase",
105
105
  children: label
106
106
  }), children]
107
107
  });
@@ -43,7 +43,7 @@ const Stepper = React.forwardRef(function Stepper({ className, steps, current, o
43
43
  className: cn("min-w-0", className),
44
44
  ...elementProps,
45
45
  children: /* @__PURE__ */ jsx("ol", {
46
- className: "m-0 flex list-none flex-wrap items-center gap-(--cue-space-3) p-0 font-mono text-(length:--cue-text-label) tracking-[0.1em] uppercase",
46
+ className: "m-0 flex list-none flex-wrap items-center gap-(--cue-space-3) p-0 font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps-tight) uppercase",
47
47
  children: steps.map((step, index) => {
48
48
  const state = currentIndex === -1 || index > currentIndex ? "upcoming" : index === currentIndex ? "current" : "complete";
49
49
  const navigable = onStepChange !== void 0 && state === "complete";
@@ -59,7 +59,7 @@ declare const overlayFooterClasses = "flex items-center justify-end gap-(--cue-s
59
59
  */
60
60
  declare const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-(--radius-control) px-(--cue-space-3) font-mono text-(length:--cue-text-ui) text-fg-muted outline-none select-none data-[highlighted]:bg-accent-soft data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-40";
61
61
  /** The uppercase mono caption over a group of menu rows. */
62
- declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase select-none";
62
+ declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase select-none";
63
63
  /** The hairline between menu groups. */
64
64
  declare const menuSeparatorClasses = "my-(--cue-space-1) h-px bg-border";
65
65
  /** Padding of a menu popup — a single space unit so rows reach the rim. */
@@ -59,7 +59,7 @@ const overlayFooterClasses = "flex items-center justify-end gap-(--cue-space-3)
59
59
  */
60
60
  const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-(--radius-control) px-(--cue-space-3) font-mono text-(length:--cue-text-ui) text-fg-muted outline-none select-none data-[highlighted]:bg-accent-soft data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-40";
61
61
  /** The uppercase mono caption over a group of menu rows. */
62
- const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-[0.15em] text-fg-subtle uppercase select-none";
62
+ const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps) text-fg-subtle uppercase select-none";
63
63
  /** The hairline between menu groups. */
64
64
  const menuSeparatorClasses = "my-(--cue-space-1) h-px bg-border";
65
65
  /** Padding of a menu popup — a single space unit so rows reach the rim. */
@@ -155,7 +155,7 @@ function CommandPalette({ items, onSelect, open: openProp, defaultOpen = false,
155
155
  items: group.items,
156
156
  className: "block",
157
157
  children: [group.value === "" ? null : /* @__PURE__ */ jsx(Combobox.GroupLabel, {
158
- className: "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-micro) tracking-[0.15em] text-fg-subtle uppercase select-none",
158
+ className: "flex h-control-sm items-center px-(--cue-space-3) font-mono text-(length:--cue-text-micro) tracking-(--cue-tracking-caps) text-fg-subtle uppercase select-none",
159
159
  children: group.value
160
160
  }), /* @__PURE__ */ jsx(Combobox.Collection, { children: (item) => /* @__PURE__ */ jsxs(Combobox.Item, {
161
161
  value: item,
@@ -87,7 +87,7 @@ function ToastItem({ toast, className, closeLabel }) {
87
87
  }),
88
88
  /* @__PURE__ */ jsx(Toast.Action, {
89
89
  "data-slot": "toast-action",
90
- className: "mt-(--cue-space-1) inline-flex h-control-sm w-fit cursor-pointer items-center justify-center rounded-(--radius-control) border border-border px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-[0.1em] text-fg-muted uppercase transition-colors outline-none hover:border-border-strong hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent"
90
+ className: "mt-(--cue-space-1) inline-flex h-control-sm w-fit cursor-pointer items-center justify-center rounded-(--radius-control) border border-border px-(--cue-space-3) font-mono text-(length:--cue-text-label) tracking-(--cue-tracking-caps-tight) text-fg-muted uppercase transition-colors outline-none hover:border-border-strong hover:text-fg focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-1 focus-visible:outline-accent"
91
91
  }),
92
92
  /* @__PURE__ */ jsx(Toast.Close, {
93
93
  "aria-label": closeLabel,