@cueplusplus/ui 0.8.0 → 0.10.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 (352) hide show
  1. package/CHANGELOG.md +457 -0
  2. package/README.md +131 -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 +7 -3
  15. package/dist/index.js +5 -2
  16. package/dist/instruments/meter.d.ts +5 -1
  17. package/dist/layout/bento.d.ts +144 -0
  18. package/dist/layout/bento.js +266 -0
  19. package/dist/layout/index.d.ts +3 -2
  20. package/dist/layout/index.js +3 -2
  21. package/dist/midi/piano-keyboard.js +5 -1
  22. package/dist/primitives/button.d.ts +12 -0
  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 +3 -1
  28. package/dist/system/overrides.d.ts +80 -0
  29. package/dist/system/overrides.js +277 -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/accordion.json +24 -6
  53. package/manifest/components/activity-graph.json +20 -5
  54. package/manifest/components/agent-card.json +37 -9
  55. package/manifest/components/agent-handoff.json +21 -5
  56. package/manifest/components/agent-mode-badge.json +22 -4
  57. package/manifest/components/agent-pile.json +38 -8
  58. package/manifest/components/agent-plan.json +9 -2
  59. package/manifest/components/agent-status.json +17 -3
  60. package/manifest/components/agent-surface.json +26 -11
  61. package/manifest/components/alert-dialog.json +29 -5
  62. package/manifest/components/animated-number.json +14 -6
  63. package/manifest/components/app-bar.json +66 -13
  64. package/manifest/components/app-shell.json +133 -31
  65. package/manifest/components/app-window-frame.json +28 -6
  66. package/manifest/components/approval-card.json +37 -7
  67. package/manifest/components/artifact-card.json +16 -4
  68. package/manifest/components/ask-box.json +99 -21
  69. package/manifest/components/audience-icon.json +1 -7
  70. package/manifest/components/autocomplete.json +80 -18
  71. package/manifest/components/avatar-group.json +13 -2
  72. package/manifest/components/avatar.json +22 -4
  73. package/manifest/components/background-inbox.json +9 -2
  74. package/manifest/components/bento.json +306 -0
  75. package/manifest/components/branch-picker.json +29 -7
  76. package/manifest/components/breadcrumb.json +13 -3
  77. package/manifest/components/button-group.json +21 -4
  78. package/manifest/components/button.json +41 -4
  79. package/manifest/components/canvas-split-body.json +4 -1
  80. package/manifest/components/canvas-split-header.json +17 -4
  81. package/manifest/components/canvas-split-line.json +4 -1
  82. package/manifest/components/canvas-split-message.json +8 -1
  83. package/manifest/components/card.json +14 -3
  84. package/manifest/components/carousel.json +54 -12
  85. package/manifest/components/catalogue-icon.json +1 -7
  86. package/manifest/components/channel-beta-icon.json +1 -7
  87. package/manifest/components/channel-matrix.json +80 -15
  88. package/manifest/components/channel-released-icon.json +1 -7
  89. package/manifest/components/chart-container.json +26 -10
  90. package/manifest/components/chart-ramp.json +13 -3
  91. package/manifest/components/chart-swatch.json +9 -2
  92. package/manifest/components/chart-tooltip-content.json +31 -7
  93. package/manifest/components/chart.json +30 -6
  94. package/manifest/components/chat-panel-composer.json +9 -2
  95. package/manifest/components/checkbox-group.json +4 -2
  96. package/manifest/components/checkbox.json +17 -4
  97. package/manifest/components/checkpoint-history.json +13 -3
  98. package/manifest/components/chip.json +35 -5
  99. package/manifest/components/clamp.json +30 -7
  100. package/manifest/components/cli-tool-icon.json +1 -7
  101. package/manifest/components/code-diff.json +20 -5
  102. package/manifest/components/code-runner.json +32 -6
  103. package/manifest/components/collapsible.json +16 -4
  104. package/manifest/components/color-area.json +4 -1
  105. package/manifest/components/color-field.json +13 -3
  106. package/manifest/components/color-picker.json +75 -17
  107. package/manifest/components/color-slider.json +12 -3
  108. package/manifest/components/color-swatch.json +13 -2
  109. package/manifest/components/colors-section.json +10 -7
  110. package/manifest/components/combobox.json +81 -18
  111. package/manifest/components/command-palette.json +57 -13
  112. package/manifest/components/compaction-row.json +35 -7
  113. package/manifest/components/comparison-card.json +17 -4
  114. package/manifest/components/composer-attachment-chip.json +9 -2
  115. package/manifest/components/composer-bar.json +4 -1
  116. package/manifest/components/composer-command-item.json +8 -2
  117. package/manifest/components/composer-context.json +4 -1
  118. package/manifest/components/composer-input.json +5 -1
  119. package/manifest/components/composer-menu-item.json +4 -1
  120. package/manifest/components/composer-menu.json +12 -2
  121. package/manifest/components/composer-model-item.json +8 -2
  122. package/manifest/components/composer-model-trigger.json +8 -2
  123. package/manifest/components/composer-person-item.json +8 -2
  124. package/manifest/components/composer-send.json +8 -2
  125. package/manifest/components/composer-voice-button.json +4 -1
  126. package/manifest/components/composer-voice.json +8 -2
  127. package/manifest/components/composer.json +76 -17
  128. package/manifest/components/computer-use.json +17 -4
  129. package/manifest/components/confidence-marker.json +13 -3
  130. package/manifest/components/connection-state.json +23 -4
  131. package/manifest/components/container.json +14 -2
  132. package/manifest/components/context-breakdown.json +8 -2
  133. package/manifest/components/context-menu.json +16 -4
  134. package/manifest/components/context-usage.json +16 -4
  135. package/manifest/components/conversation-search.json +22 -5
  136. package/manifest/components/copy-button.json +32 -7
  137. package/manifest/components/cost-meter.json +12 -3
  138. package/manifest/components/cue-logotype.json +17 -3
  139. package/manifest/components/cue-mark.json +17 -3
  140. package/manifest/components/cue-portal-frame.json +14 -4
  141. package/manifest/components/data-row.json +21 -7
  142. package/manifest/components/data-table-pagination.json +18 -4
  143. package/manifest/components/data-table-toolbar.json +30 -7
  144. package/manifest/components/data-tree.json +42 -9
  145. package/manifest/components/date-field.json +13 -2
  146. package/manifest/components/date-picker.json +81 -19
  147. package/manifest/components/date-range-picker.json +81 -19
  148. package/manifest/components/day-separator.json +4 -1
  149. package/manifest/components/delegation-card.json +64 -12
  150. package/manifest/components/density.json +25 -5
  151. package/manifest/components/description-list.json +8 -1
  152. package/manifest/components/diagram.json +33 -7
  153. package/manifest/components/dialog.json +29 -6
  154. package/manifest/components/disclosure.json +31 -7
  155. package/manifest/components/dmx-bar.json +16 -4
  156. package/manifest/components/dmx-strip.json +20 -5
  157. package/manifest/components/document-reference.json +21 -5
  158. package/manifest/components/draft-restore.json +18 -4
  159. package/manifest/components/drawer.json +30 -6
  160. package/manifest/components/dropdown-menu.json +55 -10
  161. package/manifest/components/edit-message.json +32 -7
  162. package/manifest/components/elements-command-palette.json +27 -6
  163. package/manifest/components/elements-data-table.json +9 -2
  164. package/manifest/components/elements-timeline.json +8 -2
  165. package/manifest/components/elicitation-form.json +31 -6
  166. package/manifest/components/empty-state-composer.json +9 -2
  167. package/manifest/components/empty-state-suggestion.json +4 -1
  168. package/manifest/components/empty-state.json +25 -5
  169. package/manifest/components/end-of-turn-summary.json +16 -4
  170. package/manifest/components/env-var-input.json +54 -12
  171. package/manifest/components/error-state.json +17 -4
  172. package/manifest/components/export-dialog.json +27 -11
  173. package/manifest/components/eyebrow.json +4 -1
  174. package/manifest/components/feedback-dialog.json +33 -7
  175. package/manifest/components/field-description.json +4 -1
  176. package/manifest/components/field-error.json +13 -3
  177. package/manifest/components/field-label.json +4 -1
  178. package/manifest/components/field.json +4 -1
  179. package/manifest/components/file-tree.json +16 -4
  180. package/manifest/components/file-upload.json +58 -13
  181. package/manifest/components/flow-graph.json +12 -3
  182. package/manifest/components/folder-icon.json +1 -7
  183. package/manifest/components/footer.json +24 -4
  184. package/manifest/components/frac.json +10 -2
  185. package/manifest/components/generation-loader.json +17 -3
  186. package/manifest/components/generative-ui.json +39 -7
  187. package/manifest/components/grid.json +56 -5
  188. package/manifest/components/group-bar.json +25 -6
  189. package/manifest/components/guardrail-notice.json +22 -5
  190. package/manifest/components/hover-card.json +45 -8
  191. package/manifest/components/icon-button.json +22 -4
  192. package/manifest/components/image-generation.json +8 -2
  193. package/manifest/components/info-tip.json +42 -7
  194. package/manifest/components/inline-citation.json +14 -3
  195. package/manifest/components/input-group.json +24 -5
  196. package/manifest/components/input.json +13 -3
  197. package/manifest/components/item.json +42 -8
  198. package/manifest/components/job-progress.json +25 -6
  199. package/manifest/components/launcher-bubble.json +32 -7
  200. package/manifest/components/ledger.json +87 -19
  201. package/manifest/components/link.json +16 -3
  202. package/manifest/components/live-region-announcer.json +21 -5
  203. package/manifest/components/log-viewer.json +42 -10
  204. package/manifest/components/map-answer.json +17 -4
  205. package/manifest/components/markdown-text.json +36 -5
  206. package/manifest/components/math-block.json +12 -3
  207. package/manifest/components/mcp-server-icon.json +1 -7
  208. package/manifest/components/mcp-server-panel.json +18 -4
  209. package/manifest/components/memory-chips.json +9 -2
  210. package/manifest/components/menubar.json +33 -7
  211. package/manifest/components/message-actions.json +32 -7
  212. package/manifest/components/message-attachments.json +9 -2
  213. package/manifest/components/message-branches.json +14 -3
  214. package/manifest/components/message-list.json +27 -6
  215. package/manifest/components/message-pair.json +25 -5
  216. package/manifest/components/message-queue.json +13 -3
  217. package/manifest/components/message-timing.json +8 -2
  218. package/manifest/components/message.json +41 -9
  219. package/manifest/components/meter.json +43 -7
  220. package/manifest/components/mobile-composer.json +47 -10
  221. package/manifest/components/model-picker.json +13 -3
  222. package/manifest/components/multi-select.json +78 -17
  223. package/manifest/components/musical-time-input.json +34 -8
  224. package/manifest/components/navigation-menu.json +54 -12
  225. package/manifest/components/node-card.json +42 -7
  226. package/manifest/components/node-handle.json +17 -2
  227. package/manifest/components/number-field.json +28 -6
  228. package/manifest/components/number-ticker.json +8 -2
  229. package/manifest/components/onboarding.json +18 -4
  230. package/manifest/components/otp-field.json +43 -10
  231. package/manifest/components/page-shell.json +35 -6
  232. package/manifest/components/pagination.json +21 -5
  233. package/manifest/components/panel-header.json +15 -3
  234. package/manifest/components/password-input.json +38 -8
  235. package/manifest/components/permission-grant.json +28 -5
  236. package/manifest/components/permission-scopes.json +57 -12
  237. package/manifest/components/piano-keyboard.json +38 -9
  238. package/manifest/components/plussie.json +34 -5
  239. package/manifest/components/popover.json +45 -8
  240. package/manifest/components/preset-section.json +14 -8
  241. package/manifest/components/progress.json +25 -6
  242. package/manifest/components/prompt-library.json +27 -6
  243. package/manifest/components/queue-dock.json +36 -8
  244. package/manifest/components/quota-banner.json +25 -6
  245. package/manifest/components/quote-reply.json +29 -7
  246. package/manifest/components/radio-group.json +12 -2
  247. package/manifest/components/radio.json +17 -3
  248. package/manifest/components/rating.json +60 -13
  249. package/manifest/components/read-aloud.json +35 -8
  250. package/manifest/components/reasoning-effort.json +17 -4
  251. package/manifest/components/reasoning-panel.json +33 -8
  252. package/manifest/components/recommendation-card.json +35 -10
  253. package/manifest/components/regenerate-menu.json +22 -5
  254. package/manifest/components/replay-player.json +29 -7
  255. package/manifest/components/research-report.json +12 -3
  256. package/manifest/components/resizable.json +4 -1
  257. package/manifest/components/retrieval-chunks.json +16 -4
  258. package/manifest/components/revert-dock.json +61 -14
  259. package/manifest/components/reviewable-diff.json +23 -5
  260. package/manifest/components/risk-badge.json +18 -3
  261. package/manifest/components/row.json +17 -4
  262. package/manifest/components/schedule-card.json +25 -6
  263. package/manifest/components/score-breakdown.json +20 -5
  264. package/manifest/components/scroll-anchor.json +13 -3
  265. package/manifest/components/scroll-area.json +31 -8
  266. package/manifest/components/scrollable-tabs-list.json +12 -4
  267. package/manifest/components/scrub-input.json +84 -18
  268. package/manifest/components/seam-cell.json +19 -3
  269. package/manifest/components/seam-grid.json +12 -3
  270. package/manifest/components/search-input.json +49 -10
  271. package/manifest/components/section-header.json +36 -8
  272. package/manifest/components/segmented-control.json +50 -11
  273. package/manifest/components/select.json +74 -16
  274. package/manifest/components/separator.json +4 -1
  275. package/manifest/components/settings-panel.json +41 -9
  276. package/manifest/components/shape-section.json +10 -7
  277. package/manifest/components/shared-conversation.json +21 -5
  278. package/manifest/components/sheet.json +29 -6
  279. package/manifest/components/shimmer-label.json +4 -2
  280. package/manifest/components/sidebar.json +60 -13
  281. package/manifest/components/skill-icon.json +1 -7
  282. package/manifest/components/slider.json +22 -5
  283. package/manifest/components/sources.json +17 -4
  284. package/manifest/components/sparkline.json +17 -4
  285. package/manifest/components/speaker-identity.json +4 -1
  286. package/manifest/components/spec-sheet.json +16 -4
  287. package/manifest/components/spectrum-visualizer.json +41 -10
  288. package/manifest/components/spinner.json +12 -2
  289. package/manifest/components/stack-icon.json +1 -7
  290. package/manifest/components/stack.json +54 -7
  291. package/manifest/components/stacks-matrix-icon.json +1 -7
  292. package/manifest/components/stat.json +28 -4
  293. package/manifest/components/status-bar.json +50 -9
  294. package/manifest/components/status-dot.json +23 -4
  295. package/manifest/components/stepper.json +22 -5
  296. package/manifest/components/stopped-run.json +19 -4
  297. package/manifest/components/streaming-text.json +12 -3
  298. package/manifest/components/subagent-list.json +21 -5
  299. package/manifest/components/suggestions.json +27 -5
  300. package/manifest/components/swap-label.json +18 -4
  301. package/manifest/components/switch.json +18 -3
  302. package/manifest/components/table-scroll-region.json +4 -1
  303. package/manifest/components/table.json +54 -11
  304. package/manifest/components/tabs.json +16 -5
  305. package/manifest/components/tags-input.json +79 -17
  306. package/manifest/components/tail-status.json +37 -8
  307. package/manifest/components/terminal-block.json +25 -5
  308. package/manifest/components/terminal-frame.json +34 -8
  309. package/manifest/components/textarea.json +8 -3
  310. package/manifest/components/theme-configurator.json +47 -14
  311. package/manifest/components/theme-provider.json +97 -18
  312. package/manifest/components/thinking-indicator.json +8 -2
  313. package/manifest/components/thread-list.json +13 -3
  314. package/manifest/components/thread-search.json +22 -5
  315. package/manifest/components/threshold-rail.json +77 -14
  316. package/manifest/components/time-boundary.json +8 -2
  317. package/manifest/components/time-field.json +13 -2
  318. package/manifest/components/timeline-ruler.json +51 -12
  319. package/manifest/components/timeline.json +19 -3
  320. package/manifest/components/title-bar.json +19 -4
  321. package/manifest/components/toast.json +12 -3
  322. package/manifest/components/todo-list.json +8 -2
  323. package/manifest/components/toggle-group.json +33 -7
  324. package/manifest/components/toggle.json +23 -4
  325. package/manifest/components/token-editor.json +26 -10
  326. package/manifest/components/tool-call-card.json +83 -16
  327. package/manifest/components/tool-call.json +37 -9
  328. package/manifest/components/tool-error.json +34 -8
  329. package/manifest/components/tool-group.json +17 -4
  330. package/manifest/components/tool-timeline.json +37 -9
  331. package/manifest/components/toolbar.json +37 -8
  332. package/manifest/components/tooltip.json +41 -7
  333. package/manifest/components/trace-waterfall.json +12 -3
  334. package/manifest/components/tree-visibility-toggle.json +18 -3
  335. package/manifest/components/tree.json +52 -11
  336. package/manifest/components/turn-footer.json +36 -8
  337. package/manifest/components/two-step-button.json +46 -9
  338. package/manifest/components/typing-indicator.json +8 -1
  339. package/manifest/components/universe-grid.json +37 -10
  340. package/manifest/components/unread-divider.json +4 -1
  341. package/manifest/components/usage-chart.json +46 -9
  342. package/manifest/components/verdict-row.json +20 -5
  343. package/manifest/components/voice-conversation.json +37 -7
  344. package/manifest/components/web-preview.json +23 -5
  345. package/manifest/components/web-search.json +20 -5
  346. package/manifest/components/work-collapse.json +30 -7
  347. package/manifest/fixtures.json +106 -0
  348. package/manifest/manifest.json +449 -310
  349. package/manifest/tokens.json +122 -12
  350. package/package.json +16 -6
  351. package/dist/theming/_presets.d.ts +0 -11
  352. package/dist/theming/_presets.js +0 -678
