@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.
- package/dist/charts/_token.js +39 -0
- package/dist/charts/container.d.ts +58 -0
- package/dist/charts/container.js +77 -0
- package/dist/charts/index.d.ts +5 -0
- package/dist/charts/index.js +5 -0
- package/dist/charts/ramps.d.ts +140 -0
- package/dist/charts/ramps.js +615 -0
- package/dist/charts/theme.d.ts +112 -0
- package/dist/charts/theme.js +165 -0
- package/dist/charts/tones.d.ts +111 -0
- package/dist/charts/tones.js +127 -0
- package/dist/chat/_glyphs.js +55 -0
- package/dist/chat/agent-color.d.ts +84 -0
- package/dist/chat/agent-color.js +101 -0
- package/dist/chat/agent-pile.d.ts +55 -0
- package/dist/chat/agent-pile.js +121 -0
- package/dist/chat/ask-box.d.ts +88 -0
- package/dist/chat/ask-box.js +341 -0
- package/dist/chat/composer.d.ts +66 -0
- package/dist/chat/composer.js +144 -0
- package/dist/chat/delegation-card.d.ts +60 -0
- package/dist/chat/delegation-card.js +129 -0
- package/dist/chat/index.d.ts +8 -0
- package/dist/chat/index.js +8 -0
- package/dist/chat/message-list.d.ts +39 -0
- package/dist/chat/message-list.js +105 -0
- package/dist/chat/message.d.ts +47 -0
- package/dist/chat/message.js +108 -0
- package/dist/chrome/_glyphs.js +101 -0
- package/dist/chrome/_tabs-scroll.d.ts +62 -0
- package/dist/chrome/_tabs-scroll.js +68 -0
- package/dist/chrome/_tree.d.ts +75 -0
- package/dist/chrome/_tree.js +49 -0
- package/dist/chrome/empty-state.d.ts +37 -0
- package/dist/chrome/empty-state.js +57 -0
- package/dist/chrome/eyebrow.d.ts +29 -0
- package/dist/chrome/eyebrow.js +36 -0
- package/dist/chrome/index.d.ts +17 -0
- package/dist/chrome/index.js +17 -0
- package/dist/chrome/navigation-menu.d.ts +80 -0
- package/dist/chrome/navigation-menu.js +121 -0
- package/dist/chrome/page-shell.d.ts +38 -0
- package/dist/chrome/page-shell.js +66 -0
- package/dist/chrome/panel.d.ts +55 -0
- package/dist/chrome/panel.js +67 -0
- package/dist/chrome/row.d.ts +82 -0
- package/dist/chrome/row.js +106 -0
- package/dist/chrome/scroll-area.d.ts +39 -0
- package/dist/chrome/scroll-area.js +70 -0
- package/dist/chrome/section-header.d.ts +42 -0
- package/dist/chrome/section-header.js +79 -0
- package/dist/chrome/segmented-control.d.ts +58 -0
- package/dist/chrome/segmented-control.js +76 -0
- package/dist/chrome/status-bar.d.ts +46 -0
- package/dist/chrome/status-bar.js +56 -0
- package/dist/chrome/tabs.d.ts +80 -0
- package/dist/chrome/tabs.js +176 -0
- package/dist/chrome/title-bar.d.ts +37 -0
- package/dist/chrome/title-bar.js +45 -0
- package/dist/chrome/toolbar.d.ts +46 -0
- package/dist/chrome/toolbar.js +62 -0
- package/dist/chrome/tree.d.ts +98 -0
- package/dist/chrome/tree.js +248 -0
- package/dist/color/_checker.js +14 -0
- package/dist/color/_convert.d.ts +52 -0
- package/dist/color/_convert.js +66 -0
- package/dist/color/_glyphs.js +28 -0
- package/dist/color/_thumb.js +15 -0
- package/dist/color/_value.js +28 -0
- package/dist/color/color-area.d.ts +27 -0
- package/dist/color/color-area.js +40 -0
- package/dist/color/color-field.d.ts +32 -0
- package/dist/color/color-field.js +43 -0
- package/dist/color/color-picker.d.ts +71 -0
- package/dist/color/color-picker.js +178 -0
- package/dist/color/color-slider.d.ts +31 -0
- package/dist/color/color-slider.js +51 -0
- package/dist/color/color-swatch.d.ts +32 -0
- package/dist/color/color-swatch.js +44 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +7 -0
- package/dist/configurator/_export.d.ts +78 -0
- package/dist/configurator/_export.js +261 -0
- package/dist/configurator/_glyphs.js +92 -0
- package/dist/configurator/_overrides.d.ts +247 -0
- package/dist/configurator/_overrides.js +514 -0
- package/dist/configurator/configurator.d.ts +71 -0
- package/dist/configurator/configurator.js +279 -0
- package/dist/configurator/export-dialog.d.ts +30 -0
- package/dist/configurator/export-dialog.js +121 -0
- package/dist/configurator/index.d.ts +8 -0
- package/dist/configurator/index.js +8 -0
- package/dist/configurator/panel-sections.d.ts +86 -0
- package/dist/configurator/panel-sections.js +316 -0
- package/dist/configurator/token-editor.d.ts +74 -0
- package/dist/configurator/token-editor.js +149 -0
- package/dist/configurator/use-zoom.d.ts +87 -0
- package/dist/configurator/use-zoom.js +113 -0
- package/dist/date/_glyphs.js +34 -0
- package/dist/date/_segments.js +55 -0
- package/dist/date/_trigger.js +31 -0
- package/dist/date/calendar.d.ts +25 -0
- package/dist/date/calendar.js +125 -0
- package/dist/date/date-field.d.ts +34 -0
- package/dist/date/date-field.js +41 -0
- package/dist/date/date-picker.d.ts +69 -0
- package/dist/date/date-picker.js +89 -0
- package/dist/date/date-range-picker.d.ts +65 -0
- package/dist/date/date-range-picker.js +92 -0
- package/dist/date/index.d.ts +6 -0
- package/dist/date/index.js +6 -0
- package/dist/date/time-field.d.ts +29 -0
- package/dist/date/time-field.js +36 -0
- package/dist/dmx/_ramp.d.ts +77 -0
- package/dist/dmx/_ramp.js +169 -0
- package/dist/dmx/_readout.js +53 -0
- package/dist/dmx/_universe.d.ts +105 -0
- package/dist/dmx/_universe.js +112 -0
- package/dist/dmx/channel-matrix.d.ts +81 -0
- package/dist/dmx/channel-matrix.js +285 -0
- package/dist/dmx/dmx-bar.d.ts +50 -0
- package/dist/dmx/dmx-bar.js +107 -0
- package/dist/dmx/dmx-strip.d.ts +41 -0
- package/dist/dmx/dmx-strip.js +196 -0
- package/dist/dmx/index.d.ts +7 -0
- package/dist/dmx/index.js +7 -0
- package/dist/dmx/universe-grid.d.ts +61 -0
- package/dist/dmx/universe-grid.js +182 -0
- package/dist/flow/_frame.d.ts +52 -0
- package/dist/flow/_frame.js +38 -0
- package/dist/flow/controls.d.ts +41 -0
- package/dist/flow/controls.js +53 -0
- package/dist/flow/flow.css +121 -0
- package/dist/flow/handles.d.ts +40 -0
- package/dist/flow/handles.js +61 -0
- package/dist/flow/index.d.ts +7 -0
- package/dist/flow/index.js +7 -0
- package/dist/flow/node-card.d.ts +71 -0
- package/dist/flow/node-card.js +102 -0
- package/dist/flow/signal-edge.d.ts +60 -0
- package/dist/flow/signal-edge.js +108 -0
- package/dist/flow/use-flow-theme.d.ts +69 -0
- package/dist/flow/use-flow-theme.js +109 -0
- package/dist/forms/_chassis.d.ts +64 -0
- package/dist/forms/_chassis.js +59 -0
- package/dist/forms/_glyphs.js +143 -0
- package/dist/forms/_listbox.js +55 -0
- package/dist/forms/autocomplete.d.ts +61 -0
- package/dist/forms/autocomplete.js +88 -0
- package/dist/forms/button-group.d.ts +47 -0
- package/dist/forms/button-group.js +52 -0
- package/dist/forms/checkbox.d.ts +51 -0
- package/dist/forms/checkbox.js +98 -0
- package/dist/forms/combobox.d.ts +73 -0
- package/dist/forms/combobox.js +108 -0
- package/dist/forms/env-var-input.d.ts +74 -0
- package/dist/forms/env-var-input.js +116 -0
- package/dist/forms/field.d.ts +89 -0
- package/dist/forms/field.js +102 -0
- package/dist/forms/file-upload.d.ts +113 -0
- package/dist/forms/file-upload.js +232 -0
- package/dist/forms/index.d.ts +26 -0
- package/dist/forms/index.js +26 -0
- package/dist/forms/input-group.d.ts +31 -0
- package/dist/forms/input-group.js +54 -0
- package/dist/forms/input.d.ts +33 -0
- package/dist/forms/input.js +36 -0
- package/dist/forms/multi-select.d.ts +62 -0
- package/dist/forms/multi-select.js +107 -0
- package/dist/forms/number-field.d.ts +38 -0
- package/dist/forms/number-field.js +64 -0
- package/dist/forms/otp-field.d.ts +54 -0
- package/dist/forms/otp-field.js +59 -0
- package/dist/forms/password-input.d.ts +46 -0
- package/dist/forms/password-input.js +62 -0
- package/dist/forms/radio-group.d.ts +50 -0
- package/dist/forms/radio-group.js +64 -0
- package/dist/forms/rating.d.ts +69 -0
- package/dist/forms/rating.js +103 -0
- package/dist/forms/scrub-input.d.ts +81 -0
- package/dist/forms/scrub-input.js +249 -0
- package/dist/forms/search-input.d.ts +54 -0
- package/dist/forms/search-input.js +112 -0
- package/dist/forms/select.d.ts +75 -0
- package/dist/forms/select.js +91 -0
- package/dist/forms/slider.d.ts +44 -0
- package/dist/forms/slider.js +65 -0
- package/dist/forms/switch.d.ts +36 -0
- package/dist/forms/switch.js +71 -0
- package/dist/forms/tags-input.d.ts +69 -0
- package/dist/forms/tags-input.js +148 -0
- package/dist/forms/textarea.d.ts +27 -0
- package/dist/forms/textarea.js +37 -0
- package/dist/forms/toggle-group.d.ts +41 -0
- package/dist/forms/toggle-group.js +56 -0
- package/dist/forms/toggle.d.ts +41 -0
- package/dist/forms/toggle.js +100 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +104 -0
- package/dist/instruments/_ansi.js +116 -0
- package/dist/instruments/_glyphs.js +90 -0
- package/dist/instruments/_motion.js +56 -0
- package/dist/instruments/_rem.js +21 -0
- package/dist/instruments/app-window-frame.d.ts +48 -0
- package/dist/instruments/app-window-frame.js +76 -0
- package/dist/instruments/copy-button.d.ts +41 -0
- package/dist/instruments/copy-button.js +112 -0
- package/dist/instruments/data-table.d.ts +168 -0
- package/dist/instruments/data-table.js +212 -0
- package/dist/instruments/group-bar.d.ts +63 -0
- package/dist/instruments/group-bar.js +122 -0
- package/dist/instruments/index.d.ts +13 -0
- package/dist/instruments/index.js +13 -0
- package/dist/instruments/log-viewer.d.ts +74 -0
- package/dist/instruments/log-viewer.js +215 -0
- package/dist/instruments/meter.d.ts +77 -0
- package/dist/instruments/meter.js +96 -0
- package/dist/instruments/progress.d.ts +45 -0
- package/dist/instruments/progress.js +61 -0
- package/dist/instruments/seam-grid.d.ts +66 -0
- package/dist/instruments/seam-grid.js +78 -0
- package/dist/instruments/sparkline.d.ts +39 -0
- package/dist/instruments/sparkline.js +85 -0
- package/dist/instruments/stat.d.ts +35 -0
- package/dist/instruments/stat.js +59 -0
- package/dist/instruments/table.d.ts +51 -0
- package/dist/instruments/table.js +92 -0
- package/dist/instruments/terminal-frame.d.ts +89 -0
- package/dist/instruments/terminal-frame.js +256 -0
- package/dist/instruments/two-step-button.d.ts +47 -0
- package/dist/instruments/two-step-button.js +147 -0
- package/dist/instruments/usage-chart.d.ts +55 -0
- package/dist/instruments/usage-chart.js +235 -0
- package/dist/layout/_collapse.js +46 -0
- package/dist/layout/_glyphs.js +45 -0
- package/dist/layout/accordion.d.ts +52 -0
- package/dist/layout/accordion.js +81 -0
- package/dist/layout/breadcrumb.d.ts +35 -0
- package/dist/layout/breadcrumb.js +67 -0
- package/dist/layout/card.d.ts +51 -0
- package/dist/layout/card.js +76 -0
- package/dist/layout/carousel.d.ts +96 -0
- package/dist/layout/carousel.js +211 -0
- package/dist/layout/collapsible.d.ts +74 -0
- package/dist/layout/collapsible.js +126 -0
- package/dist/layout/container.d.ts +27 -0
- package/dist/layout/container.js +40 -0
- package/dist/layout/description-list.d.ts +24 -0
- package/dist/layout/description-list.js +37 -0
- package/dist/layout/grid.d.ts +40 -0
- package/dist/layout/grid.js +48 -0
- package/dist/layout/index.d.ts +15 -0
- package/dist/layout/index.js +15 -0
- package/dist/layout/item.d.ts +65 -0
- package/dist/layout/item.js +100 -0
- package/dist/layout/link.d.ts +48 -0
- package/dist/layout/link.js +63 -0
- package/dist/layout/pagination.d.ts +54 -0
- package/dist/layout/pagination.js +121 -0
- package/dist/layout/resizable.d.ts +38 -0
- package/dist/layout/resizable.js +68 -0
- package/dist/layout/sidebar.d.ts +67 -0
- package/dist/layout/sidebar.js +129 -0
- package/dist/layout/stack.d.ts +46 -0
- package/dist/layout/stack.js +62 -0
- package/dist/layout/stepper.d.ts +49 -0
- package/dist/layout/stepper.js +81 -0
- package/dist/layout/timeline.d.ts +43 -0
- package/dist/layout/timeline.js +63 -0
- package/dist/lib/aria-class.js +24 -0
- package/dist/lib/cn.js +18 -0
- package/dist/lib/compose.js +38 -0
- package/dist/lib/cva.d.ts +18 -0
- package/dist/lib/cva.js +65 -0
- package/dist/midi/_notes.d.ts +101 -0
- package/dist/midi/_notes.js +183 -0
- package/dist/midi/_ruler.d.ts +100 -0
- package/dist/midi/_ruler.js +105 -0
- package/dist/midi/_time.d.ts +107 -0
- package/dist/midi/_time.js +130 -0
- package/dist/midi/index.d.ts +9 -0
- package/dist/midi/index.js +9 -0
- package/dist/midi/musical-time-input.d.ts +48 -0
- package/dist/midi/musical-time-input.js +154 -0
- package/dist/midi/piano-keyboard.d.ts +50 -0
- package/dist/midi/piano-keyboard.js +193 -0
- package/dist/midi/spectrum-visualizer.d.ts +65 -0
- package/dist/midi/spectrum-visualizer.js +213 -0
- package/dist/midi/threshold-rail.d.ts +72 -0
- package/dist/midi/threshold-rail.js +214 -0
- package/dist/midi/timeline-ruler.d.ts +83 -0
- package/dist/midi/timeline-ruler.js +152 -0
- package/dist/overlays/_debounce.js +34 -0
- package/dist/overlays/_glyphs.js +59 -0
- package/dist/overlays/_surface.d.ts +70 -0
- package/dist/overlays/_surface.js +70 -0
- package/dist/overlays/_wheel.js +42 -0
- package/dist/overlays/alert-dialog.d.ts +67 -0
- package/dist/overlays/alert-dialog.js +117 -0
- package/dist/overlays/command-palette.d.ts +93 -0
- package/dist/overlays/command-palette.js +193 -0
- package/dist/overlays/context-menu.d.ts +64 -0
- package/dist/overlays/context-menu.js +78 -0
- package/dist/overlays/dialog.d.ts +72 -0
- package/dist/overlays/dialog.js +124 -0
- package/dist/overlays/drawer.d.ts +75 -0
- package/dist/overlays/drawer.js +151 -0
- package/dist/overlays/dropdown-menu.d.ts +79 -0
- package/dist/overlays/dropdown-menu.js +156 -0
- package/dist/overlays/hover-card.d.ts +58 -0
- package/dist/overlays/hover-card.js +91 -0
- package/dist/overlays/index.d.ts +15 -0
- package/dist/overlays/index.js +15 -0
- package/dist/overlays/info-tip.d.ts +41 -0
- package/dist/overlays/info-tip.js +45 -0
- package/dist/overlays/menubar.d.ts +72 -0
- package/dist/overlays/menubar.js +100 -0
- package/dist/overlays/popover.d.ts +66 -0
- package/dist/overlays/popover.js +114 -0
- package/dist/overlays/sheet.d.ts +78 -0
- package/dist/overlays/sheet.js +131 -0
- package/dist/overlays/toast.d.ts +106 -0
- package/dist/overlays/toast.js +146 -0
- package/dist/overlays/tooltip.d.ts +57 -0
- package/dist/overlays/tooltip.js +95 -0
- package/dist/primitives/avatar.d.ts +51 -0
- package/dist/primitives/avatar.js +93 -0
- package/dist/primitives/button.d.ts +46 -0
- package/dist/primitives/button.js +94 -0
- package/dist/primitives/chip.d.ts +47 -0
- package/dist/primitives/chip.js +152 -0
- package/dist/primitives/icon-button.d.ts +41 -0
- package/dist/primitives/icon-button.js +59 -0
- package/dist/primitives/index.d.ts +10 -0
- package/dist/primitives/index.js +10 -0
- package/dist/primitives/kbd.d.ts +17 -0
- package/dist/primitives/kbd.js +24 -0
- package/dist/primitives/separator.d.ts +25 -0
- package/dist/primitives/separator.js +30 -0
- package/dist/primitives/skeleton.d.ts +20 -0
- package/dist/primitives/skeleton.js +27 -0
- package/dist/primitives/spinner.d.ts +26 -0
- package/dist/primitives/spinner.js +31 -0
- package/dist/primitives/status-dot.d.ts +44 -0
- package/dist/primitives/status-dot.js +50 -0
- package/dist/styles.css +19 -0
- package/dist/system/density.d.ts +33 -0
- package/dist/system/density.js +49 -0
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.js +7 -0
- package/dist/system/portal.d.ts +51 -0
- package/dist/system/portal.js +122 -0
- package/dist/system/prepaint.d.ts +38 -0
- package/dist/system/prepaint.js +37 -0
- package/dist/system/theme-provider.d.ts +72 -0
- package/dist/system/theme-provider.js +252 -0
- package/dist/system/use-density.d.ts +33 -0
- package/dist/system/use-density.js +84 -0
- package/dist/system/use-isomorphic-layout-effect.js +15 -0
- package/dist/system/use-theme.d.ts +19 -0
- package/dist/system/use-theme.js +25 -0
- package/dist/theming/_presets.d.ts +11 -0
- package/dist/theming/_presets.js +487 -0
- package/dist/theming/contrast.d.ts +110 -0
- package/dist/theming/contrast.js +167 -0
- package/dist/theming/create-theme.d.ts +165 -0
- package/dist/theming/create-theme.js +442 -0
- package/dist/theming/index.d.ts +5 -0
- package/dist/theming/index.js +5 -0
- package/dist/theming/serialize.d.ts +145 -0
- package/dist/theming/serialize.js +165 -0
- 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 };
|