@cueplusplus/ui 0.7.0 → 0.9.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 (67) hide show
  1. package/CHANGELOG.md +272 -0
  2. package/README.md +49 -0
  3. package/dist/chat/message-list.js +2 -1
  4. package/dist/configurator/_export.d.ts +1 -1
  5. package/dist/configurator/_export.js +53 -13
  6. package/dist/configurator/_overrides.d.ts +25 -6
  7. package/dist/configurator/_overrides.js +30 -17
  8. package/dist/configurator/configurator.js +8 -3
  9. package/dist/configurator/panel-sections.js +43 -13
  10. package/dist/elements/command-palette.js +1 -1
  11. package/dist/elements/flow-graph.js +2 -2
  12. package/dist/elements/markdown.js +1 -1
  13. package/dist/elements/surfaces.js +4 -3
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.js +2 -1
  16. package/dist/instruments/_ledger-disclosure.js +103 -0
  17. package/dist/instruments/_ledger.d.ts +21 -0
  18. package/dist/instruments/_ledger.js +5 -0
  19. package/dist/instruments/ledger.d.ts +112 -13
  20. package/dist/instruments/ledger.js +128 -38
  21. package/dist/midi/piano-keyboard.js +5 -1
  22. package/dist/primitives/chip.d.ts +1 -1
  23. package/dist/styles.css +23 -1
  24. package/dist/system/density.d.ts +17 -8
  25. package/dist/system/density.js +39 -16
  26. package/dist/system/index.d.ts +5 -2
  27. package/dist/system/index.js +2 -1
  28. package/dist/system/overrides.d.ts +43 -0
  29. package/dist/system/overrides.js +238 -0
  30. package/dist/system/portal.d.ts +4 -2
  31. package/dist/system/portal.js +34 -3
  32. package/dist/system/prepaint.d.ts +58 -7
  33. package/dist/system/prepaint.js +72 -20
  34. package/dist/system/theme-provider.d.ts +133 -8
  35. package/dist/system/theme-provider.js +203 -72
  36. package/dist/system/theme-registry.d.ts +53 -0
  37. package/dist/system/theme-registry.js +66 -0
  38. package/dist/system/use-density.d.ts +13 -5
  39. package/dist/system/use-density.js +142 -13
  40. package/dist/system/use-theme.d.ts +5 -3
  41. package/dist/system/use-theme.js +5 -3
  42. package/dist/system/vocabulary.d.ts +15 -0
  43. package/dist/system/vocabulary.js +111 -0
  44. package/dist/theming/contrast.d.ts +2 -122
  45. package/dist/theming/contrast.js +2 -194
  46. package/dist/theming/create-theme.d.ts +37 -11
  47. package/dist/theming/create-theme.js +54 -17
  48. package/dist/theming/index.d.ts +3 -4
  49. package/dist/theming/index.js +3 -4
  50. package/dist/theming/serialize.d.ts +24 -11
  51. package/dist/theming/serialize.js +16 -18
  52. package/manifest/components/colors-section.json +2 -3
  53. package/manifest/components/cue-portal-frame.json +1 -1
  54. package/manifest/components/data-tree.json +1 -0
  55. package/manifest/components/density.json +1 -1
  56. package/manifest/components/export-dialog.json +0 -3
  57. package/manifest/components/ledger.json +82 -7
  58. package/manifest/components/preset-section.json +2 -3
  59. package/manifest/components/shape-section.json +2 -3
  60. package/manifest/components/theme-configurator.json +0 -3
  61. package/manifest/components/theme-provider.json +24 -9
  62. package/manifest/components/token-editor.json +0 -3
  63. package/manifest/manifest.json +148 -27
  64. package/manifest/tokens.json +121 -11
  65. package/package.json +15 -6
  66. package/dist/theming/_presets.d.ts +0 -11
  67. package/dist/theming/_presets.js +0 -678
@@ -1,4 +1,6 @@
1
1
  import { cn } from "../lib/cn.js";
2
+ import { headingTag } from "./_ledger.js";
3
+ import { LedgerDisclosure } from "./_ledger-disclosure.js";
2
4
  import * as React from "react";
3
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
6
  //#region src/instruments/ledger.tsx
@@ -12,24 +14,68 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
14
  * micro-label in a dense list reads as a second kind of emphasis.
13
15
  */
14
16
  const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] text-fg-subtle uppercase";