@@ -1,12 +1,23 @@
1
1
  "use client";
2
2
  import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
3
- import { DensityContext } from "./density.js";
3
+ import { DEFAULT_DENSITY, DensityContext } from "./density.js";
4
+ import { warnOnce } from "./vocabulary.js";
5
+ import { ThemeRegistryContext } from "./theme-registry.js";
6
+ import { ThemeContext } from "./theme-provider.js";
4
7
  import * as React from "react";
5
8
  //#region src/system/use-density.ts
6
9
  /**
7
- * The control ladder in rem, mirroring `packages/tokens/src/density/*.tokens.json`.
8
- * Only a fallback: {@link useControlHeight} measures the live CSS first, so
9
- * per-app token overrides and the `@media (pointer: coarse)` re-raise win.
10
+ * The **base** control ladder in rem, mirroring
11
+ * `packages/tokens/src/density/*.tokens.json`.
12
+ *
13
+ * Keyed by `BaseDensity` — the closed five — and not by the open `Density`,
14
+ * because those five are the only rungs this package can know at build time. A
15
+ * rung a theme adds exists nowhere in `ui`'s source; it arrives at runtime on a
16
+ * manifest, and {@link ladderFor} is what goes and gets it.
17
+ *
18
+ * Only a fallback either way: {@link useControlHeight} measures the live CSS
19
+ * first, so per-app token overrides and the `@media (pointer: coarse)` re-raise
20
+ * win.
10
21
  */
