@cueplusplus/ui 0.1.0 → 0.3.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 (246) hide show
  1. package/CHANGELOG.md +705 -0
  2. package/dist/brand/_chassis.d.ts +54 -0
  3. package/dist/brand/_chassis.js +81 -0
  4. package/dist/brand/_geometry.js +41 -0
  5. package/dist/brand/cue-logotype.d.ts +29 -0
  6. package/dist/brand/cue-logotype.js +60 -0
  7. package/dist/brand/cue-mark.d.ts +30 -0
  8. package/dist/brand/cue-mark.js +61 -0
  9. package/dist/brand/index.d.ts +5 -0
  10. package/dist/brand/plussie.d.ts +67 -0
  11. package/dist/brand/plussie.js +125 -0
  12. package/dist/chat/agent-pile.js +1 -1
  13. package/dist/chat/ask-box.js +2 -2
  14. package/dist/chat/delegation-card.js +1 -1
  15. package/dist/chat/message.js +2 -2
  16. package/dist/chrome/_edge-scroller.d.ts +45 -0
  17. package/dist/chrome/_edge-scroller.js +149 -0
  18. package/dist/chrome/_glyphs.js +19 -1
  19. package/dist/chrome/_tabs-scroll.d.ts +13 -8
  20. package/dist/chrome/_tabs-scroll.js +13 -8
  21. package/dist/chrome/app-bar.d.ts +103 -0
  22. package/dist/chrome/app-bar.js +175 -0
  23. package/dist/chrome/app-shell.d.ts +125 -0
  24. package/dist/chrome/app-shell.js +167 -0
  25. package/dist/chrome/footer.d.ts +75 -0
  26. package/dist/chrome/footer.js +68 -0
  27. package/dist/chrome/index.d.ts +5 -1
  28. package/dist/chrome/index.js +4 -1
  29. package/dist/chrome/navigation-menu.js +1 -1
  30. package/dist/chrome/page-shell.d.ts +1 -1
  31. package/dist/chrome/page-shell.js +15 -5
  32. package/dist/chrome/segmented-control.d.ts +19 -0
  33. package/dist/chrome/segmented-control.js +40 -21
  34. package/dist/chrome/tabs.js +21 -65
  35. package/dist/chrome/toolbar.d.ts +22 -0
  36. package/dist/chrome/toolbar.js +26 -5
  37. package/dist/configurator/_overrides.js +22 -8
  38. package/dist/configurator/configurator.js +1 -1
  39. package/dist/configurator/panel-sections.js +10 -4
  40. package/dist/dmx/channel-matrix.js +1 -1
  41. package/dist/forms/otp-field.js +1 -1
  42. package/dist/index.d.ts +18 -9
  43. package/dist/index.js +16 -10
  44. package/dist/instruments/data-table.js +1 -1
  45. package/dist/instruments/index.d.ts +2 -2
  46. package/dist/instruments/index.js +2 -2
  47. package/dist/instruments/table.d.ts +14 -1
  48. package/dist/instruments/table.js +20 -1
  49. package/dist/layout/carousel.js +1 -1
  50. package/dist/layout/container.d.ts +1 -1
  51. package/dist/layout/container.js +6 -3
  52. package/dist/layout/index.d.ts +3 -3
  53. package/dist/layout/index.js +3 -3
  54. package/dist/layout/pagination.js +1 -1
  55. package/dist/layout/sidebar.d.ts +23 -3
  56. package/dist/layout/sidebar.js +18 -7
  57. package/dist/midi/spectrum-visualizer.js +1 -1
  58. package/dist/midi/timeline-ruler.js +1 -1
  59. package/dist/overlays/command-palette.js +1 -1
  60. package/dist/overlays/dialog.js +1 -1
  61. package/dist/overlays/dropdown-menu.js +2 -2
  62. package/dist/overlays/hover-card.js +1 -1
  63. package/dist/overlays/index.js +1 -1
  64. package/dist/overlays/popover.js +1 -1
  65. package/dist/overlays/sheet.js +1 -1
  66. package/dist/overlays/toast.js +1 -1
  67. package/dist/primitives/chip.d.ts +1 -1
  68. package/dist/primitives/index.d.ts +1 -1
  69. package/dist/primitives/index.js +1 -1
  70. package/dist/primitives/status-dot.d.ts +7 -0
  71. package/dist/primitives/status-dot.js +8 -1
  72. package/dist/styles.css +58 -2
  73. package/dist/system/density.d.ts +4 -1
  74. package/dist/system/density.js +12 -4
  75. package/dist/system/index.d.ts +4 -4
  76. package/dist/system/portal.d.ts +5 -5
  77. package/dist/system/portal.js +30 -11
  78. package/dist/system/prepaint.d.ts +34 -9
  79. package/dist/system/prepaint.js +48 -25
  80. package/dist/system/theme-provider.d.ts +48 -11
  81. package/dist/system/theme-provider.js +109 -35
  82. package/dist/system/use-density.d.ts +3 -3
  83. package/dist/system/use-density.js +16 -6
  84. package/dist/system/use-theme.d.ts +4 -2
  85. package/dist/system/use-theme.js +4 -2
  86. package/dist/theming/_presets.js +133 -5
  87. package/dist/theming/create-theme.d.ts +2 -2
  88. package/dist/theming/create-theme.js +37 -19
  89. package/dist/theming/serialize.d.ts +1 -1
  90. package/dist/theming/serialize.js +25 -1
  91. package/manifest/components/accordion.json +154 -0
  92. package/manifest/components/agent-pile.json +116 -0
  93. package/manifest/components/alert-dialog.json +190 -0
  94. package/manifest/components/app-bar.json +244 -0
  95. package/manifest/components/app-shell.json +291 -0
  96. package/manifest/components/app-window-frame.json +102 -0
  97. package/manifest/components/ask-box.json +209 -0
  98. package/manifest/components/autocomplete.json +197 -0
  99. package/manifest/components/avatar-group.json +72 -0
  100. package/manifest/components/avatar.json +95 -0
  101. package/manifest/components/breadcrumb.json +74 -0
  102. package/manifest/components/button-group.json +74 -0
  103. package/manifest/components/button.json +125 -0
  104. package/manifest/components/calendar.json +65 -0
  105. package/manifest/components/card.json +140 -0
  106. package/manifest/components/carousel.json +200 -0
  107. package/manifest/components/channel-matrix.json +173 -0
  108. package/manifest/components/chart-container.json +111 -0
  109. package/manifest/components/chart-ramp.json +72 -0
  110. package/manifest/components/chart-swatch.json +65 -0
  111. package/manifest/components/chart-tooltip-content.json +114 -0
  112. package/manifest/components/checkbox-group.json +68 -0
  113. package/manifest/components/checkbox.json +93 -0
  114. package/manifest/components/chip.json +132 -0
  115. package/manifest/components/collapsible.json +120 -0
  116. package/manifest/components/color-area.json +59 -0
  117. package/manifest/components/color-field.json +79 -0
  118. package/manifest/components/color-picker.json +175 -0
  119. package/manifest/components/color-slider.json +75 -0
  120. package/manifest/components/color-swatch.json +68 -0
  121. package/manifest/components/colors-section.json +66 -0
  122. package/manifest/components/combobox.json +198 -0
  123. package/manifest/components/command-palette.json +177 -0
  124. package/manifest/components/composer.json +176 -0
  125. package/manifest/components/container.json +60 -0
  126. package/manifest/components/context-menu.json +121 -0
  127. package/manifest/components/copy-button.json +93 -0
  128. package/manifest/components/cue-logotype.json +96 -0
  129. package/manifest/components/cue-mark.json +97 -0
  130. package/manifest/components/cue-portal-frame.json +55 -0
  131. package/manifest/components/data-table-pagination.json +83 -0
  132. package/manifest/components/data-table-toolbar.json +104 -0
  133. package/manifest/components/date-field.json +80 -0
  134. package/manifest/components/date-picker.json +191 -0
  135. package/manifest/components/date-range-picker.json +191 -0
  136. package/manifest/components/delegation-card.json +144 -0
  137. package/manifest/components/density.json +58 -0
  138. package/manifest/components/description-list.json +84 -0
  139. package/manifest/components/dialog.json +198 -0
  140. package/manifest/components/disclosure.json +109 -0
  141. package/manifest/components/dmx-bar.json +94 -0
  142. package/manifest/components/dmx-strip.json +101 -0
  143. package/manifest/components/drawer.json +179 -0
  144. package/manifest/components/dropdown-menu.json +229 -0
  145. package/manifest/components/empty-state.json +90 -0
  146. package/manifest/components/env-var-input.json +150 -0
  147. package/manifest/components/export-dialog.json +84 -0
  148. package/manifest/components/eyebrow.json +56 -0
  149. package/manifest/components/field-description.json +63 -0
  150. package/manifest/components/field-error.json +77 -0
  151. package/manifest/components/field-label.json +63 -0
  152. package/manifest/components/field.json +79 -0
  153. package/manifest/components/file-upload.json +159 -0
  154. package/manifest/components/flow-background.json +50 -0
  155. package/manifest/components/flow-controls.json +50 -0
  156. package/manifest/components/footer.json +112 -0
  157. package/manifest/components/grid.json +81 -0
  158. package/manifest/components/group-bar.json +102 -0
  159. package/manifest/components/hover-card.json +151 -0
  160. package/manifest/components/icon-button.json +88 -0
  161. package/manifest/components/info-tip.json +98 -0
  162. package/manifest/components/input-group.json +91 -0
  163. package/manifest/components/input.json +89 -0
  164. package/manifest/components/item.json +129 -0
  165. package/manifest/components/kbd.json +52 -0
  166. package/manifest/components/link.json +81 -0
  167. package/manifest/components/log-viewer.json +139 -0
  168. package/manifest/components/menubar.json +157 -0
  169. package/manifest/components/message-list.json +100 -0
  170. package/manifest/components/message.json +116 -0
  171. package/manifest/components/meter.json +114 -0
  172. package/manifest/components/multi-select.json +190 -0
  173. package/manifest/components/musical-time-input.json +113 -0
  174. package/manifest/components/navigation-menu.json +226 -0
  175. package/manifest/components/node-card.json +109 -0
  176. package/manifest/components/node-handle.json +69 -0
  177. package/manifest/components/number-field.json +107 -0
  178. package/manifest/components/otp-field.json +128 -0
  179. package/manifest/components/page-shell.json +96 -0
  180. package/manifest/components/pagination.json +92 -0
  181. package/manifest/components/panel-header.json +77 -0
  182. package/manifest/components/panel.json +71 -0
  183. package/manifest/components/password-input.json +119 -0
  184. package/manifest/components/piano-keyboard.json +120 -0
  185. package/manifest/components/plussie.json +111 -0
  186. package/manifest/components/popover.json +193 -0
  187. package/manifest/components/preset-section.json +73 -0
  188. package/manifest/components/progress.json +95 -0
  189. package/manifest/components/radio-group.json +73 -0
  190. package/manifest/components/radio.json +80 -0
  191. package/manifest/components/rating.json +141 -0
  192. package/manifest/components/resizable.json +88 -0
  193. package/manifest/components/row.json +109 -0
  194. package/manifest/components/scroll-area.json +91 -0
  195. package/manifest/components/scrollable-tabs-list.json +83 -0
  196. package/manifest/components/scrub-input.json +200 -0
  197. package/manifest/components/seam-cell.json +75 -0
  198. package/manifest/components/seam-grid.json +75 -0
  199. package/manifest/components/seam-list.json +56 -0
  200. package/manifest/components/search-input.json +134 -0
  201. package/manifest/components/section-header.json +112 -0
  202. package/manifest/components/segmented-control.json +138 -0
  203. package/manifest/components/select.json +194 -0
  204. package/manifest/components/separator.json +53 -0
  205. package/manifest/components/shape-section.json +66 -0
  206. package/manifest/components/sheet.json +186 -0
  207. package/manifest/components/sidebar.json +198 -0
  208. package/manifest/components/signal-edge.json +56 -0
  209. package/manifest/components/skeleton.json +46 -0
  210. package/manifest/components/slider.json +89 -0
  211. package/manifest/components/sparkline.json +76 -0
  212. package/manifest/components/spectrum-visualizer.json +115 -0
  213. package/manifest/components/spinner.json +61 -0
  214. package/manifest/components/stack.json +96 -0
  215. package/manifest/components/stat.json +87 -0
  216. package/manifest/components/status-bar.json +58 -0
  217. package/manifest/components/status-dot.json +78 -0
  218. package/manifest/components/stepper.json +89 -0
  219. package/manifest/components/switch.json +87 -0
  220. package/manifest/components/table-scroll-region.json +69 -0
  221. package/manifest/components/table.json +155 -0
  222. package/manifest/components/tabs.json +145 -0
  223. package/manifest/components/tags-input.json +190 -0
  224. package/manifest/components/terminal-frame.json +119 -0
  225. package/manifest/components/textarea.json +76 -0
  226. package/manifest/components/theme-configurator.json +124 -0
  227. package/manifest/components/theme-provider.json +134 -0
  228. package/manifest/components/threshold-rail.json +160 -0
  229. package/manifest/components/time-field.json +80 -0
  230. package/manifest/components/timeline-ruler.json +139 -0
  231. package/manifest/components/timeline.json +115 -0
  232. package/manifest/components/title-bar.json +82 -0
  233. package/manifest/components/toast.json +130 -0
  234. package/manifest/components/toggle-group.json +106 -0
  235. package/manifest/components/toggle.json +109 -0
  236. package/manifest/components/token-editor.json +90 -0
  237. package/manifest/components/toolbar.json +163 -0
  238. package/manifest/components/tooltip.json +164 -0
  239. package/manifest/components/tree-visibility-toggle.json +84 -0
  240. package/manifest/components/tree.json +140 -0
  241. package/manifest/components/two-step-button.json +113 -0
  242. package/manifest/components/universe-grid.json +127 -0
  243. package/manifest/components/usage-chart.json +124 -0
  244. package/manifest/manifest.json +2142 -0
  245. package/manifest/tokens.json +138 -0
  246. package/package.json +13 -5