15
- /** `headingLevel` as a tag name, so the outline is real elements, not roles. */
16
- const headingTag = (level) => level === 2 ? "h2" : level === 3 ? "h3" : "h4";
17
+ /**
18
+ * The disclosure button a collapsible heading holds.
19
+ *
20
+ * Worn *with* {@link LEDGER_LABEL}, which the heading around it is already
21
+ * wearing. The repetition is deliberate: a `<button>` carries the UA's own font
22
+ * rather than its parent's, and while Tailwind's preflight resets that to
23
+ * `font: inherit`, a component whose micro-label silently becomes 13px Arial if
24
+ * a consumer drops preflight is a component with a very confusing bug report.
25
+ * One constant applied twice cannot drift; two recipes can.
26
+ *
27
+ * `w-full` so the whole width of the heading is the hit area rather than the
28
+ * word itself, and the focus ring is inset (`-outline-offset-2`) for the same
29
+ * reason `_data-row.ts` gives: these headings dock at the top of clipped panes,
30
+ * where an outset ring on the first one is cut off by the pane's own rim.
31
+ */
32
+ const LEDGER_TOGGLE = "flex w-full min-w-0 cursor-pointer items-baseline gap-(--cue-space-2) text-left outline-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-accent";
17
33
  /**
18
34
  * A grouped run of dense rows under sticky headings: the ledger idiom.
19
35
  *
20
- * The flattest of the three surfaces this package ships over one row model, and
21
- * the one that does the least: headings, slugs, rows. **Nothing here collapses.**
22
- * A ledger with a disclosure on it would be a tree with worse semantics, so the
23
- * component emits no `aria-expanded` anywhere, and a test holds it to that —
24
- * collapse belongs to `DataTree`, and grouping-with-columns belongs to a
25
- * grouped `Table`.
36
+ * The flattest of the three surfaces this package ships over one row model:
37
+ * headings, slugs, rows. What it owns above all is the document outline. Tiers
38
+ * and groups are real `<h2>` / `<h3>` elements at caller-chosen levels, because
39
+ * in a list of two hundred rows the heading list *is* how a screen-reader user
40
+ * navigates.
41
+ *
42
+ * ## Folding: the accordion pattern, opt-in, and why it lives here
43
+ *
44
+ * This component used to refuse disclosure outright, on the grounds that *a
45
+ * ledger which folds is a tree with worse semantics*. That ruling was right
46
+ * about rows and wrong about places, and the distinction is the whole of
47
+ * {@link LedgerTierProps.collapsible}:
26
48
  *
27
- * What it does own is the document outline. Tiers and groups are real `<h2>` /
28
- * `<h3>` elements at caller-chosen levels, because in a list of two hundred
29
- * rows the heading list *is* how a screen-reader user navigates.
49
+ * - **`DataTree` folds a row.** Its `role="treeitem"` rows carry their own
50
+ * `aria-expanded`, its chevron is `role="presentation"` because in tree
51
+ * semantics the row owns its expansion, and a tier and a subgroup drawn
52
+ * through it stop being places and become rows. A consumer who measured this
53
+ * exact shape through `DataTree` counted **zero** `h2`/`h3` elements and zero
54
+ * `button[aria-expanded]`, which is `DataTree` working correctly and still
55
+ * not being what a grouped document needs.
56
+ * - **`Ledger` folds a place.** A tier is already an `<h2>`, which is already
57
+ * the right element for it, so the disclosure goes *inside* the heading as a
58
+ * real `<button aria-expanded aria-controls>` — the accordion pattern, and
59
+ * the same one `Table.GroupRow`'s note has always pointed at for a table that
60
+ * genuinely needs to fold. The outline is untouched: the heading list of a
61
+ * folding ledger and a plain one are identical, which is what lets a page
62
+ * offer both idioms over one dataset without the outline moving under a
63
+ * reader who switches.
30
64
  *
31
- * Static markup — no `"use client"`. The rows inside it may be anything;
32
- * `DataRow` is the intended tenant.
65
+ * So it is one component with two forms rather than a `DisclosureLedger` beside
66
+ * it: everything a folding ledger needs — the gutter track, the sticky offsets,
67
+ * the label recipe, the type contract — is this component, and a sibling would
68
+ * have been a copy of all of it wrapped around one `<button>`.
69
+ *
70
+ * Expansion is controllable and stores nothing. *Which* places a reader has
71
+ * folded is a fact about the reader; consumers keep that set themselves and
72
+ * pass it back in. A folded run of rows is hidden rather than unmounted, so a
73
+ * group that keeps its own fold inside a tier still keeps it after the tier has
74
+ * been shut and opened over it.
75
+ *
76
+ * Static markup — no `"use client"` — **until a heading is asked to fold**, at
77
+ * which point that heading and its rows become a small client island and the
78
+ * rest of the ledger keeps rendering on the server.
33
79
  *
34
80
  * @example
35
81
  * <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
@@ -42,6 +88,18 @@ const headingTag = (level) => level === 2 ? "h2" : level === 3 ? "h3" : "h4";
42
88
  * </Ledger.Group>
43
89
  * </Ledger.Tier>
44
90
  * </Ledger.Root>
91
+ *
92
+ * @example
93
+ * // Folded places are the reader's, so the reader's storage holds them.
94
+ * <Ledger.Tier
95
+ * heading="Ours"
96
+ * count={13}
97
+ * collapsible
98
+ * expanded={!folded.includes("ours")}
99
+ * onExpandedChange={(open) => remember("ours", open)}
100
+ * >
101
+ * <Ledger.Group label="released">…</Ledger.Group>
102
+ * </Ledger.Tier>
45
103
  */
