@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,19 +1,42 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { useIsomorphicInsertionEffect, useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
4
- import { DEFAULT_DENSITY as DEFAULT_DENSITY$1, DENSITY_LEVELS, DensityContext } from "./density.js";
4
+ import { AmbientThemeContext, DensityContext } from "./density.js";
5
+ import { buildRegistry, vocabulary, warnOnce } from "./vocabulary.js";
6
+ import { OverridesContext, inlineOverrides, overridesCss, reportOverrides } from "./overrides.js";
5
7
  import { DEFAULT_STORAGE_KEY } from "./prepaint.js";
8
+ import { ThemeRegistryContext } from "./theme-registry.js";
6
9
  import * as React from "react";
7
- import { jsx } from "react/jsx-runtime";
8
- import { DEFAULT_FONT, FONTS, THEMES } from "@cueplusplus/tokens";
10
+ import { jsx, jsxs } from "react/jsx-runtime";
11
+ import { MODES, THEME_NAME_PATTERN, validateManifest } from "@cueplusplus/theme-base";
9
12
  //#region src/system/theme-provider.tsx
10
- const MODES$1 = [
11
- "dark",
12
- "light",
13
- "system"
14
- ];
15
- const FONT_NAMES = FONTS;
16
- /** Dark-first portfolio defaults; the same triple bare `:root` in `theme.css` paints. */
13
+ /**
14
+ * The empty registry, at module scope so the default is referentially stable.
15
+ *
16
+ * Every memo below is keyed on `themes`, and a fresh `[]` per render would
17
+ * rebuild the registry, the vocabulary and the context value on every commit of
18
+ * every app that has not registered anything yet — which, for one release, is
19
+ * all of them.
20
+ */
21
+ const EMPTY = [];
22
+ /**
23
+ * Dark-first defaults, and the one thing to know about the first of them: it is
24
+ * a *name*, and since this release nothing in the library paints it.
25
+ *
26
+ * A bare `:root` used to carry `cue`'s palette, because `@cueplusplus/tokens`
27
+ * shipped ten presets and put the default one there. `styles.css` now imports
28
+ * `@cueplusplus/theme-base/base.css` instead, whose `:root` is the blank base —
29
+ * greys and one desaturated accent. So an app that registers nothing and stamps
30
+ * nothing gets `data-theme="cue"` on its provider element and paints the blank
31
+ * base, because no stylesheet on the page declares a `[data-theme="cue"]` block.
32
+ * Colour is decided by CSS, not by this constant.
33
+ *
34
+ * That is deliberate and it is why the name stays `cue`: the alternative — a
35
+ * default of `"base"` or `""` — would change what every app that *does* import
36
+ * `@cueplusplus/theme-cue/theme.css` renders when it passes no `theme` prop, to
37
+ * buy nothing, since a name that matches no imported block already paints the
38
+ * base.
39
+ */
17
40
  const DEFAULT_THEME = "cue";
18
41
  const DEFAULT_MODE = "dark";
19
42
  const DEFAULT_RESOLVED_MODE = "dark";
@@ -51,28 +74,48 @@ function readPreferences(storageKey) {
51
74
  return {};
52
75
  }
53
76
  }
