@cueplusplus/ui 0.1.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 (372) hide show
  1. package/dist/charts/_token.js +39 -0
  2. package/dist/charts/container.d.ts +58 -0
  3. package/dist/charts/container.js +77 -0
  4. package/dist/charts/index.d.ts +5 -0
  5. package/dist/charts/index.js +5 -0
  6. package/dist/charts/ramps.d.ts +140 -0
  7. package/dist/charts/ramps.js +615 -0
  8. package/dist/charts/theme.d.ts +112 -0
  9. package/dist/charts/theme.js +165 -0
  10. package/dist/charts/tones.d.ts +111 -0
  11. package/dist/charts/tones.js +127 -0
  12. package/dist/chat/_glyphs.js +55 -0
  13. package/dist/chat/agent-color.d.ts +84 -0
  14. package/dist/chat/agent-color.js +101 -0
  15. package/dist/chat/agent-pile.d.ts +55 -0
  16. package/dist/chat/agent-pile.js +121 -0
  17. package/dist/chat/ask-box.d.ts +88 -0
  18. package/dist/chat/ask-box.js +341 -0
  19. package/dist/chat/composer.d.ts +66 -0
  20. package/dist/chat/composer.js +144 -0
  21. package/dist/chat/delegation-card.d.ts +60 -0
  22. package/dist/chat/delegation-card.js +129 -0
  23. package/dist/chat/index.d.ts +8 -0
  24. package/dist/chat/index.js +8 -0
  25. package/dist/chat/message-list.d.ts +39 -0
  26. package/dist/chat/message-list.js +105 -0
  27. package/dist/chat/message.d.ts +47 -0
  28. package/dist/chat/message.js +108 -0
  29. package/dist/chrome/_glyphs.js +101 -0
  30. package/dist/chrome/_tabs-scroll.d.ts +62 -0
  31. package/dist/chrome/_tabs-scroll.js +68 -0
  32. package/dist/chrome/_tree.d.ts +75 -0
  33. package/dist/chrome/_tree.js +49 -0
  34. package/dist/chrome/empty-state.d.ts +37 -0
  35. package/dist/chrome/empty-state.js +57 -0
  36. package/dist/chrome/eyebrow.d.ts +29 -0
  37. package/dist/chrome/eyebrow.js +36 -0
  38. package/dist/chrome/index.d.ts +17 -0
  39. package/dist/chrome/index.js +17 -0
  40. package/dist/chrome/navigation-menu.d.ts +80 -0
  41. package/dist/chrome/navigation-menu.js +121 -0
  42. package/dist/chrome/page-shell.d.ts +38 -0
  43. package/dist/chrome/page-shell.js +66 -0
  44. package/dist/chrome/panel.d.ts +55 -0
  45. package/dist/chrome/panel.js +67 -0
  46. package/dist/chrome/row.d.ts +82 -0
  47. package/dist/chrome/row.js +106 -0
  48. package/dist/chrome/scroll-area.d.ts +39 -0
  49. package/dist/chrome/scroll-area.js +70 -0
  50. package/dist/chrome/section-header.d.ts +42 -0
  51. package/dist/chrome/section-header.js +79 -0
  52. package/dist/chrome/segmented-control.d.ts +58 -0
  53. package/dist/chrome/segmented-control.js +76 -0
  54. package/dist/chrome/status-bar.d.ts +46 -0
  55. package/dist/chrome/status-bar.js +56 -0
  56. package/dist/chrome/tabs.d.ts +80 -0
  57. package/dist/chrome/tabs.js +176 -0
  58. package/dist/chrome/title-bar.d.ts +37 -0
  59. package/dist/chrome/title-bar.js +45 -0
  60. package/dist/chrome/toolbar.d.ts +46 -0
  61. package/dist/chrome/toolbar.js +62 -0
  62. package/dist/chrome/tree.d.ts +98 -0
  63. package/dist/chrome/tree.js +248 -0
  64. package/dist/color/_checker.js +14 -0
  65. package/dist/color/_convert.d.ts +52 -0
  66. package/dist/color/_convert.js +66 -0
  67. package/dist/color/_glyphs.js +28 -0
  68. package/dist/color/_thumb.js +15 -0
  69. package/dist/color/_value.js +28 -0
  70. package/dist/color/color-area.d.ts +27 -0
  71. package/dist/color/color-area.js +40 -0
  72. package/dist/color/color-field.d.ts +32 -0
  73. package/dist/color/color-field.js +43 -0
  74. package/dist/color/color-picker.d.ts +71 -0
  75. package/dist/color/color-picker.js +178 -0
  76. package/dist/color/color-slider.d.ts +31 -0
  77. package/dist/color/color-slider.js +51 -0
  78. package/dist/color/color-swatch.d.ts +32 -0
  79. package/dist/color/color-swatch.js +44 -0
  80. package/dist/color/index.d.ts +7 -0
  81. package/dist/color/index.js +7 -0
  82. package/dist/configurator/_export.d.ts +78 -0
  83. package/dist/configurator/_export.js +261 -0
  84. package/dist/configurator/_glyphs.js +92 -0
  85. package/dist/configurator/_overrides.d.ts +247 -0
  86. package/dist/configurator/_overrides.js +514 -0
  87. package/dist/configurator/configurator.d.ts +71 -0
  88. package/dist/configurator/configurator.js +279 -0
  89. package/dist/configurator/export-dialog.d.ts +30 -0
  90. package/dist/configurator/export-dialog.js +121 -0
  91. package/dist/configurator/index.d.ts +8 -0
  92. package/dist/configurator/index.js +8 -0
  93. package/dist/configurator/panel-sections.d.ts +86 -0
  94. package/dist/configurator/panel-sections.js +316 -0
  95. package/dist/configurator/token-editor.d.ts +74 -0
  96. package/dist/configurator/token-editor.js +149 -0
  97. package/dist/configurator/use-zoom.d.ts +87 -0
  98. package/dist/configurator/use-zoom.js +113 -0
  99. package/dist/date/_glyphs.js +34 -0
  100. package/dist/date/_segments.js +55 -0
  101. package/dist/date/_trigger.js +31 -0
  102. package/dist/date/calendar.d.ts +25 -0
  103. package/dist/date/calendar.js +125 -0
  104. package/dist/date/date-field.d.ts +34 -0
  105. package/dist/date/date-field.js +41 -0
  106. package/dist/date/date-picker.d.ts +69 -0
  107. package/dist/date/date-picker.js +89 -0
  108. package/dist/date/date-range-picker.d.ts +65 -0
  109. package/dist/date/date-range-picker.js +92 -0
  110. package/dist/date/index.d.ts +6 -0
  111. package/dist/date/index.js +6 -0
  112. package/dist/date/time-field.d.ts +29 -0
  113. package/dist/date/time-field.js +36 -0
  114. package/dist/dmx/_ramp.d.ts +77 -0
  115. package/dist/dmx/_ramp.js +169 -0
  116. package/dist/dmx/_readout.js +53 -0
  117. package/dist/dmx/_universe.d.ts +105 -0
  118. package/dist/dmx/_universe.js +112 -0
  119. package/dist/dmx/channel-matrix.d.ts +81 -0
  120. package/dist/dmx/channel-matrix.js +285 -0
  121. package/dist/dmx/dmx-bar.d.ts +50 -0
  122. package/dist/dmx/dmx-bar.js +107 -0
  123. package/dist/dmx/dmx-strip.d.ts +41 -0
  124. package/dist/dmx/dmx-strip.js +196 -0
  125. package/dist/dmx/index.d.ts +7 -0
  126. package/dist/dmx/index.js +7 -0
  127. package/dist/dmx/universe-grid.d.ts +61 -0
  128. package/dist/dmx/universe-grid.js +182 -0
  129. package/dist/flow/_frame.d.ts +52 -0
  130. package/dist/flow/_frame.js +38 -0
  131. package/dist/flow/controls.d.ts +41 -0
  132. package/dist/flow/controls.js +53 -0
  133. package/dist/flow/flow.css +121 -0
  134. package/dist/flow/handles.d.ts +40 -0
  135. package/dist/flow/handles.js +61 -0
  136. package/dist/flow/index.d.ts +7 -0
  137. package/dist/flow/index.js +7 -0
  138. package/dist/flow/node-card.d.ts +71 -0
  139. package/dist/flow/node-card.js +102 -0
  140. package/dist/flow/signal-edge.d.ts +60 -0
  141. package/dist/flow/signal-edge.js +108 -0
  142. package/dist/flow/use-flow-theme.d.ts +69 -0
  143. package/dist/flow/use-flow-theme.js +109 -0
  144. package/dist/forms/_chassis.d.ts +64 -0
  145. package/dist/forms/_chassis.js +59 -0
  146. package/dist/forms/_glyphs.js +143 -0
  147. package/dist/forms/_listbox.js +55 -0
  148. package/dist/forms/autocomplete.d.ts +61 -0
  149. package/dist/forms/autocomplete.js +88 -0
  150. package/dist/forms/button-group.d.ts +47 -0
  151. package/dist/forms/button-group.js +52 -0
  152. package/dist/forms/checkbox.d.ts +51 -0
  153. package/dist/forms/checkbox.js +98 -0
  154. package/dist/forms/combobox.d.ts +73 -0
  155. package/dist/forms/combobox.js +108 -0
  156. package/dist/forms/env-var-input.d.ts +74 -0
  157. package/dist/forms/env-var-input.js +116 -0
  158. package/dist/forms/field.d.ts +89 -0
  159. package/dist/forms/field.js +102 -0
  160. package/dist/forms/file-upload.d.ts +113 -0
  161. package/dist/forms/file-upload.js +232 -0
  162. package/dist/forms/index.d.ts +26 -0
  163. package/dist/forms/index.js +26 -0
  164. package/dist/forms/input-group.d.ts +31 -0
  165. package/dist/forms/input-group.js +54 -0
  166. package/dist/forms/input.d.ts +33 -0
  167. package/dist/forms/input.js +36 -0
  168. package/dist/forms/multi-select.d.ts +62 -0
  169. package/dist/forms/multi-select.js +107 -0
  170. package/dist/forms/number-field.d.ts +38 -0
  171. package/dist/forms/number-field.js +64 -0
  172. package/dist/forms/otp-field.d.ts +54 -0
  173. package/dist/forms/otp-field.js +59 -0
  174. package/dist/forms/password-input.d.ts +46 -0
  175. package/dist/forms/password-input.js +62 -0
  176. package/dist/forms/radio-group.d.ts +50 -0
  177. package/dist/forms/radio-group.js +64 -0
  178. package/dist/forms/rating.d.ts +69 -0
  179. package/dist/forms/rating.js +103 -0
  180. package/dist/forms/scrub-input.d.ts +81 -0
  181. package/dist/forms/scrub-input.js +249 -0
  182. package/dist/forms/search-input.d.ts +54 -0
  183. package/dist/forms/search-input.js +112 -0
  184. package/dist/forms/select.d.ts +75 -0
  185. package/dist/forms/select.js +91 -0
  186. package/dist/forms/slider.d.ts +44 -0
  187. package/dist/forms/slider.js +65 -0
  188. package/dist/forms/switch.d.ts +36 -0
  189. package/dist/forms/switch.js +71 -0
  190. package/dist/forms/tags-input.d.ts +69 -0
  191. package/dist/forms/tags-input.js +148 -0
  192. package/dist/forms/textarea.d.ts +27 -0
  193. package/dist/forms/textarea.js +37 -0
  194. package/dist/forms/toggle-group.d.ts +41 -0
  195. package/dist/forms/toggle-group.js +56 -0
  196. package/dist/forms/toggle.d.ts +41 -0
  197. package/dist/forms/toggle.js +100 -0
  198. package/dist/index.d.ts +112 -0
  199. package/dist/index.js +104 -0
  200. package/dist/instruments/_ansi.js +116 -0
  201. package/dist/instruments/_glyphs.js +90 -0
  202. package/dist/instruments/_motion.js +56 -0
  203. package/dist/instruments/_rem.js +21 -0
  204. package/dist/instruments/app-window-frame.d.ts +48 -0
  205. package/dist/instruments/app-window-frame.js +76 -0
  206. package/dist/instruments/copy-button.d.ts +41 -0
  207. package/dist/instruments/copy-button.js +112 -0
  208. package/dist/instruments/data-table.d.ts +168 -0
  209. package/dist/instruments/data-table.js +212 -0
  210. package/dist/instruments/group-bar.d.ts +63 -0
  211. package/dist/instruments/group-bar.js +122 -0
  212. package/dist/instruments/index.d.ts +13 -0
  213. package/dist/instruments/index.js +13 -0
  214. package/dist/instruments/log-viewer.d.ts +74 -0
  215. package/dist/instruments/log-viewer.js +215 -0
  216. package/dist/instruments/meter.d.ts +77 -0
  217. package/dist/instruments/meter.js +96 -0
  218. package/dist/instruments/progress.d.ts +45 -0
  219. package/dist/instruments/progress.js +61 -0
  220. package/dist/instruments/seam-grid.d.ts +66 -0
  221. package/dist/instruments/seam-grid.js +78 -0
  222. package/dist/instruments/sparkline.d.ts +39 -0
  223. package/dist/instruments/sparkline.js +85 -0
  224. package/dist/instruments/stat.d.ts +35 -0
  225. package/dist/instruments/stat.js +59 -0
  226. package/dist/instruments/table.d.ts +51 -0
  227. package/dist/instruments/table.js +92 -0
  228. package/dist/instruments/terminal-frame.d.ts +89 -0
  229. package/dist/instruments/terminal-frame.js +256 -0
  230. package/dist/instruments/two-step-button.d.ts +47 -0
  231. package/dist/instruments/two-step-button.js +147 -0
  232. package/dist/instruments/usage-chart.d.ts +55 -0
  233. package/dist/instruments/usage-chart.js +235 -0
  234. package/dist/layout/_collapse.js +46 -0
  235. package/dist/layout/_glyphs.js +45 -0
  236. package/dist/layout/accordion.d.ts +52 -0
  237. package/dist/layout/accordion.js +81 -0
  238. package/dist/layout/breadcrumb.d.ts +35 -0
  239. package/dist/layout/breadcrumb.js +67 -0
  240. package/dist/layout/card.d.ts +51 -0
  241. package/dist/layout/card.js +76 -0
  242. package/dist/layout/carousel.d.ts +96 -0
  243. package/dist/layout/carousel.js +211 -0
  244. package/dist/layout/collapsible.d.ts +74 -0
  245. package/dist/layout/collapsible.js +126 -0
  246. package/dist/layout/container.d.ts +27 -0
  247. package/dist/layout/container.js +40 -0
  248. package/dist/layout/description-list.d.ts +24 -0
  249. package/dist/layout/description-list.js +37 -0
  250. package/dist/layout/grid.d.ts +40 -0
  251. package/dist/layout/grid.js +48 -0
  252. package/dist/layout/index.d.ts +15 -0
  253. package/dist/layout/index.js +15 -0
  254. package/dist/layout/item.d.ts +65 -0
  255. package/dist/layout/item.js +100 -0
  256. package/dist/layout/link.d.ts +48 -0
  257. package/dist/layout/link.js +63 -0
  258. package/dist/layout/pagination.d.ts +54 -0
  259. package/dist/layout/pagination.js +121 -0
  260. package/dist/layout/resizable.d.ts +38 -0
  261. package/dist/layout/resizable.js +68 -0
  262. package/dist/layout/sidebar.d.ts +67 -0
  263. package/dist/layout/sidebar.js +129 -0
  264. package/dist/layout/stack.d.ts +46 -0
  265. package/dist/layout/stack.js +62 -0
  266. package/dist/layout/stepper.d.ts +49 -0
  267. package/dist/layout/stepper.js +81 -0
  268. package/dist/layout/timeline.d.ts +43 -0
  269. package/dist/layout/timeline.js +63 -0
  270. package/dist/lib/aria-class.js +24 -0
  271. package/dist/lib/cn.js +18 -0
  272. package/dist/lib/compose.js +38 -0
  273. package/dist/lib/cva.d.ts +18 -0
  274. package/dist/lib/cva.js +65 -0
  275. package/dist/midi/_notes.d.ts +101 -0
  276. package/dist/midi/_notes.js +183 -0
  277. package/dist/midi/_ruler.d.ts +100 -0
  278. package/dist/midi/_ruler.js +105 -0
  279. package/dist/midi/_time.d.ts +107 -0
  280. package/dist/midi/_time.js +130 -0
  281. package/dist/midi/index.d.ts +9 -0
  282. package/dist/midi/index.js +9 -0
  283. package/dist/midi/musical-time-input.d.ts +48 -0
  284. package/dist/midi/musical-time-input.js +154 -0
  285. package/dist/midi/piano-keyboard.d.ts +50 -0
  286. package/dist/midi/piano-keyboard.js +193 -0
  287. package/dist/midi/spectrum-visualizer.d.ts +65 -0
  288. package/dist/midi/spectrum-visualizer.js +213 -0
  289. package/dist/midi/threshold-rail.d.ts +72 -0
  290. package/dist/midi/threshold-rail.js +214 -0
  291. package/dist/midi/timeline-ruler.d.ts +83 -0
  292. package/dist/midi/timeline-ruler.js +152 -0
  293. package/dist/overlays/_debounce.js +34 -0
  294. package/dist/overlays/_glyphs.js +59 -0
  295. package/dist/overlays/_surface.d.ts +70 -0
  296. package/dist/overlays/_surface.js +70 -0
  297. package/dist/overlays/_wheel.js +42 -0
  298. package/dist/overlays/alert-dialog.d.ts +67 -0
  299. package/dist/overlays/alert-dialog.js +117 -0
  300. package/dist/overlays/command-palette.d.ts +93 -0
  301. package/dist/overlays/command-palette.js +193 -0
  302. package/dist/overlays/context-menu.d.ts +64 -0
  303. package/dist/overlays/context-menu.js +78 -0
  304. package/dist/overlays/dialog.d.ts +72 -0
  305. package/dist/overlays/dialog.js +124 -0
  306. package/dist/overlays/drawer.d.ts +75 -0
  307. package/dist/overlays/drawer.js +151 -0
  308. package/dist/overlays/dropdown-menu.d.ts +79 -0
  309. package/dist/overlays/dropdown-menu.js +156 -0
  310. package/dist/overlays/hover-card.d.ts +58 -0
  311. package/dist/overlays/hover-card.js +91 -0
  312. package/dist/overlays/index.d.ts +15 -0
  313. package/dist/overlays/index.js +15 -0
  314. package/dist/overlays/info-tip.d.ts +41 -0
  315. package/dist/overlays/info-tip.js +45 -0
  316. package/dist/overlays/menubar.d.ts +72 -0
  317. package/dist/overlays/menubar.js +100 -0
  318. package/dist/overlays/popover.d.ts +66 -0
  319. package/dist/overlays/popover.js +114 -0
  320. package/dist/overlays/sheet.d.ts +78 -0
  321. package/dist/overlays/sheet.js +131 -0
  322. package/dist/overlays/toast.d.ts +106 -0
  323. package/dist/overlays/toast.js +146 -0
  324. package/dist/overlays/tooltip.d.ts +57 -0
  325. package/dist/overlays/tooltip.js +95 -0
  326. package/dist/primitives/avatar.d.ts +51 -0
  327. package/dist/primitives/avatar.js +93 -0
  328. package/dist/primitives/button.d.ts +46 -0
  329. package/dist/primitives/button.js +94 -0
  330. package/dist/primitives/chip.d.ts +47 -0
  331. package/dist/primitives/chip.js +152 -0
  332. package/dist/primitives/icon-button.d.ts +41 -0
  333. package/dist/primitives/icon-button.js +59 -0
  334. package/dist/primitives/index.d.ts +10 -0
  335. package/dist/primitives/index.js +10 -0
  336. package/dist/primitives/kbd.d.ts +17 -0
  337. package/dist/primitives/kbd.js +24 -0
  338. package/dist/primitives/separator.d.ts +25 -0
  339. package/dist/primitives/separator.js +30 -0
  340. package/dist/primitives/skeleton.d.ts +20 -0
  341. package/dist/primitives/skeleton.js +27 -0
  342. package/dist/primitives/spinner.d.ts +26 -0
  343. package/dist/primitives/spinner.js +31 -0
  344. package/dist/primitives/status-dot.d.ts +44 -0
  345. package/dist/primitives/status-dot.js +50 -0
  346. package/dist/styles.css +19 -0
  347. package/dist/system/density.d.ts +33 -0
  348. package/dist/system/density.js +49 -0
  349. package/dist/system/index.d.ts +8 -0
  350. package/dist/system/index.js +7 -0
  351. package/dist/system/portal.d.ts +51 -0
  352. package/dist/system/portal.js +122 -0
  353. package/dist/system/prepaint.d.ts +38 -0
  354. package/dist/system/prepaint.js +37 -0
  355. package/dist/system/theme-provider.d.ts +72 -0
  356. package/dist/system/theme-provider.js +252 -0
  357. package/dist/system/use-density.d.ts +33 -0
  358. package/dist/system/use-density.js +84 -0
  359. package/dist/system/use-isomorphic-layout-effect.js +15 -0
  360. package/dist/system/use-theme.d.ts +19 -0
  361. package/dist/system/use-theme.js +25 -0
  362. package/dist/theming/_presets.d.ts +11 -0
  363. package/dist/theming/_presets.js +487 -0
  364. package/dist/theming/contrast.d.ts +110 -0
  365. package/dist/theming/contrast.js +167 -0
  366. package/dist/theming/create-theme.d.ts +165 -0
  367. package/dist/theming/create-theme.js +442 -0
  368. package/dist/theming/index.d.ts +5 -0
  369. package/dist/theming/index.js +5 -0
  370. package/dist/theming/serialize.d.ts +145 -0
  371. package/dist/theming/serialize.js +165 -0
  372. package/package.json +216 -0