11
22
  const CONTROL_REM = {
12
23
  "ultra-compact": {
@@ -37,16 +48,112 @@ const CONTROL_REM = {
37
48
  };
38
49
  /** Root font size assumed before the first measurement, so SSR and hydration agree. */
39
50
  const ASSUMED_ROOT_FONT_SIZE = 16;
51
+ /** The three tokens the ladder is made of, in the order a warning should name them. */
52
+ const CONTROL_TOKENS = [
53
+ "control-sm",
54
+ "control-md",
55
+ "control-lg"
56
+ ];
57
+ /**
58
+ * A `"1.25rem"` geometry value as a number of rem, or `null`.
59
+ *
60
+ * `null` rather than a throw, and rem rather than any length, because the three
61
+ * `control-*` tokens are rem *by convention*: `validateManifest` requires only a
62
+ * non-empty string, and the base geometry block itself ships `radius-control:
63
+ * "3px"`, so a manifest that passes validation can perfectly well carry a value
64
+ * this cannot use. See {@link ladderFor} for what happens then.
65
+ *
66
+ * Strictly positive, too. `"0rem"` and `"-1rem"` parse perfectly well and are
67
+ * not control heights: a virtualiser handed a row height of zero divides by it
68
+ * or renders every row in the list, and a negative one is not a height at all.
69
+ * Both are manifest defects, and a defect is better reported than propagated.
70
+ */
71
+ function rem(value) {
72
+ if (value === void 0) return null;
73
+ const match = /^(-?\d*\.?\d+)rem$/.exec(value.trim());
74
+ if (match === null) return null;
75
+ const parsed = Number.parseFloat(match[1]);
76
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
77
+ }
78
+ /**
79
+ * The rem ladder for a rung: the base table, else the registry, else a warning.
80
+ *
81
+ * The registry is the only place an added rung's geometry exists on the client.
82
+ * The stylesheet has it — `theme-tools` emitted a `[data-theme="foo"]
83
+ * [data-density="foo-dense"]` block for it — but CSS is not readable as data
84
+ * before layout, and this value is needed by a `useState` initializer, which is
85
+ * the one moment nothing has been laid out yet.
86
+ *
87
+ * Every failure degrades rather than throws, and the reason is that this runs
88
+ * during render from a `useState` initializer with no error boundary anywhere
89
+ * near it: a throw there unmounts the application, in production, over a
90
+ * pre-measurement seed. And the seed really is only a seed — the added rung's
91
+ * CSS exists whatever this returns, so the layout effect's probe measures the
92
+ * true height one tick later and the fallback is visible for one commit. A
93
+ * `control-*` rem rule in `validateManifest` is what would make this
94
+ * impossible, and that is a Plan 4 carry.
95
+ */
96
+ function ladderFor(registry, density) {
97
+ const base = CONTROL_REM[density];
98
+ if (base !== void 0) return base;
99
+ const added = registry?.densities.find((d) => d.name === density)?.geometry;
100
+ if (added !== void 0) {
101
+ const ladder = {
102
+ sm: rem(added["control-sm"]),
103
+ md: rem(added["control-md"]),
104
+ lg: rem(added["control-lg"])
105
+ };
106
+ if (ladder.sm !== null && ladder.md !== null && ladder.lg !== null) return ladder;
107
+ const offender = CONTROL_TOKENS.find((token) => rem(added[token]) === null);
108
+ warnOnce(`density "${density}" declares ${offender} as ${JSON.stringify(added[offender])}, which is not a positive rem length; the ${DEFAULT_DENSITY} ladder seeds the measurement instead`);
109
+ return CONTROL_REM[DEFAULT_DENSITY];
110
+ }
111
+ warnOnce(`density "${density}" is not a base rung and no registered theme adds it; the ${DEFAULT_DENSITY} ladder is used until the probe measures it`);
112
+ return CONTROL_REM[DEFAULT_DENSITY];
113
+ }
40
114
  function rootFontSize() {
41
115
  if (typeof document === "undefined") return ASSUMED_ROOT_FONT_SIZE;
42
116
  const size = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
43
117
  return Number.isFinite(size) && size > 0 ? size : ASSUMED_ROOT_FONT_SIZE;
44
118
  }
45
- function measureControlHeight(density, size) {
46
- const fallback = CONTROL_REM[density][size] * rootFontSize();
119
+ /**
120
+ * Measure `--cue-control-<size>` on a throwaway element stamped like the caller.
121
+ *
122
+ * `theme` as well as `density`, because a rung a theme adds is emitted under
123
+ * that theme's `[data-theme]`: a bare `data-density` div on `document.body`
124
+ * matches the **root** theme's rules, so under a nested provider of another
125
+ * theme it would measure a ladder the caller is not on. `null` stamps nothing,
126
+ * which is right for a caller with no provider above it at all.
127
+ *
128
+ * `data-cue-theme` rides beside `data-theme`, both from the same value: an
129
+ * inert attribute nothing here reads back, but the emitters' exact-match
130
+ * selector does — see `densityRules`/`themeSelector`'s comment on why a CSS
131
+ * ancestor guard alone cannot tell *nearest* theme from *any* theme in the
132
+ * chain, and how a runtime-published stamp closes that where the DOM shape
133
+ * lets it.
134
+ *
135
+ * `ladder` is passed in rather than looked up here so that both reads — this
136
+ * one and the state seed — go through {@link ladderFor} exactly once each, and
137
+ * so this stays a plain function that a test can reason about without a tree.
138
+ *
139
+ * What this element cannot see is the standing concession of the whole
140
+ * approach, and it is worth naming where it is caused rather than where it is
141
+ * felt: hanging off `document.body` puts the probe outside every provider
142
+ * root, so a rule scoped under `[data-cue-root]` does not reach it. That is the
143
+ * reason the provider's density override layer is emitted as one
144
+ * document-scoped `<style>` instead — a subtree-scoped rule would leave this
145
+ * measuring unoverridden geometry while the control beside it rendered the
146
+ * override, and the two would disagree by exactly the override.
147
+ */
148
+ function measureControlHeight(ladder, density, theme, size) {
149
+ const fallback = ladder[size] * rootFontSize();
47
150
  if (typeof document === "undefined" || document.body === null) return fallback;
48
151
  const probe = document.createElement("div");
49
152
  probe.setAttribute("data-density", density);
153
+ if (theme !== null) {
154
+ probe.setAttribute("data-theme", theme);
155
+ probe.setAttribute("data-cue-theme", theme);
156
+ }
50
157
  probe.setAttribute("aria-hidden", "true");
51
158
  probe.style.cssText = `position:absolute;top:-9999px;left:-9999px;width:0;visibility:hidden;pointer-events:none;height:var(--cue-control-${size})`;
52
159
  document.body.appendChild(probe);
@@ -81,10 +188,18 @@ function useDensity() {
81
188
  * canvas renderer needs when it cannot ask CSS.
82
189
  *
83
190
  * Measured from a throwaway `document.body` probe stamped with the ambient
84
- * density, so app-wide token overrides and the `@media (pointer: coarse)`
85
- * re-raise of `ultra-compact` are honoured (overrides scoped to a subtree the
86
- * probe is not in are not). Falls back to the authored ladder × 16px during SSR
87
- * and before the first layout effect, which keeps hydration stable.
191
+ * density *and the ambient theme*, so app-wide token overrides, the `@media
192
+ * (pointer: coarse)` re-raise of `ultra-compact`, and a rung a theme adds are
193
+ * all honoured. Overrides scoped to a subtree the probe is not in are still
194
+ * not: the probe hangs off `document.body`, outside every provider root.
195
+ *
196
+ * Falls back to the authored ladder × 16px during SSR and before the first
197
+ * layout effect, which keeps hydration stable. A rung the ambient theme does
198
+ * not declare falls back to the default ladder with a development warning
199
+ * rather than throwing — the probe corrects it on the next tick either way.
200
+ *
201
+ * Safe outside a `<ThemeProvider>`: it reads the theme and the registry from
202
+ * context directly instead of through `useTheme()`, which throws.
88
203
  *
89
204
  * @param size - Which rung to measure. Defaults to `"md"`.
90
205
  * @returns Height in CSS pixels.
@@ -93,10 +208,24 @@ function useDensity() {
93
208
  */
94
209
  function useControlHeight(size = "md") {
95
210
  const density = useDensity();
96
- const [height, setHeight] = React.useState(() => CONTROL_REM[density][size] * ASSUMED_ROOT_FONT_SIZE);
211
+ const theme = React.useContext(ThemeContext)?.theme ?? null;
212
+ const ladder = ladderFor(React.useContext(ThemeRegistryContext), density);
213
+ const { sm, md, lg } = ladder;
214
+ const [height, setHeight] = React.useState(() => ladder[size] * ASSUMED_ROOT_FONT_SIZE);
97
215
  useIsomorphicLayoutEffect(() => {
98
- setHeight(measureControlHeight(density, size));
99
- }, [density, size]);
216
+ setHeight(measureControlHeight({
217
+ sm,
218
+ md,
219
+ lg
220
+ }, density, theme, size));
221
+ }, [
222
+ sm,
223
+ md,
224
+ lg,
225
+ density,
226
+ theme,
227
+ size
228
+ ]);
100
229
  return height;
101
230
  }
102
231
  //#endregion
@@ -3,12 +3,14 @@ import { ThemeContextValue } from "./theme-provider.js";
3
3
  /**
4
4
  * Read and drive the ambient theme state.
5
5
  *
6
- * Returns `{ theme, density, mode, font, resolvedMode, setTheme, setDensity,
7
- * setMode, setFont }`.
6
+ * Returns `{ theme, manifest, density, mode, font, resolvedMode, setTheme,
7
+ * setDensity, setMode, setFont }`.
8
8
  * `density` here is the *provider's* level — a nested `<Density>` island does not
9
9
  * change it; use `useDensity()` when you want the level that actually applies
10
10
  * where you are. `font` is the pairing the provider holds; it has no island, so
11
- * it is always the one in force.
11
+ * it is always the one in force. `manifest` is the active theme's, or `null`
12
+ * when that name is not in the provider's `themes` — which is every name in an
13
+ * app that has registered none; `useThemes()` is what was registered.
12
14
  *
13
15
  * @throws If called outside a `<ThemeProvider>`. Theme state is never guessed:
14
16
  * a missing provider is a wiring bug, not a fallback situation.
@@ -5,12 +5,14 @@ import * as React from "react";
5
5
  /**
6
6
  * Read and drive the ambient theme state.
7
7
  *
8
- * Returns `{ theme, density, mode, font, resolvedMode, setTheme, setDensity,
9
- * setMode, setFont }`.
8
+ * Returns `{ theme, manifest, density, mode, font, resolvedMode, setTheme,
9
+ * setDensity, setMode, setFont }`.
10
10
  * `density` here is the *provider's* level — a nested `<Density>` island does not
11
11
  * change it; use `useDensity()` when you want the level that actually applies
12
12
  * where you are. `font` is the pairing the provider holds; it has no island, so
13
- * it is always the one in force.
13
+ * it is always the one in force. `manifest` is the active theme's, or `null`
14
+ * when that name is not in the provider's `themes` — which is every name in an
15
+ * app that has registered none; `useThemes()` is what was registered.
14
16
  *
15
17
  * @throws If called outside a `<ThemeProvider>`. Theme state is never guessed:
16
18
  * a missing provider is a wiring bug, not a fallback situation.
@@ -0,0 +1,15 @@
1
+ import { Density, DensityEntry, FontEntry, FontName, ThemeManifest } from "@cueplusplus/theme-base";
2
+ //#region src/system/vocabulary.d.ts
3
+ /** What the three registry hooks read: the manifests, and the axes the ACTIVE theme offers. */
4
+ interface ThemeRegistry {
5
+ /** Every registered manifest, in registration order. */
6
+ readonly themes: readonly ThemeManifest[];
7
+ /** First registered manifest's name, or `null` with nothing registered. */
8
+ readonly defaultTheme: string | null;
9
+ /** The rungs the ACTIVE theme offers: base five, then its adds. */
10
+ readonly densities: readonly DensityEntry[];
11
+ /** The pairings the ACTIVE theme offers, by the same rule. */
12
+ readonly fonts: readonly FontEntry[];
13
+ }
14
+ //#endregion
15
+ export { ThemeRegistry };
@@ -0,0 +1,111 @@
1
+ import { DEFAULT_DENSITY, DEFAULT_FONT } from "@cueplusplus/tokens";
2
+ import { DENSITIES, FONTS, THEME_NAME_PATTERN, resolveDensities, resolveFonts } from "@cueplusplus/theme-base";
3
+ //#region src/system/vocabulary.ts
4
+ /**
5
+ * The registry's pure half: what a set of manifests accepts, and what an app
6
+ * may offer under one of them.
7
+ *
8
+ * No `"use client"` and no React, on purpose. `prepaint.ts` is server-safe by
9
+ * contract — the docs app's root layout is a Server Component and calls
10
+ * `prepaintScript()` during render — and it reaches this module to inline the
11
+ * vocabulary into the blocking script. A module reached through a client
12
+ * boundary arrives in a server graph as a client *reference* rather than a
13
+ * function, and calling one throws at prerender. So the pure half lives here,
14
+ * directive-free, and `theme-registry.tsx` holds the context and the hooks and
15
+ * re-exports this file so the barrel still has one source.
16
+ * `test/system/server-safe.test.ts` holds this file, and every file
17
+ * `prepaint.ts` reaches, to that rule.
18
+ */
19
+ /**
20
+ * What each axis accepts, and the one place that is decided.
21
+ *
22
+ * Six code paths validated a name before this existed — three against a frozen
23
+ * list from `@cueplusplus/tokens`, two against local copies, one against
24
+ * nothing at all — and they disagreed with each other at the edges. They all
25
+ * ask this now, so a stored preference means the same thing to the pre-paint
26
+ * script, the restore effect, and a portal.
27
+ *
28
+ * An empty registry is the deliberate case, not the degenerate one: an app that
29
+ * registers nothing has the base axes and no registry, and for one release that
30
+ * is every app that has not migrated — this repository's own docs site
31
+ * included. (It does not follow that such an app is unpainted: a stylesheet it
32
+ * imported may well declare the block the provider stamps. Colour is a question
33
+ * for CSS; this file only decides what a name is allowed to be.) Refusing every theme name there would reject a visitor's stored
34
+ * preference on first paint. So with nothing registered, theme validation is
35
+ * *off* (any name the selector grammar allows), and density and font validate
36
+ * against the base ladder and pairings alone. A registered set closes all
37
+ * three: only registered themes, and a theme's added rung or pairing only under
38
+ * that theme, which is what the cascade actually scopes it to.
39
+ *
40
+ * The order matters wherever a whole stored quadruple is read: resolve the
41
+ * theme first, then each axis under the theme resolution chose. See
42
+ * `acceptedPreferences()` in `theme-provider.tsx`, which is the one place that
43
+ * rule is applied, and the pre-paint script, which applies it the same way.
44
+ */
45
+ function vocabulary(themes) {
46
+ const byName = /* @__PURE__ */ new Map();
47
+ for (const manifest of themes) if (!byName.has(manifest.name)) byName.set(manifest.name, manifest);
48
+ const names = themes.length === 0 ? null : [...byName.keys()];
49
+ const densitiesFor = (theme) => [...DENSITIES, ...Object.keys((theme === null ? void 0 : byName.get(theme))?.densities.adds ?? {})];
50
+ const fontsFor = (theme) => [...FONTS, ...Object.keys((theme === null ? void 0 : byName.get(theme))?.fontPairings.adds ?? {})];
51
+ const preferredDensity = (theme) => {
52
+ const wanted = (theme === null ? void 0 : byName.get(theme))?.densities.default;
53
+ return wanted !== void 0 && densitiesFor(theme).includes(wanted) ? wanted : DEFAULT_DENSITY;
54
+ };
55
+ const preferredFont = (theme) => {
56
+ const wanted = (theme === null ? void 0 : byName.get(theme))?.fontPairings.default;
57
+ return wanted !== void 0 && fontsFor(theme).includes(wanted) ? wanted : DEFAULT_FONT;
58
+ };
59
+ return {
60
+ themes: names,
61
+ densitiesFor,
62
+ fontsFor,
63
+ preferredDensity,
64
+ preferredFont,
65
+ acceptsTheme: (name) => typeof name === "string" && (names === null ? THEME_NAME_PATTERN.test(name) : names.includes(name)),
66
+ acceptsDensity: (theme, name) => typeof name === "string" && densitiesFor(theme).includes(name),
67
+ acceptsFont: (theme, name) => typeof name === "string" && fontsFor(theme).includes(name),
68
+ supportsLight: (theme) => theme === null ? true : byName.get(theme)?.supportsLight ?? true
69
+ };
70
+ }
71
+ /** Derive the context value. Pure; the provider memoises it on `themes` and the active name. */
72
+ function buildRegistry(themes, active) {
73
+ return {
74
+ themes,
75
+ defaultTheme: themes[0]?.name ?? null,
76
+ densities: resolveDensities(themes, active),
77
+ fonts: resolveFonts(themes, active)
78
+ };
79
+ }
80
+ const seen = /* @__PURE__ */ new Set();
81
+ /**
82
+ * A development warning, once per distinct message per process.
83
+ *
84
+ * Once, because the things this warns about — a `themes` prop nobody passed, a
85
+ * rung a theme does not offer — are configuration, and configuration is wrong
86
+ * the same way on every render. A warning per render is a warning nobody reads,
87
+ * and fourteen provider tests would otherwise be fourteen copies of one line.
88
+ */
89
+ function warnOnce(message) {
90
+ if (process.env.NODE_ENV === "production" || seen.has(message)) return;
91
+ seen.add(message);
92
+ console.warn(`[cue] ${message}`);
93
+ }
94
+ /**
95
+ * {@link warnOnce}, one tier quieter.
96
+ *
97
+ * `contrastReport()` keeps two lists on purpose — a *failure* is what makes a
98
+ * screen unreadable, an *advisory* is a focus ring nobody has been failed on
99
+ * yet — and folding the second into `console.warn` would either train a reader
100
+ * to ignore the first or push a repository to turn both off. They share the
101
+ * `seen` set so "once per distinct message" is one budget and `resetWarnings()`
102
+ * clears both; the two message texts cannot collide, since a failure and an
103
+ * advisory for one pair say different things about it.
104
+ */
105
+ function noteOnce(message) {
106
+ if (process.env.NODE_ENV === "production" || seen.has(message)) return;
107
+ seen.add(message);
108
+ console.info(`[cue] ${message}`);
109
+ }
110
+ //#endregion
111
+ export { buildRegistry, noteOnce, vocabulary, warnOnce };
@@ -1,122 +1,2 @@
1
- //#region src/theming/contrast.d.ts
2
- /** Non-text and large-text AA: UI components, graphics, status dots. */
3
- declare const WCAG_AA_NON_TEXT = 3;
4
- /** Body-text AA. The floor for anything a reader is expected to read. */
5
- declare const WCAG_AA_TEXT = 4.5;
6
- /** Body-text AAA. What this portfolio holds its primary ink to. */
7
- declare const WCAG_AAA_TEXT = 7;
8
- /**
9
- * WCAG relative luminance: 0 for black, 1 for white.
10
- *
11
- * Alpha is ignored — a translucent colour has no luminance of its own, only one
12
- * in front of something. {@link contrastRatio} is where that composite happens.
13
- *
14
- * @param color - Any CSS colour string.
15
- * @returns Relative luminance in `[0, 1]`.
16
- * @throws TypeError if the value is not a colour.
17
- * @example
18
- * relativeLuminance("#767676"); // → 0.1845…
19
- */
20
- declare function relativeLuminance(color: string): number;
21
- /**
22
- * WCAG contrast ratio between two colours, from 1:1 to 21:1.
23
- *
24
- * Symmetric, because a ratio has no direction — the arguments are named for
25
- * readability, not for order. A **translucent foreground is composited over the
26
- * background first**, which is not decoration: half this system's `fg-muted`
27
- * tokens are white-alpha ink, and measuring them raw would report the ratio of
28
- * pure white and pass everything.
29
- *
30
- * @param foreground - The ink. Composited over `background` if it has alpha.
31
- * @param background - The ground. Its own alpha is ignored — nothing is behind it.
32
- * @returns The ratio, `>= 1`.
33
- * @throws TypeError naming whichever value could not be read.
34
- * @example
35
- * contrastRatio("#767676", "#ffffff"); // → 4.5422…
36
- */
37
- declare function contrastRatio(foreground: string, background: string): number;
38
- /** One pair a theme is checked on. */
39
- interface ContrastRequirement {
40
- /** `"<foreground>/<background>"` — stable, and readable in a failure message. */
41
- id: string;
42
- /** Bare token name of the ink. */
43
- foreground: string;
44
- /** Bare token name of the ground. */
45
- background: string;
46
- /** The ratio the pair has to reach. */
47
- minimum: number;
48
- /** Why this minimum, in one line — the configurator surfaces it. */
49
- reason: string;
50
- }
51
- /**
52
- * The eleven pairs that decide whether a theme is usable.
53
- *
54
- * Not every pair in the palette: the ones a reader's comprehension actually
55
- * depends on. Primary ink is held to AAA because it carries the prose; secondary
56
- * ink and accent ink to AA because they carry labels; the five status colours
57
- * and the live hue to the non-text AA floor because they are read as *signals*
58
- * — a dot, a rim, a bar — never as body copy.
59
- *
60
- * `danger` and `warn` each appear twice, and they are the only tones that do.
61
- * `ok`, `busy`, `info` and `stream` are *only* ever signals, so the non-text
62
- * floor is the whole truth about them. The red and the amber are also grounds:
63
- * they are the two tones this system fills a control with, and what they fill is
64
- * the confirm on something irreversible and the confirm on something merely
65
- * consequential. Text on either fill is text, so its ink is held to the text
66
- * floor against the tone it sits on rather than against the page.
67
- *
68
- * `stream` is measured for the same reason `ok` is: `createTheme()` darkens it
69
- * to this floor when it has to invent a light block, and a report that did not
70
- * check what the derivation targets would be checking the wrong thing.
71
- */
72
- declare const CONTRAST_REQUIREMENTS: readonly ContrastRequirement[];
73
- /** One measured pair. */
74
- interface ContrastCheck extends ContrastRequirement {
75
- /** Which block the pair was measured in. */
76
- mode: "dark" | "light";
77
- /** The colour the foreground token resolved to. */
78
- foregroundValue: string;
79
- /** The colour the background token resolved to. */
80
- backgroundValue: string;
81
- /** The measured ratio. */
82
- ratio: number;
83
- /** Whether {@link ContrastRequirement.minimum} was reached. */
84
- passes: boolean;
85
- /** Whether the values were derived rather than authored — see the light variant. */
86
- provisional: boolean;
87
- }
88
- /** One block's worth of resolved colours. */
89
- interface ContrastInput {
90
- /** Which block these are. */
91
- mode: "dark" | "light";
92
- /** Bare token name → resolved CSS colour. Pairs with a missing token are skipped. */
93
- tokens: Readonly<Record<string, string>>;
94
- /** Mark the resulting checks provisional. Defaults to `false`. */
95
- provisional?: boolean;
96
- }
97
- /** Every pair, measured, plus the verdict. */
98
- interface ContrastReport {
99
- /** True when nothing failed. */
100
- passes: boolean;
101
- /** True when any measured block was derived rather than authored. */
102
- provisional: boolean;
103
- /** Every pair that could be measured, in requirement order, dark block first. */
104
- checks: readonly ContrastCheck[];
105
- /** The subset that failed. Empty when {@link ContrastReport.passes}. */
106
- failures: readonly ContrastCheck[];
107
- }
108
- /**
109
- * Measure every requirement against every block handed in.
110
- *
111
- * A pair whose tokens are missing is **skipped**, not failed: the report says
112
- * what it measured, and a caller checking a partial palette should not be told
113
- * that a colour it never supplied is unreadable.
114
- *
115
- * @param inputs - One entry per block, dark first by convention.
116
- * @returns The report.
117
- * @example
118
- * contrastReport([{ mode: "dark", tokens: { fg: "#fff", bg: "#000" } }]).passes; // → true
119
- */
120
- declare function contrastReport(inputs: readonly ContrastInput[]): ContrastReport;
121
- //#endregion
122
- export { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport, relativeLuminance };
1
+ import { CONTRAST_REQUIREMENTS, ContrastCheck, ContrastInput, ContrastReport, ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport as contrastReport$1, relativeLuminance } from "@cueplusplus/theme-base/contrast";
2
+ export { CONTRAST_REQUIREMENTS, type ContrastCheck, type ContrastInput, type ContrastReport, type ContrastRequirement, WCAG_AAA_TEXT, WCAG_AA_NON_TEXT, WCAG_AA_TEXT, contrastRatio, contrastReport$1 as contrastReport, relativeLuminance };