54
- /** Resolve `"system"` the way {@link prepaintScript} resolves it: against the media query. */
55
- function resolveMode(mode) {
56
- if (mode !== "system") return mode;
57
- if (typeof matchMedia !== "function") return DEFAULT_RESOLVED_MODE;
58
- return matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
59
- }
60
77
  /**
61
- * The stamp {@link prepaintScript} put on `<html>`, recomputed from the storage
62
- * it read.
78
+ * The stored quadruple, accepted the way the pre-paint script accepts it:
79
+ * **theme first**, then each axis under the theme resolution actually chose.
80
+ *
81
+ * A rung a theme adds is scoped by the cascade to that theme's `data-theme`, so
82
+ * checking it against the theme that is about to be replaced rejects exactly
83
+ * the pairing the registry exists to allow — and on every commit that reads
84
+ * storage the component's `theme` is still the *prop*, never the theme the
85
+ * visitor stored. The script does `var t=…; var a=A[t]`; so does this, and it
86
+ * is the only place either rule is applied.
87
+ *
88
+ * `mode` is returned as stored, including the literal `"system"`. Resolving it
89
+ * here is what used to put `data-mode="dark"` on `<html>` for one commit under
90
+ * a stored `system`, which the stamp's own comment says must not happen.
63
91
  *
64
- * Not read back off the element, because by the time anything here runs it is
65
- * no longer there — see the `<html>` stamp below. Same key, same validation,
66
- * same fallbacks, so this returns what the script decided rather than a second
67
- * opinion about it.
92
+ * **The fallback is judged too.** It is the app's own default quadruple, which
93
+ * was chosen under the app's own default *theme* — and the theme resolution
94
+ * just chose need not be that one. Handing back `fallback.density` unchecked
95
+ * puts a rung the resolved theme does not declare onto `<html>`: an attribute
96
+ * no block matches, which is the precise failure the registry exists to
97
+ * prevent, arrived at by the code that was meant to prevent it. So each axis
98
+ * falls through the stored value, then the app default, then the base default,
99
+ * which every theme offers by construction.
100
+ *
101
+ * A *refused theme* is the one case here worth a word to the developer. The
102
+ * other three axes have a visible fallback the reader can see and change back;
103
+ * a theme that was registered when the visitor chose it and is not registered
104
+ * now is almost always a registration that was dropped by accident, and it
105
+ * shows up as "my users keep losing their theme" long after the commit that
106
+ * caused it. With an empty registry nothing is refused for being unregistered
107
+ * — theme validation is off — so the only refusal left there is a name that
108
+ * could not be a `data-theme` value at all, and it gets the other sentence:
109
+ * "register it" is not the fix for `"9bad"`.
68
110
  */