46
104
  const Ledger = {
47
105
  Root: React.forwardRef(function LedgerRoot({ className, containment = true, ...elementProps }, ref) {
@@ -52,53 +110,85 @@ const Ledger = {
52
110
  ...elementProps
53
111
  });
54
112
  }),
55
- Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, children, ...elementProps }, ref) {
113
+ Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, collapsible = false, expanded, defaultExpanded = true, onExpandedChange, children, ...elementProps }, ref) {
56
114
  const Heading = headingTag(headingLevel);
57
- return /* @__PURE__ */ jsxs("section", {
115
+ const words = /* @__PURE__ */ jsxs(Fragment, { children: [
116
+ /* @__PURE__ */ jsx("span", {
117
+ "data-slot": "ledger-tier-label",
118
+ children: heading
119
+ }),
120
+ count === void 0 ? null : /* @__PURE__ */ jsx("span", {
121
+ "data-slot": "ledger-tier-count",
122
+ className: "tabular-nums",
123
+ children: count
124
+ }),
125
+ note === void 0 ? null : /* @__PURE__ */ jsx("span", {
126
+ "data-slot": "ledger-tier-note",
127
+ className: "tracking-normal normal-case",
128
+ children: note
129
+ })
130
+ ] });
131
+ const ground = sticky ? "sticky top-0 z-2 bg-bg" : null;
132
+ return /* @__PURE__ */ jsx("section", {
58
133
  ref,
59
134
  "data-slot": "ledger-tier",
60
135
  className: cn("flex flex-col", className),
61
136
  ...elementProps,
62
- children: [heading === void 0 ? null : /* @__PURE__ */ jsxs(Heading, {
137
+ children: heading === void 0 ? children : collapsible ? /* @__PURE__ */ jsx(LedgerDisclosure, {
138
+ headingLevel,
139
+ headingSlot: "ledger-tier-heading",
140
+ headingClassName: cn(LEDGER_LABEL, "py-(--cue-pad-row-y)", ground),
141
+ toggleSlot: "ledger-tier-toggle",
142
+ toggleClassName: cn(LEDGER_LABEL, LEDGER_TOGGLE),
143
+ panelSlot: "ledger-tier-rows",
144
+ panelClassName: "flex flex-col",
145
+ label: words,
146
+ expanded,
147
+ defaultExpanded,
148
+ onExpandedChange,
149
+ children
150
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
63
151
  "data-slot": "ledger-tier-heading",
64
- className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)", sticky ? "sticky top-0 z-2 bg-bg" : null),
65
- children: [
66
- /* @__PURE__ */ jsx("span", {
67
- "data-slot": "ledger-tier-label",
68
- children: heading
69
- }),
70
- count === void 0 ? null : /* @__PURE__ */ jsx("span", {
71
- "data-slot": "ledger-tier-count",
72
- className: "tabular-nums",
73
- children: count
74
- }),
75
- note === void 0 ? null : /* @__PURE__ */ jsx("span", {
76
- "data-slot": "ledger-tier-note",
77
- className: "tracking-normal normal-case",
78
- children: note
79
- })
80
- ]
81
- }), children]
152
+ className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)", ground),
153
+ children: words
154
+ }), children] })
82
155
  });