@@ -1,37 +1,60 @@
1
+ import { DEFAULT_DENSITY, DEFAULT_FONT, DENSITIES, FONTS, MODES, THEMES } from "@cueplusplus/tokens";
1
2
  //#region src/system/prepaint.ts
3
+ /**
4
+ * Pre-paint stamping. Server-safe by design: no `"use client"`, no React, no
5
+ * DOM access at module scope — `app/layout.tsx` calls `prepaintScript()` on the
6
+ * server and inlines the result before hydration.
7
+ */
2
8
  /** localStorage key the ThemeProvider and the pre-paint script share by default. */
3
9
  const DEFAULT_STORAGE_KEY = "cue-ui";
10
+ /** `<` and `>` escaped, so no value can close the host `<script>` element. */
11
+ const scriptSafe = (json) => json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
4
12
  /**
5
13
  * Embed a string in JavaScript source that will itself be embedded in HTML.
6
14
  * `<` becomes `\u003c` so no value can close the host `<script>` element.
7
15
  */
8
16
  function jsStringLiteral(value) {
9
- return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
17
+ return scriptSafe(JSON.stringify(value));
10
18
  }
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(_){}}()`;
19
+ function prepaintScript(storageKeyOrOptions = DEFAULT_STORAGE_KEY, defaultTheme = "cue", defaultDensity = DEFAULT_DENSITY, defaultFont = DEFAULT_FONT) {
20
+ const options = typeof storageKeyOrOptions === "string" ? {
21
+ storageKey: storageKeyOrOptions,
22
+ defaults: {
23
+ theme: defaultTheme,
24
+ density: defaultDensity,
25
+ mode: "dark"
26
+ },
27
+ readStoredPreferences: true
28
+ } : storageKeyOrOptions;
29
+ if (options === null || typeof options !== "object" || options.defaults === void 0) throw new TypeError("prepaintScript options require a complete default triple");
30
+ const defaults = options.defaults;
31
+ if (defaults.theme === void 0 || defaults.density === void 0 || defaults.mode === void 0) throw new TypeError("prepaintScript options require a complete default triple");
32
+ if (!THEMES.includes(defaults.theme)) throw new TypeError("prepaintScript defaults contain an invalid theme");
33
+ if (!DENSITIES.includes(defaults.density)) throw new TypeError("prepaintScript defaults contain an invalid density");
34
+ if (!MODES.includes(defaults.mode)) throw new TypeError("prepaintScript defaults contain an invalid mode");
35
+ if (defaults.font !== void 0 && !FONTS.includes(defaults.font)) throw new TypeError("prepaintScript defaults contain an invalid font");
36
+ if (options.readStoredPreferences !== void 0 && typeof options.readStoredPreferences !== "boolean") throw new TypeError("prepaintScript readStoredPreferences must be boolean");
37
+ const storageKey = options.storageKey ?? "cue-ui";
38
+ if (typeof storageKey !== "string") throw new TypeError("prepaintScript storageKey must be a string");
39
+ const readStoredPreferences = options.readStoredPreferences ?? true;
40
+ const key = jsStringLiteral(storageKey);
41
+ const fallbackTheme = jsStringLiteral(defaults.theme);
42
+ const fallbackDensity = jsStringLiteral(defaults.density);
43
+ const fallbackMode = jsStringLiteral(defaults.mode);
44
+ const fallbackFont = jsStringLiteral(defaults.font ?? defaultFont);
45
+ const themes = scriptSafe(JSON.stringify([...THEMES]));
46
+ const densities = scriptSafe(JSON.stringify([...DENSITIES]));
47
+ const modes = scriptSafe(JSON.stringify([...MODES]));
48
+ /**
49
+ * The pairings, from the package that authors them, for the same reason.
50
+ *
51
+ * This is also the axis where an unvalidated read is worst: a stale or
52
+ * hand-edited `font` would stamp an attribute the stylesheet has no block
53
+ * for, `--cue-font-sans` would keep the base stack, and the page would render
54
+ * in a family the picker says is not selected.
55
+ */
56
+ const pairings = scriptSafe(JSON.stringify([...FONTS]));
57
+ return `!function(){try{var k=${key},e=document.documentElement,s=null;` + (readStoredPreferences ? "try{var r=localStorage.getItem(k);s=r?JSON.parse(r):null}catch(_){}" : "") + `s=s&&typeof s=="object"?s:{};var t=${themes}.indexOf(s.theme)>-1?s.theme:${fallbackTheme};var d=${densities}.indexOf(s.density)>-1?s.density:${fallbackDensity};var f=${pairings}.indexOf(s.font)>-1?s.font:${fallbackFont};var m=${modes}.indexOf(s.mode)>-1?s.mode:${fallbackMode};e.setAttribute("data-theme",t);e.setAttribute("data-density",d);e.setAttribute("data-font",f);e.setAttribute("data-mode",m);e.style.colorScheme=m==="system"?"light dark":m}catch(_){}}()`;
35
58
  }
36
59
  //#endregion
37
60
  export { DEFAULT_STORAGE_KEY, prepaintScript };
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { Density, Mode, ThemeName } from "@cueplusplus/tokens";
2
+ import { Density, FontName, Mode, ThemeName } from "@cueplusplus/tokens";
3
3
  //#region src/system/theme-provider.d.ts
4
4
  /** A mode that has been resolved to an actual palette — `"system"` never survives this far. */
5
5
  type ResolvedMode = "dark" | "light";
@@ -11,6 +11,8 @@ interface ThemeContextValue {
11
11
  density: Density;
12
12
  /** Mode as chosen, including the literal `"system"`. */
13
13
  mode: Mode;
14
+ /** Active font pairing. */
15
+ font: FontName;
14
16
  /** Mode after resolving `"system"` against `prefers-color-scheme`. */
15
17
  resolvedMode: ResolvedMode;
16
18
  /** Switch theme and persist the new preference triple. */
@@ -19,22 +21,51 @@ interface ThemeContextValue {
19
21
  setDensity: (density: Density) => void;
20
22
  /** Switch mode (`"system"` included) and persist the new preference triple. */
21
23
  setMode: (mode: Mode) => void;
24
+ /** Switch the font pairing and persist the new preference. */
25
+ setFont: (font: FontName) => void;
26
+ }
27
+ /** Provider-owned font-family overrides that detached portals must republish. */
28
+ interface FontFamilies {
29
+ /** Value for `--cue-font-sans`. */
30
+ sans?: string;
31
+ /** Value for `--cue-font-mono`. */
32
+ mono?: string;
22
33
  }
23
34
  interface ThemeProviderProps {
24
35
  /** Initial theme preset. Defaults to `"cue"`. Changing it after mount adopts the new value. */
25
36
  theme?: ThemeName;
26
- /** Initial density level. Defaults to `"normal"`. */
37
+ /** Initial density level. Defaults to `"compact"`. */
27
38
  density?: Density;
28
39
  /** Initial mode. Defaults to `"dark"`; `"system"` tracks `prefers-color-scheme`. */
29
40
  mode?: Mode;
41
+ /**
42
+ * Initial font pairing. Defaults to `"system"` — the platform's own faces,
43
+ * nothing downloaded, and the theme keeps whatever monospace it authored.
44
+ *
45
+ * A pairing that needs delivering is a set of *names*: this library ships no
46
+ * font files, and a pairing nobody delivers falls through its stack to the
47
+ * platform rather than failing. See `FONT_PAIRINGS[font].faces` for the
48
+ * custom properties an app assigns to make one resolve.
49
+ *
50
+ * **Root-level only.** There is no font island: a nested provider forwards
51
+ * this axis to the root, ignores this prop, and its `setFont` drives the root.
52
+ * A page that changed face halfway down is a page with a bug, and a specimen
53
+ * that genuinely wants one — a picker row, a docs page showing all eight —
54
+ * needs nothing from this library but `data-font` on a `<div>`.
55
+ */
56
+ font?: FontName;
30
57
  /** Multiplier on every text token, published as `--cue-font-scale`. Defaults to `1`. */
31
58
  fontScale?: number;
59
+ /** Optional app-owned font stacks, published as inline CUE font custom properties. */
60
+ fontFamilies?: FontFamilies;
32
61
  /**
33
- * localStorage key holding `{ theme, density, mode }`. Defaults to `"cue-ui"`.
62
+ * localStorage key holding `{ theme, density, font, mode }`. Defaults to `"cue-ui"`.
34
63
  * Only the outermost provider reads or writes it — a nested provider is an
35
64
  * island whose explicit props are the point, not a second preference store.
36
65
  */
37
66
  storageKey?: string;
67
+ /** Restore and write root preferences in localStorage. Defaults to `true`. */
68
+ persistPreferences?: boolean;
38
69
  /** Stamp the single child element instead of rendering a wrapper `<div>`. */
39
70
  asChild?: boolean;
40
71
  /** Extra classes for the provider root. */
@@ -46,27 +77,33 @@ interface ThemeProviderProps {
46
77
  }
47
78
  /**
48
79
  * 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.
80
+ * so the token layer resolves, puts `data-font` on `<html>`, publishes
81
+ * `--cue-font-scale`, and owns the persisted user preference.
51
82
  *
52
83
  * 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
84
+ * (or the single child when `asChild`). The `theme`/`density`/`font`/`mode` props are
54
85
  * *initial* values — the provider holds the state so `setTheme` and friends can
55
86
  * drive it — but a changed prop is adopted after mount, so a controlling parent
56
87
  * still works.
57
88
  *
89
+ * The font pairing is the one axis that never lands on this element: it has no
90
+ * island form, `<html>` is its only home, and restating it here would shadow the
91
+ * pre-paint stamp for the whole page on the first frame. See the `stamp` object
92
+ * below.
93
+ *
58
94
  * Persistence and the pre-paint contract: setters write
59
- * `{ theme, density, mode }` to `localStorage[storageKey]`, which is exactly what
95
+ * `{ theme, density, font, mode }` to `localStorage[storageKey]`, which is exactly what
60
96
  * {@link prepaintScript} reads to stamp `<html>` before the first paint. The
61
97
  * outermost provider keeps `<html>` in sync while the app runs (and restores the
62
98
  * previous stamp on unmount) so the page ground, UA scrollbars and form controls
63
- * follow the theme; nested providers never touch `<html>`.
99
+ * follow the theme; nested providers never touch `<html>`, and never own the
100
+ * font pairing — `useTheme().font` and `setFont` inside one are the root's.
64
101
  *
65
102
  * @example
66
- * <ThemeProvider theme="terminal" density="ultra-compact" mode="system">
103
+ * <ThemeProvider theme="terminal" density="ultra-compact" font="plex" mode="system">
67
104
  * <App />
68
105
  * </ThemeProvider>
69
106
  */
70
- declare function ThemeProvider({ theme: themeProp, density: densityProp, mode: modeProp, fontScale, storageKey, asChild, className, style: styleProp, children }: ThemeProviderProps): React.JSX.Element;
107
+ declare function ThemeProvider({ theme: themeProp, density: densityProp, mode: modeProp, font: fontProp, fontScale, fontFamilies, storageKey, persistPreferences, asChild, className, style: styleProp, children }: ThemeProviderProps): React.JSX.Element;
71
108
  //#endregion
72
- export { ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps };
109
+ export { FontFamilies, ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps };
@@ -1,19 +1,20 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect.js";
4
- import { DENSITY_LEVELS, DensityContext } from "./density.js";
4
+ import { DEFAULT_DENSITY as DEFAULT_DENSITY$1, DENSITY_LEVELS, DensityContext } from "./density.js";
5
5
  import { DEFAULT_STORAGE_KEY } from "./prepaint.js";
6
6
  import * as React from "react";
7
7
  import { jsx } from "react/jsx-runtime";
8
+ import { DEFAULT_FONT, FONTS, THEMES } from "@cueplusplus/tokens";
8
9
  //#region src/system/theme-provider.tsx
9
- const MODES = [
10
+ const MODES$1 = [
10
11
  "dark",
11
12
  "light",
12
13
  "system"
13
14
  ];
15
+ const FONT_NAMES = FONTS;
14
16
  /** Dark-first portfolio defaults; the same triple bare `:root` in `theme.css` paints. */
15
17
  const DEFAULT_THEME = "cue";
16
- const DEFAULT_DENSITY = "normal";
17
18
  const DEFAULT_MODE = "dark";
18
19
  const DEFAULT_RESOLVED_MODE = "dark";
19
20
  /**
@@ -33,6 +34,8 @@ const ThemeContext = React.createContext(null);
33
34
  * Internal: not part of the public API.
34
35
  */
35
36
  const FontScaleContext = React.createContext(null);
37
+ /** Effective font-family overrides from the nearest provider chain. */
38
+ const FontFamiliesContext = React.createContext(null);
36
39
  function isOneOf(allowed, value) {
37
40
  return typeof value === "string" && allowed.includes(value);
38
41
  }
@@ -56,62 +59,91 @@ function writePreferences(storageKey, preferences) {
56
59
  }
57
60
  /**
58
61
  * 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.
62
+ * so the token layer resolves, puts `data-font` on `<html>`, publishes
63
+ * `--cue-font-scale`, and owns the persisted user preference.
61
64
  *
62
65
  * 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
66
+ * (or the single child when `asChild`). The `theme`/`density`/`font`/`mode` props are
64
67
  * *initial* values — the provider holds the state so `setTheme` and friends can
65
68
  * drive it — but a changed prop is adopted after mount, so a controlling parent
66
69
  * still works.
67
70
  *
71
+ * The font pairing is the one axis that never lands on this element: it has no
72
+ * island form, `<html>` is its only home, and restating it here would shadow the
73
+ * pre-paint stamp for the whole page on the first frame. See the `stamp` object
74
+ * below.
75
+ *
68
76
  * Persistence and the pre-paint contract: setters write
69
- * `{ theme, density, mode }` to `localStorage[storageKey]`, which is exactly what
77
+ * `{ theme, density, font, mode }` to `localStorage[storageKey]`, which is exactly what
70
78
  * {@link prepaintScript} reads to stamp `<html>` before the first paint. The
71
79
  * outermost provider keeps `<html>` in sync while the app runs (and restores the
72
80
  * previous stamp on unmount) so the page ground, UA scrollbars and form controls
73
- * follow the theme; nested providers never touch `<html>`.
81
+ * follow the theme; nested providers never touch `<html>`, and never own the
82
+ * font pairing — `useTheme().font` and `setFont` inside one are the root's.
74
83
  *
75
84
  * @example
76
- * <ThemeProvider theme="terminal" density="ultra-compact" mode="system">
85
+ * <ThemeProvider theme="terminal" density="ultra-compact" font="plex" mode="system">
77
86
  * <App />
78
87
  * </ThemeProvider>
79
88
  */
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;
89
+ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp = DEFAULT_DENSITY$1, mode: modeProp = DEFAULT_MODE, font: fontProp = DEFAULT_FONT, fontScale = 1, fontFamilies, storageKey = DEFAULT_STORAGE_KEY, persistPreferences = true, asChild = false, className, style: styleProp, children }) {
90
+ const ambient = React.useContext(ThemeContext);
91
+ const isRoot = ambient === null;
82
92
  const [theme, setThemeState] = React.useState(themeProp);
83
93
  const [density, setDensityState] = React.useState(densityProp);
84
94
  const [mode, setModeState] = React.useState(modeProp);
95
+ const [rootFont, setRootFontState] = React.useState(() => {
96
+ if (!isRoot || !persistPreferences) return fontProp;
97
+ const stored = readPreferences(storageKey).font;
98
+ return isOneOf(FONT_NAMES, stored) ? stored : fontProp;
99
+ });
100
+ const font = isRoot ? rootFont : ambient.font;
85
101
  const [systemMode, setSystemMode] = React.useState(DEFAULT_RESOLVED_MODE);
86
102
  const resolvedMode = mode === "system" ? systemMode : mode;
87
103
  const restoredKey = React.useRef(null);
104
+ const [restored, setRestored] = React.useState(false);
88
105
  useIsomorphicLayoutEffect(() => {
89
- if (!isRoot || restoredKey.current === storageKey) return;
106
+ if (!isRoot) return;
107
+ if (!persistPreferences) {
108
+ setRestored(true);
109
+ return;
110
+ }
111
+ if (restoredKey.current === storageKey) return;
90
112
  restoredKey.current = storageKey;
113
+ setRestored(true);
91
114
  const stored = readPreferences(storageKey);
92
- if (typeof stored.theme === "string" && stored.theme.length > 0) setThemeState(stored.theme);
115
+ if (isOneOf(THEMES, stored.theme)) setThemeState(stored.theme);
93
116
  if (isOneOf(DENSITY_LEVELS, stored.density)) setDensityState(stored.density);
94
- if (isOneOf(MODES, stored.mode)) setModeState(stored.mode);
95
- }, [isRoot, storageKey]);
117
+ if (isOneOf(FONT_NAMES, stored.font)) setRootFontState(stored.font);
118
+ if (isOneOf(MODES$1, stored.mode)) setModeState(stored.mode);
119
+ }, [
120
+ isRoot,
121
+ persistPreferences,
122
+ storageKey
123
+ ]);
96
124
  const lastProps = React.useRef({
97
125
  theme: themeProp,
98
126
  density: densityProp,
99
- mode: modeProp
127
+ mode: modeProp,
128
+ font: fontProp
100
129
  });
101
130
  React.useEffect(() => {
102
131
  const previous = lastProps.current;
103
132
  lastProps.current = {
104
133
  theme: themeProp,
105
134
  density: densityProp,
106
- mode: modeProp
135
+ mode: modeProp,
136
+ font: fontProp
107
137
  };
108
138
  if (previous.theme !== themeProp) setThemeState(themeProp);
109
139
  if (previous.density !== densityProp) setDensityState(densityProp);
110
140
  if (previous.mode !== modeProp) setModeState(modeProp);
141
+ if (previous.font !== fontProp) setRootFontState(fontProp);
111
142
  }, [
112
143
  themeProp,
113
144
  densityProp,
114
- modeProp
145
+ modeProp,
146
+ fontProp
115
147
  ]);
116
148
  useIsomorphicLayoutEffect(() => {
117
149
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
@@ -124,13 +156,15 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
124
156
  const stateRef = React.useRef({
125
157
  theme,
126
158
  density,
127
- mode
159
+ mode,
160
+ font
128
161
  });
129
162
  useIsomorphicLayoutEffect(() => {
130
163
  stateRef.current = {
131
164
  theme,
132
165
  density,
133
- mode
166
+ mode,
167
+ font
134
168
  };
135
169
  });
136
170
  const persist = React.useCallback((patch) => {
@@ -139,9 +173,13 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
139
173
  ...patch
140
174
  };
141
175
  stateRef.current = next;
142
- if (!isRoot) return;
176
+ if (!isRoot || !persistPreferences) return;
143
177
  writePreferences(storageKey, next);
144
- }, [isRoot, storageKey]);
178
+ }, [
179
+ isRoot,
180
+ persistPreferences,
181
+ storageKey
182
+ ]);
145
183
  const setTheme = React.useCallback((next) => {
146
184
  setThemeState(next);
147
185
  persist({ theme: next });
@@ -154,25 +192,35 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
154
192
  setModeState(next);
155
193
  persist({ mode: next });
156
194
  }, [persist]);
195
+ const rootSetFont = React.useCallback((next) => {
196
+ setRootFontState(next);
197
+ persist({ font: next });
198
+ }, [persist]);
199
+ /** Nested providers hand the axis straight up: one document, one pairing. */
200
+ const setFont = isRoot ? rootSetFont : ambient.setFont;
157
201
  const originalStamp = React.useRef(null);
158
202
  useIsomorphicLayoutEffect(() => {
159
- if (!isRoot || typeof document === "undefined") return;
203
+ if (!isRoot || !restored || typeof document === "undefined") return;
160
204
  const root = document.documentElement;
161
205
  originalStamp.current ??= {
162
206
  theme: root.getAttribute("data-theme"),
163
207
  density: root.getAttribute("data-density"),
208
+ font: root.getAttribute("data-font"),
164
209
  mode: root.getAttribute("data-mode"),
165
210
  colorScheme: root.style.colorScheme
166
211
  };
167
212
  root.setAttribute("data-theme", theme);
168
213
  root.setAttribute("data-density", density);
169
- root.setAttribute("data-mode", resolvedMode);
170
- root.style.colorScheme = resolvedMode;
214
+ root.setAttribute("data-font", font);
215
+ root.setAttribute("data-mode", mode);
216
+ root.style.colorScheme = mode === "system" ? "light dark" : mode;
171
217
  }, [
172
218
  isRoot,
219
+ restored,
173
220
  theme,
174
221
  density,
175
- resolvedMode
222
+ font,
223
+ mode
176
224
  ]);
177
225
  React.useEffect(() => () => {
178
226
  const original = originalStamp.current;
@@ -185,6 +233,7 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
185
233
  };
186
234
  restore("data-theme", original.theme);
187
235
  restore("data-density", original.density);
236
+ restore("data-font", original.font);
188
237
  restore("data-mode", original.mode);
189
238
  root.style.colorScheme = original.colorScheme;
190
239
  }, []);
@@ -192,33 +241,55 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
192
241
  theme,
193
242
  density,
194
243
  mode,
244
+ font,
195
245
  resolvedMode,
196
246
  setTheme,
197
247
  setDensity,
198
- setMode
248
+ setMode,
249
+ setFont
199
250
  }), [
200
251
  theme,
201
252
  density,
202
253
  mode,
254
+ font,
203
255
  resolvedMode,
204
256
  setTheme,
205
257
  setDensity,
206
- setMode
258
+ setMode,
259
+ setFont
260
+ ]);
261
+ const parentFontFamilies = React.useContext(FontFamiliesContext);
262
+ const effectiveFontFamilies = React.useMemo(() => {
263
+ const sans = fontFamilies?.sans ?? parentFontFamilies?.sans;
264
+ const mono = fontFamilies?.mono ?? parentFontFamilies?.mono;
265
+ return sans === void 0 && mono === void 0 ? null : {
266
+ sans,
267
+ mono
268
+ };
269
+ }, [
270
+ fontFamilies?.sans,
271
+ fontFamilies?.mono,
272
+ parentFontFamilies?.sans,
273
+ parentFontFamilies?.mono
207
274
  ]);
208
275
  const style = React.useMemo(() => ({
209
- colorScheme: resolvedMode,
276
+ colorScheme: mode === "system" ? "light dark" : mode,
210
277
  "--cue-font-scale": fontScale,
278
+ ...fontFamilies?.sans === void 0 ? null : { "--cue-font-sans": fontFamilies.sans },
279
+ ...fontFamilies?.mono === void 0 ? null : { "--cue-font-mono": fontFamilies.mono },
211
280
  ...styleProp
212
281
  }), [
213
- resolvedMode,
282
+ mode,
214
283
  fontScale,
284
+ fontFamilies?.sans,
285
+ fontFamilies?.mono,
215
286
  styleProp
216
287
  ]);
217
288
  const stamp = {
218
289
  "data-cue-root": "",
219
290
  "data-theme": theme,
220
291
  "data-density": density,
221
- "data-mode": resolvedMode
292
+ "data-mode": mode
222
293
  };
223
294
  let root;
224
295
  if (asChild) {
@@ -241,12 +312,15 @@ function ThemeProvider({ theme: themeProp = DEFAULT_THEME, density: densityProp
241
312
  value: contextValue,
242
313
  children: /* @__PURE__ */ jsx(FontScaleContext.Provider, {
243
314
  value: fontScale,
244
- children: /* @__PURE__ */ jsx(DensityContext.Provider, {
245
- value: density,
246
- children: root
315
+ children: /* @__PURE__ */ jsx(FontFamiliesContext.Provider, {
316
+ value: effectiveFontFamilies,
317
+ children: /* @__PURE__ */ jsx(DensityContext.Provider, {
318
+ value: density,
319
+ children: root
320
+ })
247
321
  })
248
322
  })
249
323
  });
250
324
  }
251
325
  //#endregion
252
- export { FontScaleContext, ThemeContext, ThemeProvider };
326
+ export { FontFamiliesContext, FontScaleContext, ThemeContext, ThemeProvider };
@@ -4,11 +4,11 @@ import { Density } from "@cueplusplus/tokens";
4
4
  type ControlSize = "sm" | "md" | "lg";
5
5
  /**
6
6
  * The density level that actually applies here: the nearest `<Density>` island,
7
- * else the `<ThemeProvider>` level, else `"normal"`.
7
+ * else the `<ThemeProvider>` level, else `"compact"`.
8
8
  *
9
9
  * This is a JS read for code that cannot use CSS (virtualised row heights,
10
10
  * canvas drawing, layout math). Styling should use the density tokens or the
11
- * `density-ultra:` / `density-large:` variants instead.
11
+ * per-rung variants (`density-ultra-compact:` … `density-ultra-large:`) instead.
12
12
  *
13
13
  * @returns The applicable density level.
14
14
  */
@@ -26,7 +26,7 @@ declare function useDensity(): Density;
26
26
  * @param size - Which rung to measure. Defaults to `"md"`.
27
27
  * @returns Height in CSS pixels.
28
28
  * @example
29
- * const rowHeight = useControlHeight("sm"); // → 20 at normal density
29
+ * const rowHeight = useControlHeight("sm"); // → 20 at compact density
30
30
  */
31
31
  declare function useControlHeight(size?: ControlSize): number;
32
32
  //#endregion
@@ -14,15 +14,25 @@ const CONTROL_REM = {
14
14
  md: 1.25,
15
15
  lg: 1.5
16
16
  },
17
- normal: {
17
+ "compact": {
18
18
  sm: 1.25,
19
19
  md: 1.5,
20
20
  lg: 1.75
21
21
  },
22
- large: {
22
+ "normal": {
23
23
  sm: 1.75,
24
24
  md: 2,
25
25
  lg: 2.25
26
+ },
27
+ "large": {
28
+ sm: 2.25,
29
+ md: 2.5,
30
+ lg: 2.75
31
+ },
32
+ "ultra-large": {
33
+ sm: 2.75,
34
+ md: 3,
35
+ lg: 3.25
26
36
  }
27
37
  };
28
38
  /** Root font size assumed before the first measurement, so SSR and hydration agree. */
@@ -46,16 +56,16 @@ function measureControlHeight(density, size) {
46
56
  }
47
57
  /**
48
58
  * The density level that actually applies here: the nearest `<Density>` island,
49
- * else the `<ThemeProvider>` level, else `"normal"`.
59
+ * else the `<ThemeProvider>` level, else `"compact"`.
50
60
  *
51
61
  * This is a JS read for code that cannot use CSS (virtualised row heights,
52
62
  * canvas drawing, layout math). Styling should use the density tokens or the
53
- * `density-ultra:` / `density-large:` variants instead.
63
+ * per-rung variants (`density-ultra-compact:` … `density-ultra-large:`) instead.
54
64
  *
55
65
  * @returns The applicable density level.
56
66
  */
57
67
  function useDensity() {
58
- return React.useContext(DensityContext) ?? "normal";
68
+ return React.useContext(DensityContext) ?? "compact";
59
69
  }
60
70
  /**
61
71
  * The pixel height of a control at the ambient density — what a virtualiser or a
@@ -70,7 +80,7 @@ function useDensity() {
70
80
  * @param size - Which rung to measure. Defaults to `"md"`.
71
81
  * @returns Height in CSS pixels.
72
82
  * @example
73
- * const rowHeight = useControlHeight("sm"); // → 20 at normal density
83
+ * const rowHeight = useControlHeight("sm"); // → 20 at compact density
74
84
  */
75
85
  function useControlHeight(size = "md") {
76
86
  const density = useDensity();
@@ -3,10 +3,12 @@ import { ThemeContextValue } from "./theme-provider.js";
3
3
  /**
4
4
  * Read and drive the ambient theme state.
5
5
  *
6
- * Returns `{ theme, density, mode, resolvedMode, setTheme, setDensity, setMode }`.
6
+ * Returns `{ theme, density, mode, font, resolvedMode, setTheme, setDensity,
7
+ * setMode, setFont }`.
7
8
  * `density` here is the *provider's* level — a nested `<Density>` island does not
8
9
  * change it; use `useDensity()` when you want the level that actually applies
9
- * where you are.
10
+ * where you are. `font` is the pairing the provider holds; it has no island, so
11
+ * it is always the one in force.
10
12
  *
11
13
  * @throws If called outside a `<ThemeProvider>`. Theme state is never guessed:
12
14
  * a missing provider is a wiring bug, not a fallback situation.
@@ -5,10 +5,12 @@ import * as React from "react";
5
5
  /**
6
6
  * Read and drive the ambient theme state.
7
7
  *
8
- * Returns `{ theme, density, mode, resolvedMode, setTheme, setDensity, setMode }`.
8
+ * Returns `{ theme, density, mode, font, resolvedMode, setTheme, setDensity,
9
+ * setMode, setFont }`.
9
10
  * `density` here is the *provider's* level — a nested `<Density>` island does not
10
11
  * change it; use `useDensity()` when you want the level that actually applies
11
- * where you are.
12
+ * where you are. `font` is the pairing the provider holds; it has no island, so
13
+ * it is always the one in force.
12
14
  *
13
15
  * @throws If called outside a `<ThemeProvider>`. Theme state is never guessed:
14
16
  * a missing provider is a wiring bug, not a fallback situation.