@@ -0,0 +1,122 @@
1
+ "use client";
2
+ import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
3
+ import { DensityContext } from "./density.js";
4
+ import { FontScaleContext, ThemeContext } from "./theme-provider.js";
5
+ import * as React from "react";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/system/portal.tsx
8
+ const DEFAULT_THEME = "cue";
9
+ const DEFAULT_RESOLVED_MODE = "dark";
10
+ /** Custom property the density text ladder multiplies by; see `FontScaleContext`. */
11
+ const FONT_SCALE_PROPERTY = "--cue-font-scale";
12
+ /**
13
+ * Read the stamp from React context — never from the DOM. A portal is mounted
14
+ * outside its owner's DOM subtree, so `getComputedStyle`-style discovery would
15
+ * read the *body's* theme, which is exactly the bug this contract exists to
16
+ * prevent. The same detachment is why `--cue-font-scale` travels here too: the
17
+ * provider publishes it as an inline custom property, which a body-level portal
18
+ * would otherwise never inherit.
19
+ */
20
+ function usePortalStamp() {
21
+ const theme = React.useContext(ThemeContext);
22
+ const island = React.useContext(DensityContext);
23
+ const fontScale = React.useContext(FontScaleContext);
24
+ return {
25
+ theme: theme?.theme ?? DEFAULT_THEME,
26
+ density: island ?? theme?.density ?? "normal",
27
+ mode: theme?.resolvedMode ?? DEFAULT_RESOLVED_MODE,
28
+ fontScale
29
+ };
30
+ }
31
+ /**
32
+ * Props to spread onto a Base UI `Portal` part so the portal root inherits the
33
+ * caller's theme, density and mode.
34
+ *
35
+ * Returns a `container` element appended to `document.body` and stamped with
36
+ * `data-theme` / `data-density` / `data-mode` (plus `--cue-font-scale`) copied
37
+ * from the nearest provider and density island. The container is
38
+ * `display: contents`, so it changes no layout and creates no containing block —
39
+ * fixed-position overlay content behaves exactly as if it were a direct child of
40
+ * `<body>`.
41
+ *
42
+ * Before the container exists (server render, first client render) the object is
43
+ * empty, which leaves the Base UI default (`document.body`) in place.
44
+ *
45
+ * Every overlay in this system spreads this. It is the single fix for the
46
+ * nested-density portal bug (spec §6).
47
+ *
48
+ * @returns `{ container }` once mounted, `{}` otherwise.
49
+ * @example
50
+ * const portalProps = useCuePortalProps();
51
+ * <Popover.Portal {...portalProps}><Popover.Positioner>…</Popover.Positioner></Popover.Portal>
52
+ */
53
+ function useCuePortalProps() {
54
+ const stamp = usePortalStamp();
55
+ const stampRef = React.useRef(stamp);
56
+ stampRef.current = stamp;
57
+ const [container, setContainer] = React.useState(null);
58
+ useIsomorphicLayoutEffect(() => {
59
+ if (typeof document === "undefined") return;
60
+ const element = document.createElement("div");
61
+ element.setAttribute("data-cue-portal", "");
62
+ element.style.display = "contents";
63
+ applyStamp(element, stampRef.current);
64
+ document.body.appendChild(element);
65
+ setContainer(element);
66
+ return () => {
67
+ element.remove();
68
+ setContainer(null);
69
+ };
70
+ }, []);
71
+ useIsomorphicLayoutEffect(() => {
72
+ if (container === null) return;
73
+ applyStamp(container, stamp);
74
+ }, [
75
+ container,
76
+ stamp.theme,
77
+ stamp.density,
78
+ stamp.mode,
79
+ stamp.fontScale
80
+ ]);
81
+ return React.useMemo(() => container === null ? {} : { container }, [container]);
82
+ }
83
+ /** Write a stamp onto a live element (the imperative half of the contract). */
84
+ function applyStamp(element, stamp) {
85
+ element.setAttribute("data-theme", stamp.theme);
86
+ element.setAttribute("data-density", stamp.density);
87
+ element.setAttribute("data-mode", stamp.mode);
88
+ if (stamp.fontScale === null) element.style.removeProperty(FONT_SCALE_PROPERTY);
89
+ else element.style.setProperty(FONT_SCALE_PROPERTY, String(stamp.fontScale));
90
+ }
91
+ /**
92
+ * Inline version of the portal contract, for portals whose container cannot be
93
+ * chosen (a third-party overlay, or content already portaled by something else).
94
+ *
95
+ * Wraps `children` in a `display: contents` element carrying the same
96
+ * `data-theme` / `data-density` / `data-mode` triple and `--cue-font-scale` as
97
+ * {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
98
+ * element per overlay instead of one per render tree — and reach for this only
99
+ * when the container prop is not available.
100
+ *
101
+ * @example
102
+ * <SomeThirdPartyPortal><CuePortalFrame>{content}</CuePortalFrame></SomeThirdPartyPortal>
103
+ */
104
+ function CuePortalFrame({ children, className, style }) {
105
+ const stamp = usePortalStamp();
106
+ const frameStyle = {
107
+ display: "contents",
108
+ ...stamp.fontScale === null ? null : { [FONT_SCALE_PROPERTY]: stamp.fontScale },
109
+ ...style
110
+ };
111
+ return /* @__PURE__ */ jsx("div", {
112
+ "data-cue-portal": "",
113
+ "data-theme": stamp.theme,
114
+ "data-density": stamp.density,
115
+ "data-mode": stamp.mode,
116
+ className,
117
+ style: frameStyle,
118
+ children
119
+ });
120
+ }
121
+ //#endregion
122
+ export { CuePortalFrame, useCuePortalProps };
@@ -0,0 +1,38 @@
1
+ import { ThemeName } from "@cueplusplus/tokens";
2
+ //#region src/system/prepaint.d.ts
3
+ /** localStorage key the ThemeProvider and the pre-paint script share by default. */
4
+ declare const DEFAULT_STORAGE_KEY = "cue-ui";
5
+ /** Shape persisted under {@link DEFAULT_STORAGE_KEY} — kept in sync with the provider's writer. */
6
+ interface PersistedPreferences {
7
+ /** Last theme the user picked. */
8
+ theme?: string;
9
+ /** Last density level the user picked. */
10
+ density?: string;
11
+ /** Last mode the user picked, including the literal `"system"`. */
12
+ mode?: string;
13
+ }
14
+ /**
15
+ * Build the blocking inline script that stamps `data-theme`, `data-density`,
16
+ * `data-mode` and `color-scheme` on `<html>` from localStorage before the first
17
+ * paint.
18
+ *
19
+ * Render it as `<script dangerouslySetInnerHTML={{ __html: prepaintScript() }} />`
20
+ * in `<head>`, above everything else. The output never contains `</script>` and
21
+ * never throws: private-mode localStorage failures degrade to the dark-first
22
+ * defaults (`cue` / `normal` / `dark`), which are also what bare `:root` in
23
+ * `@cueplusplus/tokens/theme.css` already paints.
24
+ *
25
+ * @param storageKey - localStorage key to read. Must match the `storageKey`
26
+ * passed to `<ThemeProvider>`. Defaults to {@link DEFAULT_STORAGE_KEY}.
27
+ * @param defaultTheme - Theme to stamp for a visitor with nothing persisted yet.
28
+ * Must match the `theme` passed to `<ThemeProvider>`, or the first paint
29
+ * dresses `<html>` in one preset while the app renders in another. Defaults to
30
+ * `"cue"`.
31
+ * @returns One line of JavaScript, safe to inline verbatim.
32
+ * @example
33
+ * // An app whose provider is <ThemeProvider theme="terminal">:
34
+ * prepaintScript(DEFAULT_STORAGE_KEY, "terminal");
35
+ */
36
+ declare function prepaintScript(storageKey?: string, defaultTheme?: ThemeName): string;
37
+ //#endregion
38
+ export { DEFAULT_STORAGE_KEY, PersistedPreferences, prepaintScript };
@@ -0,0 +1,37 @@
1
+ //#region src/system/prepaint.ts
2
+ /** localStorage key the ThemeProvider and the pre-paint script share by default. */
3
+ const DEFAULT_STORAGE_KEY = "cue-ui";
4
+ /**
5
+ * Embed a string in JavaScript source that will itself be embedded in HTML.
6
+ * `<` becomes `\u003c` so no value can close the host `<script>` element.
7
+ */
8
+ function jsStringLiteral(value) {
9
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
10
+ }
11
+ /**
12
+ * Build the blocking inline script that stamps `data-theme`, `data-density`,
13
+ * `data-mode` and `color-scheme` on `<html>` from localStorage before the first
14
+ * paint.
15
+ *
16
+ * Render it as `<script dangerouslySetInnerHTML={{ __html: prepaintScript() }} />`
17
+ * in `<head>`, above everything else. The output never contains `<\/script>` and
18
+ * never throws: private-mode localStorage failures degrade to the dark-first
19
+ * defaults (`cue` / `normal` / `dark`), which are also what bare `:root` in
20
+ * `@cueplusplus/tokens/theme.css` already paints.
21
+ *
22
+ * @param storageKey - localStorage key to read. Must match the `storageKey`
23
+ * passed to `<ThemeProvider>`. Defaults to {@link DEFAULT_STORAGE_KEY}.
24
+ * @param defaultTheme - Theme to stamp for a visitor with nothing persisted yet.
25
+ * Must match the `theme` passed to `<ThemeProvider>`, or the first paint
26
+ * dresses `<html>` in one preset while the app renders in another. Defaults to
27
+ * `"cue"`.
28
+ * @returns One line of JavaScript, safe to inline verbatim.
29
+ * @example
30
+ * // An app whose provider is <ThemeProvider theme="terminal">:
31
+ * prepaintScript(DEFAULT_STORAGE_KEY, "terminal");
32
+ */
33
+ function prepaintScript(storageKey = DEFAULT_STORAGE_KEY, defaultTheme = "cue") {
34
+ return `!function(){try{var k=${jsStringLiteral(storageKey)},e=document.documentElement,s=null;try{var r=localStorage.getItem(k);s=r?JSON.parse(r):null}catch(_){}s=s&&typeof s=="object"?s:{};var t=typeof s.theme=="string"&&s.theme?s.theme:${jsStringLiteral(defaultTheme)};var d=["ultra-compact","normal","large"].indexOf(s.density)>-1?s.density:"normal";var m=["dark","light","system"].indexOf(s.mode)>-1?s.mode:"dark";if(m==="system")m=typeof matchMedia=="function"&&matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";e.setAttribute("data-theme",t);e.setAttribute("data-density",d);e.setAttribute("data-mode",m);e.style.colorScheme=m}catch(_){}}()`;
35
+ }
36
+ //#endregion
37
+ export { DEFAULT_STORAGE_KEY, prepaintScript };
@@ -0,0 +1,72 @@
1
+ import * as React from "react";
2
+ import { Density, Mode, ThemeName } from "@cueplusplus/tokens";
3
+ //#region src/system/theme-provider.d.ts
4
+ /** A mode that has been resolved to an actual palette — `"system"` never survives this far. */
5
+ type ResolvedMode = "dark" | "light";
6
+ /** What `useTheme()` returns. */
7
+ interface ThemeContextValue {
8
+ /** Active theme preset. */
9
+ theme: ThemeName;
10
+ /** Density the *provider* holds — a nested `<Density>` island does not change it. */
11
+ density: Density;
12
+ /** Mode as chosen, including the literal `"system"`. */
13
+ mode: Mode;
14
+ /** Mode after resolving `"system"` against `prefers-color-scheme`. */
15
+ resolvedMode: ResolvedMode;
16
+ /** Switch theme and persist the new preference triple. */
17
+ setTheme: (theme: ThemeName) => void;
18
+ /** Switch the provider-level density and persist the new preference triple. */
19
+ setDensity: (density: Density) => void;
20
+ /** Switch mode (`"system"` included) and persist the new preference triple. */
21
+ setMode: (mode: Mode) => void;
22
+ }
23
+ interface ThemeProviderProps {
24
+ /** Initial theme preset. Defaults to `"cue"`. Changing it after mount adopts the new value. */
25
+ theme?: ThemeName;
26
+ /** Initial density level. Defaults to `"normal"`. */
27
+ density?: Density;
28
+ /** Initial mode. Defaults to `"dark"`; `"system"` tracks `prefers-color-scheme`. */
29
+ mode?: Mode;
30
+ /** Multiplier on every text token, published as `--cue-font-scale`. Defaults to `1`. */
31
+ fontScale?: number;
32
+ /**
33
+ * localStorage key holding `{ theme, density, mode }`. Defaults to `"cue-ui"`.
34
+ * Only the outermost provider reads or writes it — a nested provider is an
35
+ * island whose explicit props are the point, not a second preference store.
36
+ */
37
+ storageKey?: string;
38
+ /** Stamp the single child element instead of rendering a wrapper `<div>`. */
39
+ asChild?: boolean;
40
+ /** Extra classes for the provider root. */
41
+ className?: string;
42
+ /** Inline styles for the provider root, merged after the token style. */
43
+ style?: React.CSSProperties;
44
+ /** The application subtree. */
45
+ children?: React.ReactNode;
46
+ }
47
+ /**
48
+ * Root of the theme system: stamps `data-theme`, `data-density` and `data-mode`
49
+ * so the token layer resolves, publishes `--cue-font-scale`, and owns the
50
+ * persisted user preference.
51
+ *
52
+ * Rendering: `<div data-cue-root data-theme data-density data-mode style={{ colorScheme, --cue-font-scale }}>`
53
+ * (or the single child when `asChild`). The `theme`/`density`/`mode` props are
54
+ * *initial* values — the provider holds the state so `setTheme` and friends can
55
+ * drive it — but a changed prop is adopted after mount, so a controlling parent
56
+ * still works.
57
+ *
58
+ * Persistence and the pre-paint contract: setters write
59
+ * `{ theme, density, mode }` to `localStorage[storageKey]`, which is exactly what
60
+ * {@link prepaintScript} reads to stamp `<html>` before the first paint. The
61
+ * outermost provider keeps `<html>` in sync while the app runs (and restores the
62
+ * previous stamp on unmount) so the page ground, UA scrollbars and form controls
63
+ * follow the theme; nested providers never touch `<html>`.
64
+ *
65
+ * @example
66
+ * <ThemeProvider theme="terminal" density="ultra-compact" mode="system">
67
+ * <App />
68
+ * </ThemeProvider>
69
+ */
70
+ declare function ThemeProvider({ theme: themeProp, density: densityProp, mode: modeProp, fontScale, storageKey, asChild, className, style: styleProp, children }: ThemeProviderProps): React.JSX.Element;
71
+ //#endregion
72
+ export { ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps };
@@ -0,0 +1,252 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
4
+ import { DENSITY_LEVELS, DensityContext } from "./density.js";
5
+ import { DEFAULT_STORAGE_KEY } from "./prepaint.js";
6
+ import * as React from "react";
7
+ import { jsx } from "react/jsx-runtime";
8
+ //#region src/system/theme-provider.tsx
9
+ const MODES = [
10
+ "dark",
11
+ "light",
12
+ "system"
13
+ ];
14
+ /** Dark-first portfolio defaults; the same triple bare `:root` in `theme.css` paints. */
15
+ const DEFAULT_THEME = "cue";
16
+ const DEFAULT_DENSITY = "normal";
17
+ const DEFAULT_MODE = "dark";
18
+ const DEFAULT_RESOLVED_MODE = "dark";
19
+ /**
20
+ * Ambient theme state. `null` outside a provider, which is how `useTheme()`
21
+ * knows to throw and how the portal helpers know to use the defaults.
22
+ *
23
+ * Internal: consumers read it through `useTheme()`.
24
+ */
25
+ const ThemeContext = React.createContext(null);
26
+ /**
27
+ * The nearest provider's `fontScale`, published on its own so portal roots can
28
+ * republish it. A portal mounts outside the provider's DOM subtree and so
29
+ * inherits none of its custom properties — including `--cue-font-scale`, which
30
+ * every text token multiplies by. `null` means "no provider above", in which
31
+ * case a portal leaves whatever the document already declares alone.
32
+ *
33
+ * Internal: not part of the public API.
34
+ */
35
+ const FontScaleContext = React.createContext(null);
36
+ function isOneOf(allowed, value) {
37
+ return typeof value === "string" && allowed.includes(value);
38
+ }
39
+ function readPreferences(storageKey) {
40
+ if (typeof localStorage === "undefined") return {};
41
+ try {
42
+ const raw = localStorage.getItem(storageKey);
43
+ if (raw === null) return {};
44
+ const parsed = JSON.parse(raw);
45
+ if (parsed === null || typeof parsed !== "object") return {};
46
+ return parsed;
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+ function writePreferences(storageKey, preferences) {
52
+ if (typeof localStorage === "undefined") return;
53
+ try {
54
+ localStorage.setItem(storageKey, JSON.stringify(preferences));
55
+ } catch {}
56
+ }
57
+ /**
58
+ * Root of the theme system: stamps `data-theme`, `data-density` and `data-mode`
59
+ * so the token layer resolves, publishes `--cue-font-scale`, and owns the
60
+ * persisted user preference.
61
+ *
62
+ * Rendering: `<div data-cue-root data-theme data-density data-mode style={{ colorScheme, --cue-font-scale }}>`
63
+ * (or the single child when `asChild`). The `theme`/`density`/`mode` props are
64
+ * *initial* values — the provider holds the state so `setTheme` and friends can
65
+ * drive it — but a changed prop is adopted after mount, so a controlling parent
66
+ * still works.
67
+ *
68
+ * Persistence and the pre-paint contract: setters write
69
+ * `{ theme, density, mode }` to `localStorage[storageKey]`, which is exactly what
70
+ * {@link prepaintScript} reads to stamp `<html>` before the first paint. The
71
+ * outermost provider keeps `<html>` in sync while the app runs (and restores the
72
+ * previous stamp on unmount) so the page ground, UA scrollbars and form controls
73
+ * follow the theme; nested providers never touch `<html>`.
74
+ *
75
+ * @example
76
+ * <ThemeProvider theme="terminal" density="ultra-compact" mode="system">
77
+ * <App />
78
+ * </ThemeProvider>
79
+ */
80
+ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp = DEFAULT_DENSITY, mode: modeProp = DEFAULT_MODE, fontScale = 1, storageKey = DEFAULT_STORAGE_KEY, asChild = false, className, style: styleProp, children }) {
81
+ const isRoot = React.useContext(ThemeContext) === null;
82
+ const [theme, setThemeState] = React.useState(themeProp);
83
+ const [density, setDensityState] = React.useState(densityProp);
84
+ const [mode, setModeState] = React.useState(modeProp);
85
+ const [systemMode, setSystemMode] = React.useState(DEFAULT_RESOLVED_MODE);
86
+ const resolvedMode = mode === "system" ? systemMode : mode;
87
+ const restoredKey = React.useRef(null);
88
+ useIsomorphicLayoutEffect(() => {
89
+ if (!isRoot || restoredKey.current === storageKey) return;
90
+ restoredKey.current = storageKey;
91
+ const stored = readPreferences(storageKey);
92
+ if (typeof stored.theme === "string" && stored.theme.length > 0) setThemeState(stored.theme);
93
+ if (isOneOf(DENSITY_LEVELS, stored.density)) setDensityState(stored.density);
94
+ if (isOneOf(MODES, stored.mode)) setModeState(stored.mode);
95
+ }, [isRoot, storageKey]);
96
+ const lastProps = React.useRef({
97
+ theme: themeProp,
98
+ density: densityProp,
99
+ mode: modeProp
100
+ });
101
+ React.useEffect(() => {
102
+ const previous = lastProps.current;
103
+ lastProps.current = {
104
+ theme: themeProp,
105
+ density: densityProp,
106
+ mode: modeProp
107
+ };
108
+ if (previous.theme !== themeProp) setThemeState(themeProp);
109
+ if (previous.density !== densityProp) setDensityState(densityProp);
110
+ if (previous.mode !== modeProp) setModeState(modeProp);
111
+ }, [
112
+ themeProp,
113
+ densityProp,
114
+ modeProp
115
+ ]);
116
+ useIsomorphicLayoutEffect(() => {
117
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
118
+ const query = window.matchMedia("(prefers-color-scheme: light)");
119
+ const sync = () => setSystemMode(query.matches ? "light" : "dark");
120
+ sync();
121
+ query.addEventListener("change", sync);
122
+ return () => query.removeEventListener("change", sync);
123
+ }, []);
124
+ const stateRef = React.useRef({
125
+ theme,
126
+ density,
127
+ mode
128
+ });
129
+ useIsomorphicLayoutEffect(() => {
130
+ stateRef.current = {
131
+ theme,
132
+ density,
133
+ mode
134
+ };
135
+ });
136
+ const persist = React.useCallback((patch) => {
137
+ const next = {
138
+ ...stateRef.current,
139
+ ...patch
140
+ };
141
+ stateRef.current = next;
142
+ if (!isRoot) return;
143
+ writePreferences(storageKey, next);
144
+ }, [isRoot, storageKey]);
145
+ const setTheme = React.useCallback((next) => {
146
+ setThemeState(next);
147
+ persist({ theme: next });
148
+ }, [persist]);
149
+ const setDensity = React.useCallback((next) => {
150
+ setDensityState(next);
151
+ persist({ density: next });
152
+ }, [persist]);
153
+ const setMode = React.useCallback((next) => {
154
+ setModeState(next);
155
+ persist({ mode: next });
156
+ }, [persist]);
157
+ const originalStamp = React.useRef(null);
158
+ useIsomorphicLayoutEffect(() => {
159
+ if (!isRoot || typeof document === "undefined") return;
160
+ const root = document.documentElement;
161
+ originalStamp.current ??= {
162
+ theme: root.getAttribute("data-theme"),
163
+ density: root.getAttribute("data-density"),
164
+ mode: root.getAttribute("data-mode"),
165
+ colorScheme: root.style.colorScheme
166
+ };
167
+ root.setAttribute("data-theme", theme);
168
+ root.setAttribute("data-density", density);
169
+ root.setAttribute("data-mode", resolvedMode);
170
+ root.style.colorScheme = resolvedMode;
171
+ }, [
172
+ isRoot,
173
+ theme,
174
+ density,
175
+ resolvedMode
176
+ ]);
177
+ React.useEffect(() => () => {
178
+ const original = originalStamp.current;
179
+ if (original === null || typeof document === "undefined") return;
180
+ originalStamp.current = null;
181
+ const root = document.documentElement;
182
+ const restore = (name, value) => {
183
+ if (value === null) root.removeAttribute(name);
184
+ else root.setAttribute(name, value);
185
+ };
186
+ restore("data-theme", original.theme);
187
+ restore("data-density", original.density);
188
+ restore("data-mode", original.mode);
189
+ root.style.colorScheme = original.colorScheme;
190
+ }, []);
191
+ const contextValue = React.useMemo(() => ({
192
+ theme,
193
+ density,
194
+ mode,
195
+ resolvedMode,
196
+ setTheme,
197
+ setDensity,
198
+ setMode
199
+ }), [
200
+ theme,
201
+ density,
202
+ mode,
203
+ resolvedMode,
204
+ setTheme,
205
+ setDensity,
206
+ setMode
207
+ ]);
208
+ const style = React.useMemo(() => ({
209
+ colorScheme: resolvedMode,
210
+ "--cue-font-scale": fontScale,
211
+ ...styleProp
212
+ }), [
213
+ resolvedMode,
214
+ fontScale,
215
+ styleProp
216
+ ]);
217
+ const stamp = {
218
+ "data-cue-root": "",
219
+ "data-theme": theme,
220
+ "data-density": density,
221
+ "data-mode": resolvedMode
222
+ };
223
+ let root;
224
+ if (asChild) {
225
+ const child = React.Children.only(children);
226
+ root = React.cloneElement(child, {
227
+ ...stamp,
228
+ className: cn(className, child.props.className),
229
+ style: {
230
+ ...style,
231
+ ...child.props.style
232
+ }
233
+ });
234
+ } else root = /* @__PURE__ */ jsx("div", {
235
+ ...stamp,
236
+ className,
237
+ style,
238
+ children
239
+ });
240
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, {
241
+ value: contextValue,
242
+ children: /* @__PURE__ */ jsx(FontScaleContext.Provider, {
243
+ value: fontScale,
244
+ children: /* @__PURE__ */ jsx(DensityContext.Provider, {
245
+ value: density,
246
+ children: root
247
+ })
248
+ })
249
+ });
250
+ }
251
+ //#endregion
252
+ export { FontScaleContext, ThemeContext, ThemeProvider };
@@ -0,0 +1,33 @@
1
+ import { Density } from "@cueplusplus/tokens";
2
+ //#region src/system/use-density.d.ts
3
+ /** Rung of the control ladder to measure. */
4
+ type ControlSize = "sm" | "md" | "lg";
5
+ /**
6
+ * The density level that actually applies here: the nearest `<Density>` island,
7
+ * else the `<ThemeProvider>` level, else `"normal"`.
8
+ *
9
+ * This is a JS read for code that cannot use CSS (virtualised row heights,
10
+ * canvas drawing, layout math). Styling should use the density tokens or the
11
+ * `density-ultra:` / `density-large:` variants instead.
12
+ *
13
+ * @returns The applicable density level.
14
+ */
15
+ declare function useDensity(): Density;
16
+ /**
17
+ * The pixel height of a control at the ambient density — what a virtualiser or a
18
+ * canvas renderer needs when it cannot ask CSS.
19
+ *
20
+ * Measured from a throwaway `document.body` probe stamped with the ambient
21
+ * density, so app-wide token overrides and the `@media (pointer: coarse)`
22
+ * re-raise of `ultra-compact` are honoured (overrides scoped to a subtree the
23
+ * probe is not in are not). Falls back to the authored ladder × 16px during SSR
24
+ * and before the first layout effect, which keeps hydration stable.
25
+ *
26
+ * @param size - Which rung to measure. Defaults to `"md"`.
27
+ * @returns Height in CSS pixels.
28
+ * @example
29
+ * const rowHeight = useControlHeight("sm"); // → 20 at normal density
30
+ */
31
+ declare function useControlHeight(size?: ControlSize): number;
32
+ //#endregion
33
+ export { ControlSize, useControlHeight, useDensity };
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
3
+ import { DensityContext } from "./density.js";
4
+ import * as React from "react";
5
+ //#region src/system/use-density.ts
6
+ /**
7
+ * The control ladder in rem, mirroring `packages/tokens/src/density/*.tokens.json`.
8
+ * Only a fallback: {@link useControlHeight} measures the live CSS first, so
9
+ * per-app token overrides and the `@media (pointer: coarse)` re-raise win.
10
+ */
11
+ const CONTROL_REM = {
12
+ "ultra-compact": {
13
+ sm: 1.125,
14
+ md: 1.25,
15
+ lg: 1.5
16
+ },
17
+ normal: {
18
+ sm: 1.25,
19
+ md: 1.5,
20
+ lg: 1.75
21
+ },
22
+ large: {
23
+ sm: 1.75,
24
+ md: 2,
25
+ lg: 2.25
26
+ }
27
+ };
28
+ /** Root font size assumed before the first measurement, so SSR and hydration agree. */
29
+ const ASSUMED_ROOT_FONT_SIZE = 16;
30
+ function rootFontSize() {
31
+ if (typeof document === "undefined") return ASSUMED_ROOT_FONT_SIZE;
32
+ const size = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
33
+ return Number.isFinite(size) && size > 0 ? size : ASSUMED_ROOT_FONT_SIZE;
34
+ }
35
+ function measureControlHeight(density, size) {
36
+ const fallback = CONTROL_REM[density][size] * rootFontSize();
37
+ if (typeof document === "undefined" || document.body === null) return fallback;
38
+ const probe = document.createElement("div");
39
+ probe.setAttribute("data-density", density);
40
+ probe.setAttribute("aria-hidden", "true");
41
+ probe.style.cssText = `position:absolute;top:-9999px;left:-9999px;width:0;visibility:hidden;pointer-events:none;height:var(--cue-control-${size})`;
42
+ document.body.appendChild(probe);
43
+ const measured = probe.getBoundingClientRect().height;
44
+ probe.remove();
45
+ return measured > 0 ? measured : fallback;
46
+ }
47
+ /**
48
+ * The density level that actually applies here: the nearest `<Density>` island,
49
+ * else the `<ThemeProvider>` level, else `"normal"`.
50
+ *
51
+ * This is a JS read for code that cannot use CSS (virtualised row heights,
52
+ * canvas drawing, layout math). Styling should use the density tokens or the
53
+ * `density-ultra:` / `density-large:` variants instead.
54
+ *
55
+ * @returns The applicable density level.
56
+ */
57
+ function useDensity() {
58
+ return React.useContext(DensityContext) ?? "normal";
59
+ }
60
+ /**
61
+ * The pixel height of a control at the ambient density — what a virtualiser or a
62
+ * canvas renderer needs when it cannot ask CSS.
63
+ *
64
+ * Measured from a throwaway `document.body` probe stamped with the ambient
65
+ * density, so app-wide token overrides and the `@media (pointer: coarse)`
66
+ * re-raise of `ultra-compact` are honoured (overrides scoped to a subtree the
67
+ * probe is not in are not). Falls back to the authored ladder × 16px during SSR
68
+ * and before the first layout effect, which keeps hydration stable.
69
+ *
70
+ * @param size - Which rung to measure. Defaults to `"md"`.
71
+ * @returns Height in CSS pixels.
72
+ * @example
73
+ * const rowHeight = useControlHeight("sm"); // → 20 at normal density
74
+ */
75
+ function useControlHeight(size = "md") {
76
+ const density = useDensity();
77
+ const [height, setHeight] = React.useState(() => CONTROL_REM[density][size] * ASSUMED_ROOT_FONT_SIZE);
78
+ useIsomorphicLayoutEffect(() => {
79
+ setHeight(measureControlHeight(density, size));
80
+ }, [density, size]);
81
+ return height;
82
+ }
83
+ //#endregion
84
+ export { useControlHeight, useDensity };
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ //#region src/system/use-isomorphic-layout-effect.ts
4
+ /**
5
+ * `useLayoutEffect` in the browser, `useEffect` on the server.
6
+ *
7
+ * The theme system stamps DOM attributes and creates portal containers in
8
+ * layout effects so they land *before* the browser paints — a density flash
9
+ * reflows the whole page, which is worse than a colour flash (spec §6). React
10
+ * warns when `useLayoutEffect` runs during SSR, so the server gets the
11
+ * (never-invoked) passive variant instead.
12
+ */
13
+ const useIsomorphicLayoutEffect = typeof document === "undefined" ? React.useEffect : React.useLayoutEffect;
14
+ //#endregion
15
+ export { useIsomorphicLayoutEffect };
@@ -0,0 +1,19 @@
1
+ import { ThemeContextValue } from "./theme-provider.js";
2
+ //#region src/system/use-theme.d.ts
3
+ /**
4
+ * Read and drive the ambient theme state.
5
+ *
6
+ * Returns `{ theme, density, mode, resolvedMode, setTheme, setDensity, setMode }`.
7
+ * `density` here is the *provider's* level — a nested `<Density>` island does not
8
+ * change it; use `useDensity()` when you want the level that actually applies
9
+ * where you are.
10
+ *
11
+ * @throws If called outside a `<ThemeProvider>`. Theme state is never guessed:
12
+ * a missing provider is a wiring bug, not a fallback situation.
13
+ * @example
14
+ * const { resolvedMode, setMode } = useTheme();
15
+ * <Switch checked={resolvedMode === "light"} onCheckedChange={(on) => setMode(on ? "light" : "dark")} />
16
+ */
17
+ declare function useTheme(): ThemeContextValue;
18
+ //#endregion
19
+ export { useTheme };