@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,8 +1,11 @@
1
+ import { TokenOverrides, isSafeTokenValue } from "./overrides.js";
1
2
  import { FontFamilies, ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps } from "./theme-provider.js";
2
3
  import { Density, DensityProps } from "./density.js";
3
4
  import { CuePortalFrame, CuePortalFrameProps, useCuePortalProps } from "./portal.js";
4
5
  import { DEFAULT_STORAGE_KEY, PersistedPreferences, PrepaintDefaults, PrepaintOptions, prepaintScript } from "./prepaint.js";
6
+ import { ThemeRegistry } from "./vocabulary.js";
7
+ import { useDensities, useFonts, useThemes } from "./theme-registry.js";
5
8
  import { ControlSize, useControlHeight, useDensity } from "./use-density.js";
6
9
  import { useTheme } from "./use-theme.js";
7
- import { Density as DensityLevel, FontName, Mode, ThemeName } from "@cueplusplus/tokens";
8
- export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityLevel, type DensityProps, type FontFamilies, type FontName, type Mode, type PersistedPreferences, type PrepaintDefaults, type PrepaintOptions, type ResolvedMode, type ThemeContextValue, type ThemeName, ThemeProvider, type ThemeProviderProps, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
10
+ import { Density as DensityLevel, DensityEntry, FontEntry, FontName, Mode, ThemeManifest, ThemeName } from "@cueplusplus/theme-base";
11
+ export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityEntry, type DensityLevel, type DensityProps, type FontEntry, type FontFamilies, type FontName, type Mode, type PersistedPreferences, type PrepaintDefaults, type PrepaintOptions, type ResolvedMode, type ThemeContextValue, type ThemeManifest, type ThemeName, ThemeProvider, type ThemeProviderProps, type ThemeRegistry, type TokenOverrides, isSafeTokenValue, prepaintScript, useControlHeight, useCuePortalProps, useDensities, useDensity, useFonts, useTheme, useThemes };
@@ -1,7 +1,9 @@
1
1
  import { Density } from "./density.js";
2
+ import { isSafeTokenValue } from "./overrides.js";
2
3
  import { DEFAULT_STORAGE_KEY, prepaintScript } from "./prepaint.js";
4
+ import { useDensities, useFonts, useThemes } from "./theme-registry.js";
3
5
  import { ThemeProvider } from "./theme-provider.js";
4
6
  import { CuePortalFrame, useCuePortalProps } from "./portal.js";
5
7
  import { useControlHeight, useDensity } from "./use-density.js";
6
8
  import { useTheme } from "./use-theme.js";