83
156
  }),
84
- Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, children, ...elementProps }, ref) {
157
+ Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, collapsible = false, expanded, defaultExpanded = true, onExpandedChange, children, ...elementProps }, ref) {
85
158
  const Heading = headingTag(headingLevel);
86
159
  const labelled = label !== void 0;
160
+ const ground = sticky ? "sticky self-start [top:var(--cue-ledger-slug-top,2.5rem)] bg-bg" : null;
87
161
  return /* @__PURE__ */ jsx("div", {
88
162
  ref,
89
163
  "data-slot": "ledger-group",
90
164
  "data-labelled": labelled ? "" : void 0,
91
165
  className: cn(labelled ? "grid [grid-template-columns:var(--cue-ledger-gutter,7rem)_minmax(0,1fr)]" : "flex flex-col", className),
92
166
  ...elementProps,
93
- children: labelled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
167
+ children: !labelled ? children : collapsible ? /* @__PURE__ */ jsx(LedgerDisclosure, {
168
+ headingLevel,
169
+ headingSlot: "ledger-group-label",
170
+ headingClassName: cn(LEDGER_LABEL, "min-w-0 py-(--cue-pad-row-y) pr-(--cue-space-3)", ground),
171
+ toggleSlot: "ledger-group-toggle",
172
+ toggleClassName: cn(LEDGER_LABEL, LEDGER_TOGGLE),
173
+ panelSlot: "ledger-group-rows",
174
+ panelClassName: "flex min-w-0 flex-col",
175
+ label: /* @__PURE__ */ jsx("span", {
176
+ className: "min-w-0 truncate",
177
+ children: label
178
+ }),
179
+ expanded,
180
+ defaultExpanded,
181
+ onExpandedChange,
182
+ children
183
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
94
184
  "data-slot": "ledger-group-label",
95
- className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)", sticky ? "sticky self-start [top:var(--cue-ledger-slug-top,2.5rem)] bg-bg" : null),
185
+ className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)", ground),
96
186
  children: label
97
187
  }), /* @__PURE__ */ jsx("div", {
98
188
  "data-slot": "ledger-group-rows",
99
189
  className: "flex min-w-0 flex-col",
100
190
  children
101
- })] }) : children
191
+ })] })
102
192
  });
103
193
  }),