69
- function prepaintStamp(storageKey, fallback) {
70
- const stored = readPreferences(storageKey);
111
+ function acceptedPreferences(vocab, stored, fallback) {
112
+ const theme = vocab.acceptsTheme(stored.theme) ? stored.theme : fallback.theme;
113
+ if (stored.theme !== void 0 && stored.theme !== theme) warnOnce(vocab.themes === null ? `stored theme "${stored.theme}" is not a valid theme name; falling back to "${theme}". A theme name must match ${String(THEME_NAME_PATTERN)}.` : `stored theme "${stored.theme}" is not registered; falling back to "${theme}". A visitor who chose it keeps losing it until its manifest is back in ThemeProvider's themes prop.`);
71
114
  return {
72
- theme: typeof stored.theme === "string" && stored.theme.length > 0 ? stored.theme : fallback.theme,
73
- density: isOneOf(DENSITY_LEVELS, stored.density) ? stored.density : fallback.density,
74
- font: isOneOf(FONT_NAMES, stored.font) ? stored.font : fallback.font,
75
- mode: resolveMode(isOneOf(MODES$1, stored.mode) ? stored.mode : fallback.mode)
115
+ theme,
116
+ density: vocab.acceptsDensity(theme, stored.density) ? stored.density : vocab.acceptsDensity(theme, fallback.density) ? fallback.density : vocab.preferredDensity(theme),
117
+ font: vocab.acceptsFont(theme, stored.font) ? stored.font : vocab.acceptsFont(theme, fallback.font) ? fallback.font : vocab.preferredFont(theme),
118
+ mode: isOneOf(MODES, stored.mode) ? stored.mode : fallback.mode
76
119
  };
77
120
  }
78
121
  function writePreferences(storageKey, preferences) {
@@ -84,7 +127,8 @@ function writePreferences(storageKey, preferences) {
84
127
  /**
85
128
  * Root of the theme system: stamps `data-theme`, `data-density` and `data-mode`
86
129
  * so the token layer resolves, puts `data-font` on `<html>`, publishes
87
- * `--cue-font-scale`, and owns the persisted user preference.
130
+ * `--cue-font-scale`, owns the persisted user preference, and publishes the
131
+ * registry the three axis hooks read.
88
132
  *
89
133
  * Rendering: `<div data-cue-root data-theme data-density data-mode style={{ colorScheme, --cue-font-scale }}>`
90
134
  * (or the single child when `asChild`). The `theme`/`density`/`font`/`mode` props are
@@ -106,26 +150,72 @@ function writePreferences(storageKey, preferences) {
106
150
  * font pairing — `useTheme().font` and `setFont` inside one are the root's.
107
151
  *
108
152
  * @example
109
- * <ThemeProvider theme="terminal" density="ultra-compact" font="plex" mode="system">
153
+ * // `cue` and `terminal` are the `manifest.json` each theme package ships, read
154
+ * // from the `…/manifest.json` subpath of `@cueplusplus/theme-cue` and
155
+ * // `@cueplusplus/theme-terminal`. Written that way round on purpose: a literal
156
+ * // import statement in this comment reads, to every import-graph gate in this
157
+ * // repository, as `ui` depending on a theme package — which is the one thing
158
+ * // it may not do.
159
+ * <ThemeProvider themes={[cue, terminal]} theme="terminal" density="ultra-compact" font="plex" mode="system">
110
160
  * <App />
111
161
  * </ThemeProvider>
112
162
  */
113
- function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp = DEFAULT_DENSITY$1, mode: modeProp = DEFAULT_MODE, font: fontProp = DEFAULT_FONT, fontScale = 1, fontFamilies, storageKey = DEFAULT_STORAGE_KEY, persistPreferences = true, asChild = false, className, style: styleProp, children }) {
163
+ function ThemeProvider({ themes: themesProp, theme: themeProp, density: densityProp, mode: modeProp = DEFAULT_MODE, font: fontProp, fontScale = 1, fontFamilies, overrides, storageKey = DEFAULT_STORAGE_KEY, persistPreferences = true, asChild = false, className, style: styleProp, children }) {
114
164
  const ambient = React.useContext(ThemeContext);
115
165
  const isRoot = ambient === null;
116
- const [theme, setThemeState] = React.useState(themeProp);
117
- const [density, setDensityState] = React.useState(densityProp);
166
+ const ambientRegistry = React.useContext(ThemeRegistryContext);
167
+ const themes = themesProp ?? ambientRegistry?.themes ?? EMPTY;
168
+ const vocab = React.useMemo(() => vocabulary(themes), [themes]);
169
+ React.useMemo(() => {
170
+ if (process.env.NODE_ENV === "production") return;
171
+ for (const manifest of themes) {
172
+ const problems = validateManifest(manifest);
173
+ if (problems.length > 0) throw new TypeError(`ThemeProvider: manifest ${JSON.stringify(manifest.name)} is invalid\n${problems.map((p) => ` ${p.path}: ${p.message}`).join("\n")}`);
174
+ }
175
+ for (const manifest of themes) {
176
+ const rung = manifest.densities.default;
177
+ if (rung !== void 0 && !vocab.acceptsDensity(manifest.name, rung)) warnOnce(`manifest "${manifest.name}": densities.default "${rung}" is neither a base rung nor one this theme adds; the base default is used`);
178
+ const pairing = manifest.fontPairings.default;
179
+ if (pairing !== void 0 && !vocab.acceptsFont(manifest.name, pairing)) warnOnce(`manifest "${manifest.name}": fontPairings.default "${pairing}" is neither a base pairing nor one this theme adds; the base default is used`);
180
+ }
181
+ if (isRoot && themes.length === 0) warnOnce("ThemeProvider: no `themes` were registered, so there is no registry: useThemes() is empty, useTheme().manifest is null, the pre-paint script accepts any well-formed theme name out of storage, and no theme's added density rungs or font pairings are offered. Whatever [data-theme] blocks the page imported still paint. Pass themes={[…]} — see @cueplusplus/theme-base.");
182
+ }, [
183
+ themes,
184
+ vocab,
185
+ isRoot
186
+ ]);
187
+ const initialTheme = themeProp ?? (isRoot ? themes[0]?.name : ambient?.theme) ?? DEFAULT_THEME;
188
+ const initialDensity = densityProp ?? vocab.preferredDensity(initialTheme);
189
+ const initialFont = fontProp ?? vocab.preferredFont(initialTheme);
190
+ const [theme, setThemeState] = React.useState(initialTheme);
191
+ const [densityState, setDensityState] = React.useState(initialDensity);
118
192
  const [mode, setModeState] = React.useState(modeProp);
119
- const [rootFont, setRootFontState] = React.useState(() => {
120
- if (!isRoot || !persistPreferences) return fontProp;
121
- const stored = readPreferences(storageKey).font;
122
- return isOneOf(FONT_NAMES, stored) ? stored : fontProp;
193
+ const [storedSeed] = React.useState(() => {
194
+ if (!isRoot || !persistPreferences) return {
195
+ theme: initialTheme,
196
+ font: initialFont
197
+ };
198
+ const stored = readPreferences(storageKey);
199
+ const theme = vocab.acceptsTheme(stored.theme) ? stored.theme : initialTheme;
200
+ const fallback = fontProp ?? (theme === initialTheme ? initialFont : vocab.preferredFont(theme));
201
+ return {
202
+ theme,
203
+ font: vocab.acceptsFont(theme, stored.font) ? stored.font : fallback
204
+ };
123
205
  });
124
- const font = isRoot ? rootFont : ambient.font;
206
+ const [rootFont, setRootFontState] = React.useState(storedSeed.font);
207
+ const rawFont = isRoot ? rootFont : ambient.font;
125
208
  const [systemMode, setSystemMode] = React.useState(DEFAULT_RESOLVED_MODE);
126
- const resolvedMode = mode === "system" ? systemMode : mode;
127
209
  const restoredKey = React.useRef(null);
128
210
  const [restored, setRestored] = React.useState(false);
211
+ const manifest = React.useMemo(() => themes.find((m) => m.name === theme) ?? null, [themes, theme]);
212
+ if (vocab.themes !== null && manifest === null) warnOnce(`theme "${theme}" is not registered; useTheme().manifest is null and only the base axes are offered. Add its manifest to ThemeProvider's themes prop.`);
213
+ const density = vocab.acceptsDensity(theme, densityState) ? densityState : vocab.preferredDensity(theme);
214
+ if (density !== densityState) warnOnce(`density "${densityState}" is not offered by theme "${theme}"; using "${density}"`);
215
+ const fontTheme = isRoot && persistPreferences && !restored ? storedSeed.theme : theme;
216
+ const font = vocab.acceptsFont(fontTheme, rawFont) ? rawFont : vocab.preferredFont(fontTheme);
217
+ if (font !== rawFont) warnOnce(`font "${rawFont}" is not offered by theme "${fontTheme}"; using "${font}"`);
218
+ const resolvedMode = manifest?.supportsLight === false ? "dark" : mode === "system" ? systemMode : mode;
129
219
  useIsomorphicLayoutEffect(() => {
130
220
  if (!isRoot) return;
131
221
  if (!persistPreferences) {
@@ -136,10 +226,17 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
136
226
  restoredKey.current = storageKey;
137
227
  setRestored(true);
138
228
  const stored = readPreferences(storageKey);
139
- if (isOneOf(THEMES, stored.theme)) setThemeState(stored.theme);
140
- if (isOneOf(DENSITY_LEVELS, stored.density)) setDensityState(stored.density);
141
- if (isOneOf(FONT_NAMES, stored.font)) setRootFontState(stored.font);
142
- if (isOneOf(MODES$1, stored.mode)) setModeState(stored.mode);
229
+ const next = acceptedPreferences(vocab, stored, {
230
+ theme,
231
+ density,
232
+ font,
233
+ mode
234
+ });
235
+ setThemeState(next.theme);
236
+ setDensityState(next.density);
237
+ setRootFontState(next.font);
238
+ setModeState(next.mode);
239
+ if (stored.theme !== void 0 && stored.theme !== next.theme || stored.density !== void 0 && stored.density !== next.density || stored.font !== void 0 && stored.font !== next.font || stored.mode !== void 0 && stored.mode !== next.mode) writePreferences(storageKey, next);
143
240
  }, [
144
241
  isRoot,
145
242
  persistPreferences,
@@ -159,10 +256,10 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
159
256
  mode: modeProp,
160
257
  font: fontProp
161
258
  };
162
- if (previous.theme !== themeProp) setThemeState(themeProp);
163
- if (previous.density !== densityProp) setDensityState(densityProp);
259
+ if (previous.theme !== themeProp && themeProp !== void 0) setThemeState(themeProp);
260
+ if (previous.density !== densityProp && densityProp !== void 0) setDensityState(densityProp);
164
261
  if (previous.mode !== modeProp) setModeState(modeProp);
165
- if (previous.font !== fontProp) setRootFontState(fontProp);
262
+ if (previous.font !== fontProp && fontProp !== void 0) setRootFontState(fontProp);
166
263
  }, [
167
264
  themeProp,
168
265
  densityProp,
@@ -179,16 +276,16 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
179
276
  }, []);
180
277
  const stateRef = React.useRef({
181
278
  theme,
182
- density,
279
+ density: densityState,
183
280
  mode,
184
- font
281
+ font: rawFont
185
282
  });
186
283
  useIsomorphicLayoutEffect(() => {
187
284
  stateRef.current = {
188
285
  theme,
189
- density,
286
+ density: densityState,
190
287
  mode,
191
- font
288
+ font: rawFont
192
289
  };
193
290
  });
194
291
  const persist = React.useCallback((patch) => {
@@ -226,17 +323,13 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
226
323
  useIsomorphicInsertionEffect(() => {
227
324
  if (!isRoot || typeof document === "undefined") return;
228
325
  const root = document.documentElement;
229
- const next = restored || !persistPreferences ? {
326
+ const current = {
230
327
  theme,
231
328
  density,
232
329
  font,
233
330
  mode
234
- } : prepaintStamp(storageKey, {
235
- theme: themeProp,
236
- density: densityProp,
237
- font: fontProp,
238
- mode: modeProp
239
- });
331
+ };
332
+ const next = restored || !persistPreferences ? current : acceptedPreferences(vocab, readPreferences(storageKey), current);
240
333
  originalStamp.current ??= {
241
334
  theme: root.getAttribute("data-theme"),
242
335
  density: root.getAttribute("data-density"),
@@ -251,7 +344,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
251
344
  write("data-density", next.density);
252
345
  write("data-font", next.font);
253
346
  write("data-mode", next.mode);
254
- const scheme = next.mode === "system" ? "light dark" : next.mode;
347
+ const scheme = (themes.find((m) => m.name === next.theme) ?? null)?.supportsLight === false ? "dark" : next.mode === "system" ? "light dark" : next.mode;
255
348
  if (root.style.colorScheme !== scheme) root.style.colorScheme = scheme;
256
349
  }, [
257
350
  isRoot,
@@ -262,10 +355,8 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
262
355
  density,
263
356
  font,
264
357
  mode,
265
- themeProp,
266
- densityProp,
267
- fontProp,
268
- modeProp
358
+ themes,
359
+ vocab
269
360
  ]);
270
361
  React.useEffect(() => () => {
271
362
  const original = originalStamp.current;
@@ -284,6 +375,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
284
375
  }, []);
285
376
  const contextValue = React.useMemo(() => ({
286
377
  theme,
378
+ manifest,
287
379
  density,
288
380
  mode,
289
381
  font,
@@ -294,6 +386,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
294
386
  setFont
295
387
  }), [
296
388
  theme,
389
+ manifest,
297
390
  density,
298
391
  mode,
299
392
  font,
@@ -303,6 +396,8 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
303
396
  setMode,
304
397
  setFont
305
398
  ]);
399
+ /** The axes on offer under the theme that is actually active. */
400
+ const registry = React.useMemo(() => buildRegistry(themes, theme), [themes, theme]);
306
401
  const parentFontFamilies = React.useContext(FontFamiliesContext);
307
402
  const effectiveFontFamilies = React.useMemo(() => {
308
403
  const sans = fontFamilies?.sans ?? parentFontFamilies?.sans;
@@ -317,17 +412,40 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
317
412
  parentFontFamilies?.sans,
318
413
  parentFontFamilies?.mono
319
414
  ]);
415
+ const overridesKey = JSON.stringify(overrides ?? null);
416
+ const inline = React.useMemo(() => inlineOverrides(overrides, resolvedMode), [overridesKey, resolvedMode]);
417
+ const overridesStyle = React.useMemo(() => overridesCss(overrides), [overridesKey]);
418
+ const parentOverrides = React.useContext(OverridesContext);
419
+ const effectiveOverrides = React.useMemo(() => {
420
+ const merged = {
421
+ ...parentOverrides,
422
+ ...inline
423
+ };
424
+ return Object.keys(merged).length === 0 ? null : merged;
425
+ }, [parentOverrides, inline]);
426
+ React.useEffect(() => {
427
+ if (process.env.NODE_ENV === "production") return;
428
+ reportOverrides(manifest, resolvedMode, overrides, theme);
429
+ }, [
430
+ manifest,
431
+ resolvedMode,
432
+ overridesKey,
433
+ theme
434
+ ]);
320
435
  const style = React.useMemo(() => ({
321
- colorScheme: mode === "system" ? "light dark" : mode,
436
+ colorScheme: manifest?.supportsLight === false ? "dark" : mode === "system" ? "light dark" : mode,
322
437
  "--cue-font-scale": fontScale,
323
438
  ...fontFamilies?.sans === void 0 ? null : { "--cue-font-sans": fontFamilies.sans },
324
439
  ...fontFamilies?.mono === void 0 ? null : { "--cue-font-mono": fontFamilies.mono },
440
+ ...inline,
325
441
  ...styleProp
326
442
  }), [
443
+ manifest?.supportsLight,
327
444
  mode,
328
445
  fontScale,
329
446
  fontFamilies?.sans,
330
447
  fontFamilies?.mono,
448
+ inline,
331
449
  styleProp
332
450
  ]);
333
451
  const stamp = {
@@ -353,15 +471,28 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
353
471
  style,
354
472
  children
355
473
  });
356
- return /* @__PURE__ */ jsx(ThemeContext.Provider, {
357
- value: contextValue,
358
- children: /* @__PURE__ */ jsx(FontScaleContext.Provider, {
359
- value: fontScale,
360
- children: /* @__PURE__ */ jsx(FontFamiliesContext.Provider, {
361
- value: effectiveFontFamilies,
362
- children: /* @__PURE__ */ jsx(DensityContext.Provider, {
363
- value: density,
364
- children: root
474
+ const densityLayer = overridesStyle === "" ? null : /* @__PURE__ */ jsx("style", {
475
+ "data-cue-overrides": "",
476
+ children: overridesStyle
477
+ });
478
+ return /* @__PURE__ */ jsx(ThemeRegistryContext.Provider, {
479
+ value: registry,
480
+ children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
481
+ value: contextValue,
482
+ children: /* @__PURE__ */ jsx(AmbientThemeContext.Provider, {
483
+ value: theme,
484
+ children: /* @__PURE__ */ jsx(FontScaleContext.Provider, {
485
+ value: fontScale,
486
+ children: /* @__PURE__ */ jsx(FontFamiliesContext.Provider, {
487
+ value: effectiveFontFamilies,
488
+ children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
489
+ value: effectiveOverrides,
490
+ children: /* @__PURE__ */ jsxs(DensityContext.Provider, {
491
+ value: density,
492
+ children: [root, densityLayer]
493
+ })
494
+ })
495
+ })
365
496
  })
366
497
  })
367
498
  })
@@ -0,0 +1,53 @@
1
+ import { ThemeRegistry } from "./vocabulary.js";
2
+ import "react";
3
+ import { DensityEntry, FontEntry, ThemeManifest } from "@cueplusplus/theme-base";
4
+ //#region src/system/theme-registry.d.ts
5
+ /**
6
+ * Every registered manifest, in registration order.
7
+ *
8
+ * The array a picker enumerates. It is the `themes` prop the nearest provider
9
+ * was handed, not a copy: registration is an application decision, and this
10
+ * library never discovers a theme by itself.
11
+ *
12
+ * @throws If called outside a `<ThemeProvider>`, like `useTheme()`.
13
+ * @example
14
+ * const themes = useThemes();
15
+ * const { theme, setTheme } = useTheme();
16
+ * return themes.map((m) => (
17
+ * <Chip key={m.name} selected={m.name === theme} onClick={() => setTheme(m.name)}>
18
+ * {m.name}
19
+ * </Chip>
20
+ * ));
21
+ */
22
+ declare function useThemes(): readonly ThemeManifest[];
23
+ /**
24
+ * The rungs an app may offer right now: the base five, then the ACTIVE theme's
25
+ * additions.
26
+ *
27
+ * Active, not registered: a rung a theme adds is emitted under that theme's
28
+ * `[data-theme]`, so offering another theme's rung would offer a control that
29
+ * changes nothing. Switch theme and this list changes with it.
30
+ *
31
+ * @throws If called outside a `<ThemeProvider>`.
32
+ * @example
33
+ * const rungs = useDensities();
34
+ * const { density, setDensity } = useTheme();
35
+ * <Select
36
+ * items={rungs.map((d) => ({ value: d.name, label: d.name }))}
37
+ * value={density}
38
+ * onValueChange={setDensity}
39
+ * />
40
+ */
41
+ declare function useDensities(): readonly DensityEntry[];
42
+ /**
43
+ * The font pairings, by the same rule: the base eight, then the active theme's.
44
+ *
45
+ * @throws If called outside a `<ThemeProvider>`.
46
+ * @example
47
+ * // The stacks a pairing resolves to, for a specimen that renders its own row.
48
+ * const { font } = useTheme();
49
+ * const sans = useFonts().find((f) => f.name === font)?.stacks["font-sans"];
50
+ */
51
+ declare function useFonts(): readonly FontEntry[];
52
+ //#endregion
53
+ export { type ThemeRegistry, useDensities, useFonts, useThemes };
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import "./vocabulary.js";
3
+ import * as React from "react";
4
+ //#region src/system/theme-registry.tsx
5
+ /** Internal: consumers read it through the three hooks. `null` outside a provider. */
6
+ const ThemeRegistryContext = React.createContext(null);
7
+ function useRegistry(hook) {
8
+ const registry = React.useContext(ThemeRegistryContext);
9
+ if (registry === null) throw new Error(`${hook}() must be called inside a <ThemeProvider>.`);
10
+ return registry;
11
+ }
12
+ /**
13
+ * Every registered manifest, in registration order.
14
+ *
15
+ * The array a picker enumerates. It is the `themes` prop the nearest provider
16
+ * was handed, not a copy: registration is an application decision, and this
17
+ * library never discovers a theme by itself.
18
+ *
19
+ * @throws If called outside a `<ThemeProvider>`, like `useTheme()`.
20
+ * @example
21
+ * const themes = useThemes();
22
+ * const { theme, setTheme } = useTheme();
23
+ * return themes.map((m) => (
24
+ * <Chip key={m.name} selected={m.name === theme} onClick={() => setTheme(m.name)}>
25
+ * {m.name}
26
+ * </Chip>
27
+ * ));
28
+ */
29
+ function useThemes() {
30
+ return useRegistry("useThemes").themes;
31
+ }
32
+ /**
33
+ * The rungs an app may offer right now: the base five, then the ACTIVE theme's
34
+ * additions.
35
+ *
36
+ * Active, not registered: a rung a theme adds is emitted under that theme's
37
+ * `[data-theme]`, so offering another theme's rung would offer a control that
38
+ * changes nothing. Switch theme and this list changes with it.
39
+ *
40
+ * @throws If called outside a `<ThemeProvider>`.
41
+ * @example
42
+ * const rungs = useDensities();
43
+ * const { density, setDensity } = useTheme();
44
+ * <Select
45
+ * items={rungs.map((d) => ({ value: d.name, label: d.name }))}
46
+ * value={density}
47
+ * onValueChange={setDensity}
48
+ * />
49
+ */
50
+ function useDensities() {
51
+ return useRegistry("useDensities").densities;
52
+ }
53
+ /**
54
+ * The font pairings, by the same rule: the base eight, then the active theme's.
55
+ *
56
+ * @throws If called outside a `<ThemeProvider>`.
57
+ * @example
58
+ * // The stacks a pairing resolves to, for a specimen that renders its own row.
59
+ * const { font } = useTheme();
60
+ * const sans = useFonts().find((f) => f.name === font)?.stacks["font-sans"];
61
+ */
62
+ function useFonts() {
63
+ return useRegistry("useFonts").fonts;
64
+ }
65
+ //#endregion
66
+ export { ThemeRegistryContext, useDensities, useFonts, useThemes };
@@ -1,4 +1,4 @@
1
- import { Density } from "@cueplusplus/tokens";
1
+ import { Density } from "@cueplusplus/theme-base";
2
2
  //#region src/system/use-density.d.ts
3
3
  /** Rung of the control ladder to measure. */
4
4
  type ControlSize = "sm" | "md" | "lg";
@@ -27,10 +27,18 @@ declare function useDensity(): Density;
27
27
  * canvas renderer needs when it cannot ask CSS.
28
28
  *
29
29
  * Measured from a throwaway `document.body` probe stamped with the ambient
30
- * density, so app-wide token overrides and the `@media (pointer: coarse)`
31
- * re-raise of `ultra-compact` are honoured (overrides scoped to a subtree the
32
- * probe is not in are not). Falls back to the authored ladder × 16px during SSR
33
- * and before the first layout effect, which keeps hydration stable.
30
+ * density *and the ambient theme*, so app-wide token overrides, the `@media
31
+ * (pointer: coarse)` re-raise of `ultra-compact`, and a rung a theme adds are
32
+ * all honoured. Overrides scoped to a subtree the probe is not in are still
33
+ * not: the probe hangs off `document.body`, outside every provider root.
34
+ *
35
+ * Falls back to the authored ladder × 16px during SSR and before the first
36
+ * layout effect, which keeps hydration stable. A rung the ambient theme does
37
+ * not declare falls back to the default ladder with a development warning
38
+ * rather than throwing — the probe corrects it on the next tick either way.
39
+ *
40
+ * Safe outside a `<ThemeProvider>`: it reads the theme and the registry from
41
+ * context directly instead of through `useTheme()`, which throws.
34
42
  *
35
43
  * @param size - Which rung to measure. Defaults to `"md"`.
36
44
  * @returns Height in CSS pixels.