7
- export { CuePortalFrame, DEFAULT_STORAGE_KEY, Density, ThemeProvider, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
9
+ export { CuePortalFrame, DEFAULT_STORAGE_KEY, Density, ThemeProvider, isSafeTokenValue, prepaintScript, useControlHeight, useCuePortalProps, useDensities, useDensity, useFonts, useTheme, useThemes };
@@ -0,0 +1,80 @@
1
+ import "react";
2
+ import { ColorToken, GeometryToken, ThemeManifest } from "@cueplusplus/theme-base";
3
+ //#region src/system/overrides.d.ts
4
+ /**
5
+ * A typed edit over the active theme.
6
+ *
7
+ * Not `ThemeOverrides`: `@cueplusplus/ui/configurator` already exports a type
8
+ * by that name — the serialised edit snapshot its panel writes — and the root
9
+ * barrel re-exports `./system` wholesale, so two public types under one name in
10
+ * one package would be an alias every consumer of both subpaths has to write.
11
+ * This one overrides *tokens*, which is what it is named for.
12
+ *
13
+ * Every key is checked against the token contracts, so a typo is a compile
14
+ * error rather than a custom property nothing reads.
15
+ */
16
+ interface TokenOverrides {
17
+ /**
18
+ * Colour tokens, per block. Only the block the resolved mode is in is
19
+ * emitted, so a provider under `mode="light"` never publishes the dark edit —
20
+ * which is what makes the two independent rather than a merge.
21
+ */
22
+ colors?: {
23
+ dark?: Partial<Record<ColorToken, string>>;
24
+ light?: Partial<Record<ColorToken, string>>;
25
+ };
26
+ /**
27
+ * Geometry, per rung. The key is a rung name — one of the base five, or one a
28
+ * registered theme adds — and the value only the tokens that move.
29
+ */
30
+ densities?: Partial<Record<string, Partial<Record<GeometryToken, string>>>>;
31
+ /**
32
+ * Font stacks. These write `--cue-font-sans` / `--cue-font-mono` /
33
+ * `--cue-font-display` directly, and so shadow the pairing on `data-font` for
34
+ * the subtree — see `ThemeProviderProps.overrides`.
35
+ */
36
+ fonts?: Partial<{
37
+ sans: string;
38
+ mono: string;
39
+ display: string;
40
+ }>;
41
+ }
42
+ /**
43
+ * Whether a string may be written into the density stylesheet this module
44
+ * builds.
45
+ *
46
+ * The same rule {@link overridesCss} applies at `SAFE_VALUE`, exported because a
47
+ * caller that builds a {@link TokenOverrides} wants to refuse a value at the
48
+ * *input* rather than watch it be dropped, silently and one warning later, at
49
+ * the sheet. A panel that cannot ask the question has two bad answers open to
50
+ * it: keep a second copy of this regex, which stops being this regex the first
51
+ * time either moves, or write the value anyway and let a reader wonder why the
52
+ * picture did not change.
53
+ *
54
+ * `@cueplusplus/ui/configurator` exports `isSafeCssValue` (`configurator/_overrides.ts:239`)
55
+ * for the configurator's own snapshot, and this is deliberately **not** that
56
+ * function: same rule, asked from the module that owns it, without dragging in
57
+ * a subpath that also carries `ThemeConfigurator`, `ExportDialog` and an
58
+ * optional `react-aria-components` peer. Neither imports the other, and the
59
+ * test beside this file is what holds them to one answer.
60
+ *
61
+ * **The one thing the rule does not cover, said out loud: an unterminated
62
+ * `/*`.** It carries none of the five refused characters, so it is accepted and
63
+ * written, and everything after it in that stylesheet — the closing brace and
64
+ * any later declaration — is inside a comment. That is a value of the caller's
65
+ * own that stops their *other* overrides applying, not an injection: with `;`,
66
+ * `{`, `}`, `<` and `\` all refused, no second value can close the comment and
67
+ * open a rule. A caller that wants to refuse it as well can say so on top of
68
+ * this; tightening `SAFE_VALUE` itself would change what `overridesCss` writes,
69
+ * which is a different release note from this one.
70
+ *
71
+ * @param value - The proposed declaration value.
72
+ * @returns `true` when it cannot close its declaration, its block or the
73
+ * `<style>` element — see the comment case above for what that does not mean.
74
+ * @example
75
+ * isSafeTokenValue("Iosevka, monospace"); // → true
76
+ * isSafeTokenValue("red;}"); // → false
77
+ */
78
+ declare function isSafeTokenValue(value: string): boolean;
79
+ //#endregion
80
+ export { TokenOverrides, isSafeTokenValue };
@@ -0,0 +1,277 @@
1
+ "use client";
2
+ import { noteOnce, warnOnce } from "./vocabulary.js";
3
+ import * as React from "react";
4
+ import { THEME_NAME_PATTERN, resolve } from "@cueplusplus/theme-base";
5
+ import { contrastReport } from "@cueplusplus/theme-base/contrast";
6
+ //#region src/system/overrides.ts
7
+ /**
8
+ * `overrides` — the one theme edit an application may make without owning a
9
+ * theme package.
10
+ *
11
+ * A theme is a package now, and the honest way to change one is to publish
12
+ * another. But there is a real band of edits below that: one accent for a
13
+ * customer's tenant, a font stack an app already loads, a rung that needs two
14
+ * more pixels for a touch build. Making each of those a package would be a
15
+ * build step for a value that changes per request, and the alternative every
16
+ * application reaches for otherwise — a stray `<style>` with `!important` in it
17
+ * — outranks the token layer everywhere at once and takes the density islands
18
+ * and the portals down with it.
19
+ *
20
+ * So the edits are typed, and they land where the cascade already expects them:
21
+ *
22
+ * - **Colours and fonts are inline custom properties on the provider root.**
23
+ * Inline beats every stylesheet, needs no `!important`, is scoped to the
24
+ * subtree by inheritance, and — because a portal mounts on `<body>` and
25
+ * inherits nothing from its owner — is re-stamped onto the portal container
26
+ * through {@link OverridesContext}, exactly like `--cue-font-scale` already is.
27
+ * - **Densities are one document-scoped `<style>`.** A rung is selected by
28
+ * attribute, not inherited, so it cannot be an inline declaration at all; and
29
+ * `useControlHeight`'s probe hangs off `document.body`, outside every provider
30
+ * root, so a subtree-scoped rule would leave the measurement and the paint
31
+ * disagreeing by exactly the override. See {@link overridesCss}.
32
+ *
33
+ * The dev pass measures the result: an override is a colour decision made
34
+ * without the generator that would have checked it, so this file checks it
35
+ * instead. See {@link reportOverrides}.
36
+ */
37
+ /**
38
+ * The prefix every token in this system is published under.
39
+ *
40
+ * Restated here rather than imported from `../theming`: that group reaches
41
+ * `culori` and the whole generator, and `system/` is in the import graph of
42
+ * every component in the package. One six-character constant is the cheaper
43
+ * copy. `test/system/overrides.test.tsx` holds it to `CUE_TOKEN_PREFIX`.
44
+ */
45
+ const PREFIX = "--cue-";
46
+ /**
47
+ * The already-flattened, referentially stable inline record the provider
48
+ * publishes for portals.
49
+ *
50
+ * Flattened and stable both matter. `usePortalStamp()` builds a fresh object on
51
+ * every render, so a `TokenOverrides` handed down raw would be a new dependency
52
+ * on every render of every overlay, and every open popover would re-write its
53
+ * container's custom properties for nothing. The provider computes this once,
54
+ * keyed on the serialised prop and the resolved mode, and hands the *same*
55
+ * object down until one of those actually moves.
56
+ *
57
+ * `null` means "nothing to stamp" — either no provider above, or a provider
58
+ * whose overrides publish no colour or font for this mode.
59
+ *
60
+ * Internal: consumers get this through the provider, never directly.
61
+ */
62
+ const OverridesContext = React.createContext(null);
63
+ /**
64
+ * The inline declarations for the resolved mode: colours and fonts.
65
+ *
66
+ * Only one colour block is ever emitted. The mode is a fact about the document
67
+ * at this instant, and publishing both blocks would mean the dark edit sat as a
68
+ * dead custom property under a light palette, waiting for somebody to `var()`
69
+ * it by mistake.
70
+ *
71
+ * No validation of the *values* here, deliberately: these go through React's
72
+ * `style` prop and `CSSStyleDeclaration.setProperty`, and the CSSOM drops a
73
+ * declaration it cannot parse. A bad colour is a colour that does not apply,
74
+ * not an injection — which is exactly the difference between this half of the
75
+ * feature and {@link overridesCss}, where the text is written into a stylesheet
76
+ * by hand and has to be guarded.
77
+ *
78
+ * @param overrides - The prop, or `undefined`.
79
+ * @param mode - The mode after `"system"` has been resolved.
80
+ * @returns Custom property name → value; empty when there is nothing to say.
81
+ * @example
82
+ * inlineOverrides({ colors: { light: { accent: "#b35900" } } }, "light");
83
+ * // → { "--cue-accent": "#b35900" }
84
+ */
85
+ function inlineOverrides(overrides, mode) {
86
+ const inline = {};
87
+ for (const [token, value] of Object.entries(overrides?.colors?.[mode] ?? {})) if (value !== void 0) inline[`${PREFIX}${token}`] = value;
88
+ const fonts = overrides?.fonts;
89
+ if (fonts?.sans !== void 0) inline[`${PREFIX}font-sans`] = fonts.sans;
90
+ if (fonts?.mono !== void 0) inline[`${PREFIX}font-mono`] = fonts.mono;
91
+ if (fonts?.display !== void 0) inline[`${PREFIX}font-display`] = fonts.display;
92
+ return inline;
93
+ }
94
+ /**
95
+ * A value that can be written into a stylesheet without closing the rule.
96
+ *
97
+ * The inline half needs no such guard — the CSSOM parses each declaration on
98
+ * its own — but this text is concatenated into a `<style>` by hand, so a value
99
+ * carrying `;`, `}` or `<` would end the declaration, the block or the element
100
+ * and let whatever follows be parsed as CSS. An override value can easily come
101
+ * from a tenant record or a query string, so the check is not theoretical.
102
+ */
103
+ const SAFE_VALUE = /^[^;{}<>\\]+$/;
104
+ /**
105
+ * Whether a string may be written into the density stylesheet this module
106
+ * builds.
107
+ *
108
+ * The same rule {@link overridesCss} applies at `SAFE_VALUE`, exported because a
109
+ * caller that builds a {@link TokenOverrides} wants to refuse a value at the
110
+ * *input* rather than watch it be dropped, silently and one warning later, at
111
+ * the sheet. A panel that cannot ask the question has two bad answers open to
112
+ * it: keep a second copy of this regex, which stops being this regex the first
113
+ * time either moves, or write the value anyway and let a reader wonder why the
114
+ * picture did not change.
115
+ *
116
+ * `@cueplusplus/ui/configurator` exports `isSafeCssValue` (`configurator/_overrides.ts:239`)
117
+ * for the configurator's own snapshot, and this is deliberately **not** that
118
+ * function: same rule, asked from the module that owns it, without dragging in
119
+ * a subpath that also carries `ThemeConfigurator`, `ExportDialog` and an
120
+ * optional `react-aria-components` peer. Neither imports the other, and the
121
+ * test beside this file is what holds them to one answer.
122
+ *
123
+ * **The one thing the rule does not cover, said out loud: an unterminated
124
+ * `/*`.** It carries none of the five refused characters, so it is accepted and
125
+ * written, and everything after it in that stylesheet — the closing brace and
126
+ * any later declaration — is inside a comment. That is a value of the caller's
127
+ * own that stops their *other* overrides applying, not an injection: with `;`,
128
+ * `{`, `}`, `<` and `\` all refused, no second value can close the comment and
129
+ * open a rule. A caller that wants to refuse it as well can say so on top of
130
+ * this; tightening `SAFE_VALUE` itself would change what `overridesCss` writes,
131
+ * which is a different release note from this one.
132
+ *
133
+ * @param value - The proposed declaration value.
134
+ * @returns `true` when it cannot close its declaration, its block or the
135
+ * `<style>` element — see the comment case above for what that does not mean.
136
+ * @example
137
+ * isSafeTokenValue("Iosevka, monospace"); // → true
138
+ * isSafeTokenValue("red;}"); // → false
139
+ */
140
+ function isSafeTokenValue(value) {
141
+ return SAFE_VALUE.test(value);
142
+ }
143
+ /**
144
+ * The document-scoped stylesheet text for the density layer.
145
+ *
146
+ * **Why the selector says the attribute twice.** A rung a theme retunes ships
147
+ * as `[data-theme="t"] [data-density="x"]` — specificity (0,2,0). An override
148
+ * that has to beat the base rung *and* tie the theme's own has to reach (0,2,0)
149
+ * as well, and the honest way to write that on a single element is to repeat
150
+ * the attribute: no invented ancestor, no `!important`, no `:is()` trick whose
151
+ * specificity depends on its longest argument. Tied on specificity, source
152
+ * order decides — and the provider's element is appended to `<body>`, after
153
+ * every stylesheet in `<head>`, so the override wins. The same arithmetic puts
154
+ * it above the configurator's persisted snapshot, which writes bare
155
+ * `[data-density="x"]` at (0,1,0) into `<head>`.
156
+ *
157
+ * **Why it is document-scoped rather than nested under the provider root.**
158
+ * `useControlHeight()` measures `--cue-control-<size>` on a throwaway element
159
+ * appended to `document.body`, which is outside `[data-cue-root]`. A rule
160
+ * scoped under the root would leave that probe reading unoverridden geometry
161
+ * while the control beside it painted the override, and every consumer of the
162
+ * measured height — virtualised rows, canvas layout, the DMX grid — would be
163
+ * off by exactly the override. The cost of the document scope is stated in
164
+ * `ThemeProviderProps.overrides`: a nested provider's rung edits reach the
165
+ * whole page, the same way a theme's own rung rules do.
166
+ *
167
+ * @param overrides - The prop, or `undefined`.
168
+ * @returns One rule per rung, newline-separated; `""` when there is nothing to
169
+ * emit, which is the provider's signal to render no element at all.
170
+ * @example
171
+ * overridesCss({ densities: { compact: { "control-md": "1.75rem" } } });
172
+ * // → '[data-density="compact"][data-density="compact"]{--cue-control-md:1.75rem}'
173
+ */
174
+ function overridesCss(overrides) {
175
+ const rules = [];
176
+ for (const [rung, patch] of Object.entries(overrides?.densities ?? {})) {
177
+ if (!THEME_NAME_PATTERN.test(rung)) {
178
+ warnOnce(`overrides: density "${rung}" is not a usable rung name; its overrides are dropped`);
179
+ continue;
180
+ }
181
+ const declarations = [];
182
+ for (const [token, value] of Object.entries(patch ?? {})) {
183
+ if (value === void 0) continue;
184
+ if (!SAFE_VALUE.test(value)) {
185
+ warnOnce(`overrides: value ${JSON.stringify(value)} for "${token}" is not a CSS value`);
186
+ continue;
187
+ }
188
+ declarations.push(`${PREFIX}${token}:${value}`);
189
+ }
190
+ if (declarations.length === 0) continue;
191
+ rules.push(`[data-density="${rung}"][data-density="${rung}"]{${declarations.join(";")}}`);
192
+ }
193
+ return rules.join("\n");
194
+ }
195
+ /**
196
+ * Measure the palette an override actually produces, and name what it broke.
197
+ *
198
+ * `createTheme()` hands back a contrast report because a generated palette that
199
+ * nobody measured is a colour toy. An `overrides.colors` edit is the same
200
+ * decision made *without* the generator — one hex, dropped over a theme whose
201
+ * own build already cleared the eleven pairs — so it is measured here instead,
202
+ * against the palette it is layered onto.
203
+ *
204
+ * **Only what the override is answerable for is reported.** The palette is
205
+ * measured twice, once without the edit and once with it, and a pair is named
206
+ * when the edit introduced its failure, lowered its ratio, or *named either of
207
+ * its tokens*. Five of the ten themes this repository ships carry a **waived**
208
+ * required failure — a pair their own build recorded as known-bad — and
209
+ * reporting the overridden result flat would blame a tenant's accent for a pair
210
+ * it never touched, under a message that begins with the word `overrides:`. A
211
+ * report a reader learns to ignore is worse than no report. But a waiver covers
212
+ * the ink the *theme* shipped: the moment an app writes one of the two tokens
213
+ * itself, the pair is the app's, whichever way the ratio moved. See `caused()`.
214
+ *
215
+ * Failures `console.warn` and advisories `console.info`, matching the two tiers
216
+ * `contrastReport()` itself keeps: a failure is what makes a screen unreadable,
217
+ * an advisory is a ring nobody has been failed on yet.
218
+ *
219
+ * With no manifest registered for the active name the baseline is the blank
220
+ * base, which is what `resolve(null, …)` returns and what an unregistered theme
221
+ * can be measured against — the numbers are then the base's, not the palette
222
+ * the document is painted in. The message still names the theme the document is
223
+ * *stamped* with, because that is the screen the reader is looking at.
224
+ *
225
+ * Development only, and once per distinct message per process.
226
+ *
227
+ * @param manifest - The active theme's manifest, or `null` when unregistered.
228
+ * @param mode - The mode after `"system"` has been resolved.
229
+ * @param overrides - The prop, or `undefined`.
230
+ * @param theme - The name on `data-theme`, for the message.
231
+ */
232
+ function reportOverrides(manifest, mode, overrides, theme) {
233
+ if (process.env.NODE_ENV === "production") return;
234
+ const patch = overrides?.colors?.[mode];
235
+ if (patch === void 0 || Object.keys(patch).length === 0) return;
236
+ const base = resolve(manifest, { mode }).colors;
237
+ let before;
238
+ let after;
239
+ try {
240
+ before = new Map(contrastReport([{
241
+ mode,
242
+ tokens: base
243
+ }]).checks.map((check) => [check.id, check]));
244
+ after = contrastReport([{
245
+ mode,
246
+ tokens: {
247
+ ...base,
248
+ ...patch
249
+ }
250
+ }]);
251
+ } catch {
252
+ warnOnce(`overrides: a value in the ${mode} block is not a colour this can measure; the contrast pass was skipped`);
253
+ return;
254
+ }
255
+ /**
256
+ * A pair the edit is answerable for.
257
+ *
258
+ * Three of these clauses are about the *result* moving — newly measurable,
259
+ * newly failing, failing worse. The fourth is about authorship, and it is the
260
+ * one that took a review to find: an override that names either token of a
261
+ * pair has adopted that pair, even when the theme was already failing it and
262
+ * even when the edit improved the ratio without clearing the floor. Under
263
+ * `venu`, whose own build waived `accent-fg/accent` at 2.67, an app setting
264
+ * `accent-fg` to something that lands at 3.60 has made a colour decision of
265
+ * its own and is still under the 4.5 floor — and the theme's waiver covers
266
+ * the theme's ink, not the app's. Without the clause the report would go
267
+ * quiet on exactly the pair the app just took responsibility for.
268
+ */
269
+ const caused = (check) => {
270
+ const was = before.get(check.id);
271
+ return was === void 0 || was.passes || check.ratio < was.ratio || check.foreground in patch || check.background in patch;
272
+ };
273
+ for (const check of after.failures) if (caused(check)) warnOnce(`overrides: ${check.id} at ${check.ratio.toFixed(2)}:1 fails its ${check.minimum} floor under theme "${theme}"`);
274
+ for (const check of after.advisories) if (caused(check)) noteOnce(`overrides: ${check.id} at ${check.ratio.toFixed(2)}:1 misses its ${check.minimum} advisory floor under theme "${theme}"`);
275
+ }
276
+ //#endregion
277
+ export { OverridesContext, inlineOverrides, isSafeTokenValue, overridesCss, reportOverrides };
@@ -6,7 +6,8 @@ import * as React from "react";
6
6
  *
7
7
  * Returns a `container` element appended to `document.body` and stamped with
8
8
  * `data-theme` / `data-density` / `data-font` / `data-mode` (plus
9
- * `--cue-font-scale`) copied from the nearest provider and density island, and
9
+ * `--cue-font-scale` and any `ThemeProvider` `overrides` colour or font
10
+ * property) copied from the nearest provider and density island, and
10
11
  * with `data-cue-skin` / `data-cue-fidelity` when the caller sits inside an