104
194
  Empty: React.forwardRef(function LedgerEmpty({ className, ...elementProps }, ref) {
@@ -49,7 +49,11 @@ const PianoKeyboard = React.forwardRef(function PianoKeyboard({ className, range
49
49
  const [drag, setDrag] = React.useState(null);
50
50
  const settled = React.useMemo(() => keyboardWindow(range, octaves), [range, octaves]);
51
51
  const view = drag?.window ?? settled;
52
- const layout = React.useMemo(() => keyboardLayout(view), [view.start, view.end]);
52
+ const { start: viewStart, end: viewEnd } = view;
53
+ const layout = React.useMemo(() => keyboardLayout({
54
+ start: viewStart,
55
+ end: viewEnd
56
+ }), [viewStart, viewEnd]);
53
57
  const lit = React.useMemo(() => new Set(highlight ?? []), [highlight]);
54
58
  const setEdge = (edge, note) => {
55
59
  const next = edge === "start" ? {
@@ -30,7 +30,7 @@ import * as React from "react";
30
30
  * <span className={chipVariants({ variant: "tag" })}>mcp</span>
31
31
  */
32
32
  declare const chipVariants: (props?: ({
33
- tone?: "danger" | "info" | "warn" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
33
+ tone?: "info" | "warn" | "danger" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
34
34
  variant?: "outline" | "tinted" | "tag" | null | undefined;
35
35
  interactive?: boolean | null | undefined;
36
36
  } & ClassProp) | undefined) => string;
package/dist/styles.css CHANGED
@@ -3,13 +3,35 @@
3
3
  * Consumer app CSS:
4
4
  * @import "tailwindcss";
5
5
  * @import "@cueplusplus/ui/styles.css";
6
+ * @import "@cueplusplus/theme-cue/theme.css"; (or any theme package)
6
7
  *
7
8
  * Tailwind v4 does not scan node_modules, so the library registers its own
8
9
  * sources: `@source "./"` resolves relative to THIS file, i.e. the shipped
9
10
  * dist/, which is where the class strings live after the build.
10
11
  */
11
12
 
12
- @import "@cueplusplus/tokens/theme.css";
13
+ /* Two lines, and the first one is the release's whole behaviour change.
14
+ *
15
+ * It used to be `@cueplusplus/tokens/theme.css`, which carried ten palettes and
16
+ * painted `cue` on a bare `:root`. A palette is a package now, so what this
17
+ * library ships is the *blank* base: `theme-base/base.css` declares every colour
18
+ * token at a neutral default and imports `@cueplusplus/tokens/axes.css` for the
19
+ * geometry and type axes. That is why there is no third line — `base.css`
20
+ * carries the axes with it, and `tailwind.css` maps utilities onto token names
21
+ * and pulls nothing.
22
+ *
23
+ * An app that imports this and registers no theme therefore paints greys and one
24
+ * desaturated accent, rather than somebody's brand. To get a palette back:
25
+ * install a theme package, import its stylesheet after this one, and pass its
26
+ * manifest to the provider —
27
+ *
28
+ * @import "@cueplusplus/ui/styles.css";
29
+ * @import "@cueplusplus/theme-cue/theme.css";
30
+ *
31
+ * with `<ThemeProvider themes={[cue]} theme="cue">`. Order matters and is not an
32
+ * accident: `[data-theme]` and `:root` tie at (0,1,0), so a theme's block wins
33
+ * by being declared later. */
34
+ @import "@cueplusplus/theme-base/base.css";
13
35
  @import "@cueplusplus/tokens/tailwind.css";
14
36
 
15
37
  @source "./";
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { Density } from "@cueplusplus/tokens";
2
+ import { Density } from "@cueplusplus/theme-base";
3
3
  //#region src/system/density.d.ts
4
4
  interface DensityProps {
5
5
  /**
@@ -17,14 +17,23 @@ interface DensityProps {
17
17
  /**
18
18
  * Re-scope any subtree to a different density level.
19
19
  *
20
- * Renders a plain `<div data-density="…">`: the geometry custom properties are
21
- * re-declared by the attribute selector and inherit down, so nesting is free and
22
- * needs no JS. The matching React context is published alongside for the rare
23
- * JS reads (`useDensity()`, `useControlHeight()`, portal stamping) — the DOM
24
- * attribute, not the context, is what actually restyles the tree.
20
+ * Renders a plain `<div data-density="…" data-cue-theme="…">`: the geometry
21
+ * custom properties are re-declared by the attribute selector and inherit
22
+ * down, so nesting is free and needs no JS. The matching React context is
23
+ * published alongside for the rare JS reads (`useDensity()`,
24
+ * `useControlHeight()`, portal stamping) — the DOM attributes, not the
25
+ * context, are what actually restyle the tree.
25
26
  *
26
- * Theme and mode are deliberately *not* re-scoped here: density is the only axis
27
- * an island may override (spec §6).
27
+ * `data-cue-theme` carries the nearest `<ThemeProvider>`'s name, or is
28
+ * omitted where there is none. It is read by nothing this island does —
29
+ * theme and mode are deliberately *not* re-scoped here, density is the only
30
+ * axis an island may override (spec §6) — it exists so a theme's own
31
+ * generated CSS can address exactly this island regardless of how many
32
+ * *other* themed providers sit between it and the one it belongs to. A CSS
33
+ * ancestor selector can only ask "does some ancestor carry theme `x`", never
34
+ * "is `x` the *nearest* one"; this stamp answers that question the way
35
+ * `useContext` already does, at render time, and hands the answer to CSS as
36
+ * an attribute rather than leaving it unanswerable there.
28
37
  *
29
38
  * @example
30
39
  * <Density density="ultra-compact">
@@ -10,14 +10,6 @@ import { jsx } from "react/jsx-runtime";
10
10
  * but the level a screen gets when nobody chose is the dense one.
11
11
  */
12
12
  const DEFAULT_DENSITY = "compact";
13
- /** The five levels, as a runtime array for validating persisted input. */
14
- const DENSITY_LEVELS = [
15
- "ultra-compact",
16
- "compact",
17
- "normal",
18
- "large",
19
- "ultra-large"
20
- ];
21
13
  /**
22
14
  * Nearest ambient density. `null` means "nothing has stamped a level yet", which
23
15
  * `useDensity()` reports as {@link DEFAULT_DENSITY}.
@@ -26,16 +18,45 @@ const DENSITY_LEVELS = [
26
18
  */
27
19
  const DensityContext = React.createContext(null);
28
20
  /**
21
+ * Nearest ambient theme's name, published for exactly one reader: `Density`
22
+ * below, which stamps it onto its island as `data-cue-theme` so a theme's
23
+ * CSS can tell *nearest* theme from *any* theme in the ancestor chain (see
24
+ * `densityRules`/`themeSelector`'s comment). `use-density.ts`'s measuring
25
+ * probe stamps the same attribute from the identical value, read through
26
+ * `ThemeContext` there instead, because it already imports that module with
27
+ * no cycle.
28
+ *
29
+ * Defined here, not in `theme-provider.tsx`, and re-exported from nowhere
30
+ * else. `theme-provider.tsx` already imports `DensityContext` from this
31
+ * module; a context this small living over there and imported back here
32
+ * would cycle the two modules. `null` outside a provider, the same as
33
+ * `ThemeContext`'s own theme field would be if it existed with no provider
34
+ * to require — `Density` used bare, with nothing above it, stamps nothing.
35
+ *
36
+ * Internal: `Density` is the only reader. Not `useTheme()` — that throws
37
+ * outside a provider, and this island has no reason to.
38
+ */
39
+ const AmbientThemeContext = React.createContext(null);
40
+ /**
29
41
  * Re-scope any subtree to a different density level.
30
42
  *
31
- * Renders a plain `<div data-density="…">`: the geometry custom properties are
32
- * re-declared by the attribute selector and inherit down, so nesting is free and
33
- * needs no JS. The matching React context is published alongside for the rare
34
- * JS reads (`useDensity()`, `useControlHeight()`, portal stamping) — the DOM
35
- * attribute, not the context, is what actually restyles the tree.
43
+ * Renders a plain `<div data-density="…" data-cue-theme="…">`: the geometry
44
+ * custom properties are re-declared by the attribute selector and inherit
45
+ * down, so nesting is free and needs no JS. The matching React context is
46
+ * published alongside for the rare JS reads (`useDensity()`,
47
+ * `useControlHeight()`, portal stamping) — the DOM attributes, not the
48
+ * context, are what actually restyle the tree.
36
49
  *
37
- * Theme and mode are deliberately *not* re-scoped here: density is the only axis
38
- * an island may override (spec §6).
50
+ * `data-cue-theme` carries the nearest `<ThemeProvider>`'s name, or is
51
+ * omitted where there is none. It is read by nothing this island does —
52
+ * theme and mode are deliberately *not* re-scoped here, density is the only
53
+ * axis an island may override (spec §6) — it exists so a theme's own
54
+ * generated CSS can address exactly this island regardless of how many
55
+ * *other* themed providers sit between it and the one it belongs to. A CSS
56
+ * ancestor selector can only ask "does some ancestor carry theme `x`", never
57
+ * "is `x` the *nearest* one"; this stamp answers that question the way
58
+ * `useContext` already does, at render time, and hands the answer to CSS as
59
+ * an attribute rather than leaving it unanswerable there.
39
60
  *
40
61
  * @example
41
62
  * <Density density="ultra-compact">
@@ -43,10 +64,12 @@ const DensityContext = React.createContext(null);
43
64
  * </Density>
44
65
  */
45
66
  function Density({ density, children, className, style }) {
67
+ const theme = React.useContext(AmbientThemeContext);
46
68
  return /* @__PURE__ */ jsx(DensityContext.Provider, {
47
69
  value: density,
48
70
  children: /* @__PURE__ */ jsx("div", {
49
71
  "data-density": density,
72
+ "data-cue-theme": theme ?? void 0,
50
73
  className,
51
74
  style,
52
75
  children
@@ -54,4 +77,4 @@ function Density({ density, children, className, style }) {
54
77
  });
55
78
  }
56
79
  //#endregion
57
- export { DEFAULT_DENSITY, DENSITY_LEVELS, Density, DensityContext };
80
+ export { AmbientThemeContext, DEFAULT_DENSITY, Density, DensityContext };
@@ -1,8 +1,11 @@
1
+ import { TokenOverrides } from "./overrides.js";
1
2
  import { FontFamilies, ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps } from "./theme-provider.js";
2
3
  import { Density, DensityProps } from "./density.js";
3
4
  import { CuePortalFrame, CuePortalFrameProps, useCuePortalProps } from "./portal.js";
4
5
  import { DEFAULT_STORAGE_KEY, PersistedPreferences, PrepaintDefaults, PrepaintOptions, prepaintScript } from "./prepaint.js";
6
+ import { ThemeRegistry } from "./vocabulary.js";
7
+ import { useDensities, useFonts, useThemes } from "./theme-registry.js";
5
8
  import { ControlSize, useControlHeight, useDensity } from "./use-density.js";
6
9
  import { useTheme } from "./use-theme.js";
7
- import { Density as DensityLevel, FontName, Mode, ThemeName } from "@cueplusplus/tokens";
8
- export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityLevel, type DensityProps, type FontFamilies, type FontName, type Mode, type PersistedPreferences, type PrepaintDefaults, type PrepaintOptions, type ResolvedMode, type ThemeContextValue, type ThemeName, ThemeProvider, type ThemeProviderProps, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
10
+ import { Density as DensityLevel, DensityEntry, FontEntry, FontName, Mode, ThemeManifest, ThemeName } from "@cueplusplus/theme-base";
11
+ export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityEntry, type DensityLevel, type DensityProps, type FontEntry, type FontFamilies, type FontName, type Mode, type PersistedPreferences, type PrepaintDefaults, type PrepaintOptions, type ResolvedMode, type ThemeContextValue, type ThemeManifest, type ThemeName, ThemeProvider, type ThemeProviderProps, type ThemeRegistry, type TokenOverrides, prepaintScript, useControlHeight, useCuePortalProps, useDensities, useDensity, useFonts, useTheme, useThemes };
@@ -1,7 +1,8 @@
1
1
  import { Density } from "./density.js";
2
2
  import { DEFAULT_STORAGE_KEY, prepaintScript } from "./prepaint.js";
3
+ import { useDensities, useFonts, useThemes } from "./theme-registry.js";
3
4
  import { ThemeProvider } from "./theme-provider.js";
4
5
  import { CuePortalFrame, useCuePortalProps } from "./portal.js";
5
6
  import { useControlHeight, useDensity } from "./use-density.js";
6
7
  import { useTheme } from "./use-theme.js";
7
- export { CuePortalFrame, DEFAULT_STORAGE_KEY, Density, ThemeProvider, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
8
+ export { CuePortalFrame, DEFAULT_STORAGE_KEY, Density, ThemeProvider, prepaintScript, useControlHeight, useCuePortalProps, useDensities, useDensity, useFonts, useTheme, useThemes };
@@ -0,0 +1,43 @@
1
+ import "react";
2
+ import { ColorToken, GeometryToken, ThemeManifest } from "@cueplusplus/theme-base";
3
+ //#region src/system/overrides.d.ts
4
+ /**
5
+ * A typed edit over the active theme.
6
+ *
7
+ * Not `ThemeOverrides`: `@cueplusplus/ui/configurator` already exports a type
8
+ * by that name — the serialised edit snapshot its panel writes — and the root
9
+ * barrel re-exports `./system` wholesale, so two public types under one name in
10
+ * one package would be an alias every consumer of both subpaths has to write.
11
+ * This one overrides *tokens*, which is what it is named for.
12
+ *
13
+ * Every key is checked against the token contracts, so a typo is a compile
14
+ * error rather than a custom property nothing reads.
15
+ */
16
+ interface TokenOverrides {
17
+ /**
18
+ * Colour tokens, per block. Only the block the resolved mode is in is
19
+ * emitted, so a provider under `mode="light"` never publishes the dark edit —
20
+ * which is what makes the two independent rather than a merge.
21
+ */
22
+ colors?: {
23
+ dark?: Partial<Record<ColorToken, string>>;
24
+ light?: Partial<Record<ColorToken, string>>;
25
+ };
26
+ /**
27
+ * Geometry, per rung. The key is a rung name — one of the base five, or one a
28
+ * registered theme adds — and the value only the tokens that move.
29
+ */
30
+ densities?: Partial<Record<string, Partial<Record<GeometryToken, string>>>>;
31
+ /**
32
+ * Font stacks. These write `--cue-font-sans` / `--cue-font-mono` /
33
+ * `--cue-font-display` directly, and so shadow the pairing on `data-font` for
34
+ * the subtree — see `ThemeProviderProps.overrides`.
35
+ */
36
+ fonts?: Partial<{
37
+ sans: string;
38
+ mono: string;
39
+ display: string;
40
+ }>;
41
+ }
42
+ //#endregion
43
+ export { TokenOverrides };