11
12
  * `AgentSurface` island. The container is
12
13
  * `display: contents`, so it changes no layout and creates no containing block —
@@ -41,7 +42,8 @@ interface CuePortalFrameProps {
41
42
  *
42
43
  * Wraps `children` in a `display: contents` element carrying the same
43
44
  * `data-theme` / `data-density` / `data-font` / `data-mode` / `data-cue-skin` /
44
- * `data-cue-fidelity` stamp and `--cue-font-scale` as {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
45
+ * `data-cue-fidelity` stamp, `--cue-font-scale` and `overrides` properties as
46
+ * {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
45
47
  * element per overlay instead of one per render tree — and reach for this only
46
48
  * when the container prop is not available.
47
49
  *
@@ -2,6 +2,7 @@
2
2
  import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
3
3
  import { AgentSkinContext } from "./agent-skin.js";
4
4
  import { DensityContext } from "./density.js";
5
+ import { OverridesContext } from "./overrides.js";
5
6
  import { FontFamiliesContext, FontScaleContext, ThemeContext } from "./theme-provider.js";
6
7
  import * as React from "react";
7
8
  import { jsx } from "react/jsx-runtime";
@@ -19,13 +20,15 @@ const FONT_MONO_PROPERTY = "--cue-font-mono";
19
20
  * read the *body's* theme, which is exactly the bug this contract exists to
20
21
  * prevent. The same detachment is why `--cue-font-scale` travels here too: the
21
22
  * provider publishes it as an inline custom property, which a body-level portal
22
- * would otherwise never inherit.
23
+ * would otherwise never inherit — and why the provider's `overrides` do, for
24
+ * exactly the same reason and by the same route.
23
25
  */
24
26
  function usePortalStamp() {
25
27
  const theme = React.useContext(ThemeContext);
26
28
  const island = React.useContext(DensityContext);
27
29
  const fontScale = React.useContext(FontScaleContext);
28
30
  const fontFamilies = React.useContext(FontFamiliesContext);
31
+ const overrides = React.useContext(OverridesContext);
29
32
  const skin = React.useContext(AgentSkinContext);
30
33
  return {
31
34
  theme: theme?.theme ?? DEFAULT_THEME,
@@ -34,6 +37,7 @@ function usePortalStamp() {
34
37
  font: theme?.font ?? DEFAULT_FONT,
35
38
  fontScale,
36
39
  fontFamilies,
40
+ overrides,
37
41
  skin: skin?.skin ?? null,
38
42
  fidelity: skin?.fidelity ?? null
39
43
  };
@@ -44,7 +48,8 @@ function usePortalStamp() {
44
48
  *
45
49
  * Returns a `container` element appended to `document.body` and stamped with
46
50
  * `data-theme` / `data-density` / `data-font` / `data-mode` (plus
47
- * `--cue-font-scale`) copied from the nearest provider and density island, and
51
+ * `--cue-font-scale` and any `ThemeProvider` `overrides` colour or font
52
+ * property) copied from the nearest provider and density island, and
48
53
  * with `data-cue-skin` / `data-cue-fidelity` when the caller sits inside an
49
54
  * `AgentSurface` island. The container is
50
55
  * `display: contents`, so it changes no layout and creates no containing block —
@@ -92,11 +97,29 @@ function useCuePortalProps() {
92
97
  stamp.fontScale,
93
98
  stamp.fontFamilies?.sans,
94
99
  stamp.fontFamilies?.mono,
100
+ stamp.overrides,
95
101
  stamp.skin,
96
102
  stamp.fidelity
97
103
  ]);
98
104
  return React.useMemo(() => container === null ? {} : { container }, [container]);
99
105
  }
106
+ /**
107
+ * The override property names last written to each container.
108
+ *
109
+ * A stamp is applied over the top of the previous one, and `style.setProperty`
110
+ * only ever adds. So a key that *disappears* — the app switching to a mode
111
+ * whose block names different tokens, or dropping the prop — would leave its
112
+ * `--cue-*` on the container for the life of the overlay, with the in-tree
113
+ * content already painting without it. This is the record of what to take off.
114
+ *
115
+ * A `WeakMap` keyed on the element rather than an attribute on it: the same
116
+ * bookkeeping written as `data-cue-override-keys` would be one more
117
+ * document-visible mutation on every write, a second place for the truth to
118
+ * live, and a new `data-` attribute in a file whose whole contract is which
119
+ * `data-` attributes a portal carries. Containers are removed with the overlay
120
+ * and collected with it, which is what makes the weak reference the right one.
121
+ */
122
+ const stampedOverrides = /* @__PURE__ */ new WeakMap();
100
123
  /** Write a stamp onto a live element (the imperative half of the contract). */
101
124
  function applyStamp(element, stamp) {
102
125
  element.setAttribute("data-theme", stamp.theme);
@@ -107,10 +130,16 @@ function applyStamp(element, stamp) {
107
130
  else element.setAttribute("data-cue-skin", stamp.skin);
108
131
  if (stamp.fidelity === null) element.removeAttribute("data-cue-fidelity");
109
132
  else element.setAttribute("data-cue-fidelity", stamp.fidelity);
133
+ const overrides = stamp.overrides ?? {};
134
+ for (const property of stampedOverrides.get(element) ?? []) if (!(property in overrides)) element.style.removeProperty(property);
110
135
  if (stamp.fontScale === null) element.style.removeProperty(FONT_SCALE_PROPERTY);
111
136
  else element.style.setProperty(FONT_SCALE_PROPERTY, String(stamp.fontScale));
112
137
  setOptionalProperty(element, FONT_SANS_PROPERTY, stamp.fontFamilies?.sans);
113
138
  setOptionalProperty(element, FONT_MONO_PROPERTY, stamp.fontFamilies?.mono);
139
+ for (const [property, value] of Object.entries(overrides)) element.style.setProperty(property, value);
140
+ const keys = Object.keys(overrides);
141
+ if (keys.length === 0) stampedOverrides.delete(element);
142
+ else stampedOverrides.set(element, keys);
114
143
  }
115
144
  function setOptionalProperty(element, property, value) {
116
145
  if (value === void 0) element.style.removeProperty(property);
@@ -122,7 +151,8 @@ function setOptionalProperty(element, property, value) {
122
151
  *
123
152
  * Wraps `children` in a `display: contents` element carrying the same
124
153
  * `data-theme` / `data-density` / `data-font` / `data-mode` / `data-cue-skin` /
125
- * `data-cue-fidelity` stamp and `--cue-font-scale` as {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
154
+ * `data-cue-fidelity` stamp, `--cue-font-scale` and `overrides` properties as
155
+ * {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
126
156
  * element per overlay instead of one per render tree — and reach for this only
127
157
  * when the container prop is not available.
128
158
  *
@@ -136,6 +166,7 @@ function CuePortalFrame({ children, className, style }) {
136
166
  ...stamp.fontScale === null ? null : { [FONT_SCALE_PROPERTY]: stamp.fontScale },
137
167
  ...stamp.fontFamilies?.sans === void 0 ? null : { [FONT_SANS_PROPERTY]: stamp.fontFamilies.sans },
138
168
  ...stamp.fontFamilies?.mono === void 0 ? null : { [FONT_MONO_PROPERTY]: stamp.fontFamilies.mono },
169
+ ...stamp.overrides,
139
170
  ...style
140
171
  };
141
172
  return /* @__PURE__ */ jsx("div", {
@@ -1,4 +1,4 @@
1
- import { Density, FontName, Mode, ThemeName } from "@cueplusplus/tokens";
1
+ import { Density, FontName, Mode, ThemeManifest, ThemeName } from "@cueplusplus/theme-base";
2
2
  //#region src/system/prepaint.d.ts
3
3
  /** localStorage key the ThemeProvider and the pre-paint script share by default. */
4
4
  declare const DEFAULT_STORAGE_KEY = "cue-ui";
@@ -13,22 +13,49 @@ interface PersistedPreferences {
13
13
  /** Last font pairing the user picked. */
14
14
  font?: string;
15
15
  }
16
- /** Complete server-authoritative fallback stamped when storage is absent or ignored. */
16
+ /**
17
+ * The server-authoritative fallback stamped when storage is absent or ignored.
18
+ *
19
+ * `theme` and `mode` are required because nothing else can supply them: the
20
+ * theme is the app's own decision and the mode has no per-theme preference to
21
+ * fall back to. `density` and `font` are optional on purpose — leave either out
22
+ * and the script opens on the rung or the pairing `defaults.theme` itself
23
+ * declares (§5's `densities.default` / `fontPairings.default`), which is
24
+ * exactly what `<ThemeProvider>` opens on when the consumer passes no `density`
25
+ * or `font`. Naming one here is therefore the same statement as passing the
26
+ * prop: it overrides the theme's preference for the first paint, and the
27
+ * provider must be given the matching prop or the two disagree by a frame.
28
+ */
17
29
  interface PrepaintDefaults {
18
30
  theme: ThemeName;
19
- density: Density;
31
+ /** Initial density rung. Omitted, the theme's own `densities.default` decides. */
32
+ density?: Density;
20
33
  mode: Mode;
21
- /** Initial document-wide font pairing. Defaults to the token package's system pairing. */
34
+ /** Initial document-wide font pairing. Omitted, the theme's own `fontPairings.default` decides. */
22
35
  font?: FontName;
23
36
  }
24
37
  /** Options form for apps whose server projection, not localStorage, owns first paint. */
25
38
  interface PrepaintOptions {
26
39
  /** localStorage key shared with ThemeProvider. */
27
40
  storageKey?: string;
28
- /** Complete validated fallback triple. */
41
+ /** The validated fallback: `theme` and `mode` always, `density` and `font` only to override the theme's own. */
29
42
  defaults: PrepaintDefaults;
30
43
  /** Whether valid stored axes may override `defaults`. Defaults to `true`. */
31
44
  readStoredPreferences?: boolean;
45
+ /**
46
+ * The manifests this app registers, in the same order and of the same shape
47
+ * as `<ThemeProvider themes>`. The script inlines their names, and per theme
48
+ * the rungs and pairings that theme offers, so a stored preference is judged
49
+ * before paint by exactly the rule the provider applies after it.
50
+ *
51
+ * Omitted — and it is omitted by the positional overload, which cannot carry
52
+ * it — the script gets the empty-registry rule: any well-formed theme name is
53
+ * accepted, and density and font validate against the base axes alone. That
54
+ * is deliberate rather than lax. An app that registers nothing has whatever
55
+ * theme its stylesheet paints, and refusing the name it stored would repaint
56
+ * every returning visitor's first frame in a theme they did not choose.
57
+ */
58
+ themes?: readonly ThemeManifest[];
32
59
  }
33
60
  /**
34
61
  * Build the blocking inline script that stamps `data-theme`, `data-density`,
@@ -38,8 +65,32 @@ interface PrepaintOptions {
38
65
  * Render it as `<script dangerouslySetInnerHTML={{ __html: prepaintScript() }} />`
39
66
  * in `<head>`, above everything else. The output never contains `</script>` and
40
67
  * never throws: private-mode localStorage failures degrade to the dark-first
41
- * defaults (`cue` / `compact` / `system` / `dark`), which are also what bare
42
- * `:root` in `@cueplusplus/tokens/theme.css` already paints.
68
+ * defaults (`cue` / `compact` / `system` / `dark`). Two of those four are what
69
+ * bare `:root` already carries — `compact` geometry and the dark palette, from
70
+ * `@cueplusplus/theme-base/base.css` and the `@cueplusplus/tokens/axes.css` it
71
+ * imports. `cue` is not: since this release no stylesheet the library ships
72
+ * declares a `[data-theme="cue"]` block, so stamping that name paints the blank
73
+ * base unless the app imported `@cueplusplus/theme-cue/theme.css` itself. The
74
+ * script's job is to make the first frame agree with the first commit, and it
75
+ * still does — both are whatever the page's stylesheets say `cue` means.
76
+ *
77
+ * What the script *accepts* out of storage is decided by the same rule the
78
+ * provider applies a moment later. Pass the options form with `themes` — the
79
+ * array `<ThemeProvider themes>` is given — and the registered names are
80
+ * inlined, along with the rungs and pairings each theme offers, so a rung one
81
+ * theme adds is not accepted on first paint under a theme that does not have
82
+ * it. Pass nothing and the empty-registry rule applies: any well-formed theme
83
+ * name, and the base density and font axes. The positional overload below
84
+ * cannot carry a registry and always gets that rule.
85
+ *
86
+ * What it *stamps* for a visitor with nothing stored is decided the same way.
87
+ * The options form needs only `defaults.theme` and `defaults.mode`: leave
88
+ * `defaults.density` or `defaults.font` out and the script opens on the rung
89
+ * and the pairing that theme itself declares (§5's `densities.default` /
90
+ * `fontPairings.default`), which is precisely what `<ThemeProvider>` opens on
91
+ * when no `density` or `font` prop names one. Name either here and you are
92
+ * making the same statement the prop makes — so pass the matching prop, or the
93
+ * first paint and the first commit disagree by a frame.
43
94
  *
44
95
  * @param storageKey - localStorage key to read. Must match the `storageKey`
45
96
  * passed to `<ThemeProvider>`. Defaults to {@link DEFAULT_STORAGE_KEY}.