@cueplusplus/ui 0.1.0 → 0.2.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/CHANGELOG.md +684 -0
- package/dist/brand/_chassis.d.ts +54 -0
- package/dist/brand/_chassis.js +81 -0
- package/dist/brand/_geometry.js +41 -0
- package/dist/brand/cue-logotype.d.ts +29 -0
- package/dist/brand/cue-logotype.js +60 -0
- package/dist/brand/cue-mark.d.ts +30 -0
- package/dist/brand/cue-mark.js +61 -0
- package/dist/brand/index.d.ts +5 -0
- package/dist/brand/plussie.d.ts +67 -0
- package/dist/brand/plussie.js +125 -0
- package/dist/chat/agent-pile.js +1 -1
- package/dist/chat/ask-box.js +2 -2
- package/dist/chat/delegation-card.js +1 -1
- package/dist/chat/message.js +2 -2
- package/dist/chrome/_edge-scroller.d.ts +45 -0
- package/dist/chrome/_edge-scroller.js +138 -0
- package/dist/chrome/_glyphs.js +19 -1
- package/dist/chrome/_tabs-scroll.d.ts +12 -7
- package/dist/chrome/_tabs-scroll.js +12 -7
- package/dist/chrome/app-bar.d.ts +103 -0
- package/dist/chrome/app-bar.js +175 -0
- package/dist/chrome/app-shell.d.ts +125 -0
- package/dist/chrome/app-shell.js +167 -0
- package/dist/chrome/footer.d.ts +75 -0
- package/dist/chrome/footer.js +68 -0
- package/dist/chrome/index.d.ts +5 -1
- package/dist/chrome/index.js +4 -1
- package/dist/chrome/navigation-menu.js +1 -1
- package/dist/chrome/page-shell.d.ts +1 -1
- package/dist/chrome/page-shell.js +15 -5
- package/dist/chrome/segmented-control.d.ts +19 -0
- package/dist/chrome/segmented-control.js +40 -21
- package/dist/chrome/tabs.js +21 -65
- package/dist/chrome/toolbar.d.ts +22 -0
- package/dist/chrome/toolbar.js +26 -5
- package/dist/configurator/_overrides.js +4 -2
- package/dist/configurator/configurator.js +1 -1
- package/dist/configurator/panel-sections.js +10 -4
- package/dist/dmx/channel-matrix.js +1 -1
- package/dist/forms/otp-field.js +1 -1
- package/dist/index.d.ts +15 -6
- package/dist/index.js +15 -9
- package/dist/instruments/data-table.js +1 -1
- package/dist/layout/carousel.js +1 -1
- package/dist/layout/container.d.ts +1 -1
- package/dist/layout/container.js +6 -3
- package/dist/layout/index.d.ts +3 -3
- package/dist/layout/index.js +3 -3
- package/dist/layout/pagination.js +1 -1
- package/dist/layout/sidebar.d.ts +23 -3
- package/dist/layout/sidebar.js +18 -7
- package/dist/midi/spectrum-visualizer.js +1 -1
- package/dist/midi/timeline-ruler.js +1 -1
- package/dist/overlays/command-palette.js +1 -1
- package/dist/overlays/dialog.js +1 -1
- package/dist/overlays/dropdown-menu.js +2 -2
- package/dist/overlays/hover-card.js +1 -1
- package/dist/overlays/index.js +1 -1
- package/dist/overlays/popover.js +1 -1
- package/dist/overlays/sheet.js +1 -1
- package/dist/overlays/toast.js +1 -1
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +1 -1
- package/dist/primitives/status-dot.d.ts +7 -0
- package/dist/primitives/status-dot.js +8 -1
- package/dist/styles.css +47 -1
- package/dist/system/density.d.ts +4 -1
- package/dist/system/density.js +12 -4
- package/dist/system/index.d.ts +2 -2
- package/dist/system/portal.d.ts +5 -5
- package/dist/system/portal.js +11 -6
- package/dist/system/prepaint.d.ts +15 -8
- package/dist/system/prepaint.js +26 -9
- package/dist/system/theme-provider.d.ts +36 -10
- package/dist/system/theme-provider.js +56 -18
- package/dist/system/use-density.d.ts +3 -3
- package/dist/system/use-density.js +16 -6
- package/dist/system/use-theme.d.ts +4 -2
- package/dist/system/use-theme.js +4 -2
- package/dist/theming/_presets.js +80 -5
- package/dist/theming/create-theme.js +13 -4
- package/package.json +4 -3
package/dist/overlays/popover.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { useComposedRefs } from "../lib/compose.js";
|
|
3
4
|
import { useCuePortalProps } from "../system/portal.js";
|
|
4
5
|
import { overlayDescriptionClasses, overlayPopupTransitionClasses, overlayPositionerClasses, overlaySurfaceClasses, overlayTitleClasses } from "./_surface.js";
|
|
5
|
-
import { useComposedRefs } from "../lib/compose.js";
|
|
6
6
|
import { useCloseOnWheel } from "./_wheel.js";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import { jsx } from "react/jsx-runtime";
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
3
|
import { cva } from "../lib/cva.js";
|
|
4
|
+
import { CrossGlyph } from "./_glyphs.js";
|
|
4
5
|
import { useCuePortalProps } from "../system/portal.js";
|
|
5
6
|
import { overlayDescriptionClasses, overlayFooterClasses, overlaySurfaceClasses, overlayTitleClasses, scrimClasses } from "./_surface.js";
|
|
6
7
|
import { IconButton } from "../primitives/icon-button.js";
|
|
7
|
-
import { CrossGlyph } from "./_glyphs.js";
|
|
8
8
|
import * as React from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
import { Dialog } from "@base-ui/react/dialog";
|
package/dist/overlays/toast.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { CrossGlyph } from "./_glyphs.js";
|
|
3
4
|
import { useCuePortalProps } from "../system/portal.js";
|
|
4
5
|
import { overlaySurfaceClasses } from "./_surface.js";
|
|
5
|
-
import { CrossGlyph } from "./_glyphs.js";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { Toast } from "@base-ui/react/toast";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { Separator, SeparatorProps } from "./separator.js";
|
|
1
2
|
import { Button, ButtonProps, buttonVariants } from "./button.js";
|
|
2
3
|
import { StatusDot, StatusDotProps, StatusTone } from "./status-dot.js";
|
|
3
4
|
import { Avatar, AvatarGroup, AvatarGroupProps, AvatarProps, AvatarSize } from "./avatar.js";
|
|
4
5
|
import { Chip, ChipProps, chipVariants } from "./chip.js";
|
|
5
6
|
import { IconButton, IconButtonProps } from "./icon-button.js";
|
|
6
7
|
import { Kbd, KbdProps } from "./kbd.js";
|
|
7
|
-
import { Separator, SeparatorProps } from "./separator.js";
|
|
8
8
|
import { Skeleton, SkeletonProps } from "./skeleton.js";
|
|
9
9
|
import { Spinner, SpinnerProps } from "./spinner.js";
|
|
10
10
|
export { Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, type AvatarSize, Button, type ButtonProps, Chip, type ChipProps, IconButton, type IconButtonProps, Kbd, type KbdProps, Separator, type SeparatorProps, Skeleton, type SkeletonProps, Spinner, type SpinnerProps, StatusDot, type StatusDotProps, type StatusTone, buttonVariants, chipVariants };
|
package/dist/primitives/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Button, buttonVariants } from "./button.js";
|
|
2
2
|
import { Chip, chipVariants } from "./chip.js";
|
|
3
3
|
import { Spinner } from "./spinner.js";
|
|
4
|
+
import { Separator } from "./separator.js";
|
|
4
5
|
import { IconButton } from "./icon-button.js";
|
|
5
6
|
import { StatusDot } from "./status-dot.js";
|
|
6
7
|
import { Avatar, AvatarGroup } from "./avatar.js";
|
|
7
8
|
import { Kbd } from "./kbd.js";
|
|
8
|
-
import { Separator } from "./separator.js";
|
|
9
9
|
import { Skeleton } from "./skeleton.js";
|
|
10
10
|
export { Avatar, AvatarGroup, Button, Chip, IconButton, Kbd, Separator, Skeleton, Spinner, StatusDot, buttonVariants, chipVariants };
|
|
@@ -32,6 +32,13 @@ interface StatusDotProps extends Omit<React.ComponentPropsWithoutRef<"span">, "c
|
|
|
32
32
|
* component always emits an `sr-only` label inside a `role="status"` wrapper,
|
|
33
33
|
* so the state is announced as well as shown (WCAG 1.4.1).
|
|
34
34
|
*
|
|
35
|
+
* The wrapper is `relative` for that label's sake and no other reason.
|
|
36
|
+
* `.sr-only` is `position: absolute`; without a positioned ancestor its
|
|
37
|
+
* containing block is the initial one, so its scrollable overflow lands on
|
|
38
|
+
* `<html>` rather than on whatever scroller it is nested in. One dot is
|
|
39
|
+
* harmless and a table of two thousand rows is a document several thousand
|
|
40
|
+
* pixels taller than anything visible on it.
|
|
41
|
+
*
|
|
35
42
|
* Static markup — no `"use client"`.
|
|
36
43
|
*
|
|
37
44
|
* @example
|
|
@@ -20,6 +20,13 @@ const toRem = (px) => `${px / 16}rem`;
|
|
|
20
20
|
* component always emits an `sr-only` label inside a `role="status"` wrapper,
|
|
21
21
|
* so the state is announced as well as shown (WCAG 1.4.1).
|
|
22
22
|
*
|
|
23
|
+
* The wrapper is `relative` for that label's sake and no other reason.
|
|
24
|
+
* `.sr-only` is `position: absolute`; without a positioned ancestor its
|
|
25
|
+
* containing block is the initial one, so its scrollable overflow lands on
|
|
26
|
+
* `<html>` rather than on whatever scroller it is nested in. One dot is
|
|
27
|
+
* harmless and a table of two thousand rows is a document several thousand
|
|
28
|
+
* pixels taller than anything visible on it.
|
|
29
|
+
*
|
|
23
30
|
* Static markup — no `"use client"`.
|
|
24
31
|
*
|
|
25
32
|
* @example
|
|
@@ -31,7 +38,7 @@ const StatusDot = React.forwardRef(function StatusDot({ className, tone, pulse =
|
|
|
31
38
|
return /* @__PURE__ */ jsxs("span", {
|
|
32
39
|
ref,
|
|
33
40
|
role: "status",
|
|
34
|
-
className: cn("inline-flex shrink-0 items-center justify-center", className),
|
|
41
|
+
className: cn("relative inline-flex shrink-0 items-center justify-center", className),
|
|
35
42
|
...elementProps,
|
|
36
43
|
children: [/* @__PURE__ */ jsx("span", {
|
|
37
44
|
"aria-hidden": "true",
|
package/dist/styles.css
CHANGED
|
@@ -14,6 +14,52 @@
|
|
|
14
14
|
|
|
15
15
|
@source "./";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/* One variant per rung of the density ladder, each named after the rung it
|
|
18
|
+
* matches — so `density-large:` means the level a consumer writes as
|
|
19
|
+
* `density="large"`, and cannot drift from it.
|
|
20
|
+
*
|
|
21
|
+
* `density-ultra:` is gone with the five-rung rename of 2026-08-19: with an
|
|
22
|
+
* `ultra-large` rung in the ladder, "ultra" no longer names one level. Its
|
|
23
|
+
* replacement is `density-ultra-compact:`. `density-large:` survives the rename
|
|
24
|
+
* as a name and changes meaning with it — it matched the ladder's top rung
|
|
25
|
+
* before, and the ladder now has two rungs above that one. */
|
|
26
|
+
@custom-variant density-ultra-compact (&:where([data-density="ultra-compact"], [data-density="ultra-compact"] *));
|
|
27
|
+
@custom-variant density-compact (&:where([data-density="compact"], [data-density="compact"] *));
|
|
28
|
+
@custom-variant density-normal (&:where([data-density="normal"], [data-density="normal"] *));
|
|
18
29
|
@custom-variant density-large (&:where([data-density="large"], [data-density="large"] *));
|
|
30
|
+
@custom-variant density-ultra-large (&:where([data-density="ultra-large"], [data-density="ultra-large"] *));
|
|
19
31
|
@custom-variant mode-light (&:where([data-mode="light"], [data-mode="light"] *));
|
|
32
|
+
|
|
33
|
+
/* The one keyframe this library's core needs, and the reason it cannot be a
|
|
34
|
+
* utility: `<Plussie idle />` blinks, and Tailwind has no `animate-*` for a
|
|
35
|
+
* two-frame squash about an SVG group's own centre.
|
|
36
|
+
*
|
|
37
|
+
* Same shape as the pulse in `flow.css`: a bespoke class, the keyframes beside
|
|
38
|
+
* it, and a `prefers-reduced-motion` block that switches it off. `fill-box`
|
|
39
|
+
* puts the transform origin at the eye's own centre rather than the SVG's
|
|
40
|
+
* corner, which is what makes the eye shut across its middle instead of sliding
|
|
41
|
+
* up. The class does nothing unless the prop asks for it — every other Plussie
|
|
42
|
+
* on the page is still. */
|
|
43
|
+
@keyframes cue-plussie-blink {
|
|
44
|
+
0%,
|
|
45
|
+
93%,
|
|
46
|
+
100% {
|
|
47
|
+
transform: scaleY(1);
|
|
48
|
+
}
|
|
49
|
+
95.5%,
|
|
50
|
+
96.5% {
|
|
51
|
+
transform: scaleY(0.05);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cue-plussie-idle {
|
|
56
|
+
transform-box: fill-box;
|
|
57
|
+
transform-origin: center;
|
|
58
|
+
animation: cue-plussie-blink 5.2s ease-in-out infinite;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@media (prefers-reduced-motion: reduce) {
|
|
62
|
+
.cue-plussie-idle {
|
|
63
|
+
animation: none;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/system/density.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import * as React from "react";
|
|
|
2
2
|
import { Density } from "@cueplusplus/tokens";
|
|
3
3
|
//#region src/system/density.d.ts
|
|
4
4
|
interface DensityProps {
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Level to apply to this subtree: `ultra-compact`, `compact`, `normal`,
|
|
7
|
+
* `large` or `ultra-large`.
|
|
8
|
+
*/
|
|
6
9
|
density: Density;
|
|
7
10
|
/** Subtree that should render at `density`. */
|
|
8
11
|
children: React.ReactNode;
|
package/dist/system/density.js
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/system/density.tsx
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Density level every unstamped subtree falls back to.
|
|
7
|
+
*
|
|
8
|
+
* `compact`, which is the second rung and not the middle one. This is console
|
|
9
|
+
* software: the ladder is symmetric about `normal` for the sake of its names,
|
|
10
|
+
* but the level a screen gets when nobody chose is the dense one.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_DENSITY = "compact";
|
|
13
|
+
/** The five levels, as a runtime array for validating persisted input. */
|
|
8
14
|
const DENSITY_LEVELS = [
|
|
9
15
|
"ultra-compact",
|
|
16
|
+
"compact",
|
|
10
17
|
"normal",
|
|
11
|
-
"large"
|
|
18
|
+
"large",
|
|
19
|
+
"ultra-large"
|
|
12
20
|
];
|
|
13
21
|
/**
|
|
14
22
|
* Nearest ambient density. `null` means "nothing has stamped a level yet", which
|
package/dist/system/index.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ import { CuePortalFrame, CuePortalFrameProps, useCuePortalProps } from "./portal
|
|
|
4
4
|
import { DEFAULT_STORAGE_KEY, PersistedPreferences, prepaintScript } from "./prepaint.js";
|
|
5
5
|
import { ControlSize, useControlHeight, useDensity } from "./use-density.js";
|
|
6
6
|
import { useTheme } from "./use-theme.js";
|
|
7
|
-
import { Density as DensityLevel, Mode, ThemeName } from "@cueplusplus/tokens";
|
|
8
|
-
export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityLevel, type DensityProps, type Mode, type PersistedPreferences, type ResolvedMode, type ThemeContextValue, type ThemeName, ThemeProvider, type ThemeProviderProps, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
|
|
7
|
+
import { Density as DensityLevel, FontName, Mode, ThemeName } from "@cueplusplus/tokens";
|
|
8
|
+
export { type ControlSize, CuePortalFrame, type CuePortalFrameProps, DEFAULT_STORAGE_KEY, Density, type DensityLevel, type DensityProps, type FontName, type Mode, type PersistedPreferences, type ResolvedMode, type ThemeContextValue, type ThemeName, ThemeProvider, type ThemeProviderProps, prepaintScript, useControlHeight, useCuePortalProps, useDensity, useTheme };
|
package/dist/system/portal.d.ts
CHANGED
|
@@ -2,11 +2,11 @@ import * as React from "react";
|
|
|
2
2
|
//#region src/system/portal.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Props to spread onto a Base UI `Portal` part so the portal root inherits the
|
|
5
|
-
* caller's theme, density and mode.
|
|
5
|
+
* caller's theme, density, font pairing and mode.
|
|
6
6
|
*
|
|
7
7
|
* Returns a `container` element appended to `document.body` and stamped with
|
|
8
|
-
* `data-theme` / `data-density` / `data-
|
|
9
|
-
* from the nearest provider and density island. The container is
|
|
8
|
+
* `data-theme` / `data-density` / `data-font` / `data-mode` (plus
|
|
9
|
+
* `--cue-font-scale`) copied from the nearest provider and density island. The container is
|
|
10
10
|
* `display: contents`, so it changes no layout and creates no containing block —
|
|
11
11
|
* fixed-position overlay content behaves exactly as if it were a direct child of
|
|
12
12
|
* `<body>`.
|
|
@@ -38,8 +38,8 @@ interface CuePortalFrameProps {
|
|
|
38
38
|
* chosen (a third-party overlay, or content already portaled by something else).
|
|
39
39
|
*
|
|
40
40
|
* Wraps `children` in a `display: contents` element carrying the same
|
|
41
|
-
* `data-theme` / `data-density` / `data-
|
|
42
|
-
* {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
|
|
41
|
+
* `data-theme` / `data-density` / `data-font` / `data-mode` stamp and
|
|
42
|
+
* `--cue-font-scale` as {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
|
|
43
43
|
* element per overlay instead of one per render tree — and reach for this only
|
|
44
44
|
* when the container prop is not available.
|
|
45
45
|
*
|
package/dist/system/portal.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DensityContext } from "./density.js";
|
|
|
4
4
|
import { FontScaleContext, ThemeContext } from "./theme-provider.js";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
import { DEFAULT_FONT } from "@cueplusplus/tokens";
|
|
7
8
|
//#region src/system/portal.tsx
|
|
8
9
|
const DEFAULT_THEME = "cue";
|
|
9
10
|
const DEFAULT_RESOLVED_MODE = "dark";
|
|
@@ -23,18 +24,19 @@ function usePortalStamp() {
|
|
|
23
24
|
const fontScale = React.useContext(FontScaleContext);
|
|
24
25
|
return {
|
|
25
26
|
theme: theme?.theme ?? DEFAULT_THEME,
|
|
26
|
-
density: island ?? theme?.density ?? "
|
|
27
|
+
density: island ?? theme?.density ?? "compact",
|
|
28
|
+
font: theme?.font ?? DEFAULT_FONT,
|
|
27
29
|
mode: theme?.resolvedMode ?? DEFAULT_RESOLVED_MODE,
|
|
28
30
|
fontScale
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Props to spread onto a Base UI `Portal` part so the portal root inherits the
|
|
33
|
-
* caller's theme, density and mode.
|
|
35
|
+
* caller's theme, density, font pairing and mode.
|
|
34
36
|
*
|
|
35
37
|
* Returns a `container` element appended to `document.body` and stamped with
|
|
36
|
-
* `data-theme` / `data-density` / `data-
|
|
37
|
-
* from the nearest provider and density island. The container is
|
|
38
|
+
* `data-theme` / `data-density` / `data-font` / `data-mode` (plus
|
|
39
|
+
* `--cue-font-scale`) copied from the nearest provider and density island. The container is
|
|
38
40
|
* `display: contents`, so it changes no layout and creates no containing block —
|
|
39
41
|
* fixed-position overlay content behaves exactly as if it were a direct child of
|
|
40
42
|
* `<body>`.
|
|
@@ -75,6 +77,7 @@ function useCuePortalProps() {
|
|
|
75
77
|
container,
|
|
76
78
|
stamp.theme,
|
|
77
79
|
stamp.density,
|
|
80
|
+
stamp.font,
|
|
78
81
|
stamp.mode,
|
|
79
82
|
stamp.fontScale
|
|
80
83
|
]);
|
|
@@ -84,6 +87,7 @@ function useCuePortalProps() {
|
|
|
84
87
|
function applyStamp(element, stamp) {
|
|
85
88
|
element.setAttribute("data-theme", stamp.theme);
|
|
86
89
|
element.setAttribute("data-density", stamp.density);
|
|
90
|
+
element.setAttribute("data-font", stamp.font);
|
|
87
91
|
element.setAttribute("data-mode", stamp.mode);
|
|
88
92
|
if (stamp.fontScale === null) element.style.removeProperty(FONT_SCALE_PROPERTY);
|
|
89
93
|
else element.style.setProperty(FONT_SCALE_PROPERTY, String(stamp.fontScale));
|
|
@@ -93,8 +97,8 @@ function applyStamp(element, stamp) {
|
|
|
93
97
|
* chosen (a third-party overlay, or content already portaled by something else).
|
|
94
98
|
*
|
|
95
99
|
* Wraps `children` in a `display: contents` element carrying the same
|
|
96
|
-
* `data-theme` / `data-density` / `data-
|
|
97
|
-
* {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
|
|
100
|
+
* `data-theme` / `data-density` / `data-font` / `data-mode` stamp and
|
|
101
|
+
* `--cue-font-scale` as {@link useCuePortalProps}. Prefer the hook — a stamped container costs one
|
|
98
102
|
* element per overlay instead of one per render tree — and reach for this only
|
|
99
103
|
* when the container prop is not available.
|
|
100
104
|
*
|
|
@@ -112,6 +116,7 @@ function CuePortalFrame({ children, className, style }) {
|
|
|
112
116
|
"data-cue-portal": "",
|
|
113
117
|
"data-theme": stamp.theme,
|
|
114
118
|
"data-density": stamp.density,
|
|
119
|
+
"data-font": stamp.font,
|
|
115
120
|
"data-mode": stamp.mode,
|
|
116
121
|
className,
|
|
117
122
|
style: frameStyle,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ThemeName } from "@cueplusplus/tokens";
|
|
1
|
+
import { Density, FontName, ThemeName } from "@cueplusplus/tokens";
|
|
2
2
|
//#region src/system/prepaint.d.ts
|
|
3
3
|
/** localStorage key the ThemeProvider and the pre-paint script share by default. */
|
|
4
4
|
declare const DEFAULT_STORAGE_KEY = "cue-ui";
|
|
@@ -10,17 +10,19 @@ interface PersistedPreferences {
|
|
|
10
10
|
density?: string;
|
|
11
11
|
/** Last mode the user picked, including the literal `"system"`. */
|
|
12
12
|
mode?: string;
|
|
13
|
+
/** Last font pairing the user picked. */
|
|
14
|
+
font?: string;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Build the blocking inline script that stamps `data-theme`, `data-density`,
|
|
16
|
-
* `data-mode` and `color-scheme` on `<html>` from localStorage
|
|
17
|
-
* paint.
|
|
18
|
+
* `data-font`, `data-mode` and `color-scheme` on `<html>` from localStorage
|
|
19
|
+
* before the first paint.
|
|
18
20
|
*
|
|
19
21
|
* Render it as `<script dangerouslySetInnerHTML={{ __html: prepaintScript() }} />`
|
|
20
22
|
* in `<head>`, above everything else. The output never contains `</script>` and
|
|
21
23
|
* never throws: private-mode localStorage failures degrade to the dark-first
|
|
22
|
-
* defaults (`cue` / `
|
|
23
|
-
* `@cueplusplus/tokens/theme.css` already paints.
|
|
24
|
+
* defaults (`cue` / `compact` / `system` / `dark`), which are also what bare
|
|
25
|
+
* `:root` in `@cueplusplus/tokens/theme.css` already paints.
|
|
24
26
|
*
|
|
25
27
|
* @param storageKey - localStorage key to read. Must match the `storageKey`
|
|
26
28
|
* passed to `<ThemeProvider>`. Defaults to {@link DEFAULT_STORAGE_KEY}.
|
|
@@ -28,11 +30,16 @@ interface PersistedPreferences {
|
|
|
28
30
|
* Must match the `theme` passed to `<ThemeProvider>`, or the first paint
|
|
29
31
|
* dresses `<html>` in one preset while the app renders in another. Defaults to
|
|
30
32
|
* `"cue"`.
|
|
33
|
+
* @param defaultDensity - Density level to stamp for a visitor with nothing
|
|
34
|
+
* persisted yet. Same rule as `defaultTheme`: it must match the `density`
|
|
35
|
+
* passed to `<ThemeProvider>` or the first frame is drawn at one rung and
|
|
36
|
+
* every frame after it at another. Defaults to `"compact"`, the library's own
|
|
37
|
+
* fallback, which is what an app that passes no `density` renders at.
|
|
31
38
|
* @returns One line of JavaScript, safe to inline verbatim.
|
|
32
39
|
* @example
|
|
33
|
-
* // An app whose provider is <ThemeProvider theme="terminal">:
|
|
34
|
-
* prepaintScript(DEFAULT_STORAGE_KEY, "terminal");
|
|
40
|
+
* // An app whose provider is <ThemeProvider theme="terminal" density="normal">:
|
|
41
|
+
* prepaintScript(DEFAULT_STORAGE_KEY, "terminal", "normal");
|
|
35
42
|
*/
|
|
36
|
-
declare function prepaintScript(storageKey?: string, defaultTheme?: ThemeName): string;
|
|
43
|
+
declare function prepaintScript(storageKey?: string, defaultTheme?: ThemeName, defaultDensity?: Density, defaultFont?: FontName): string;
|
|
37
44
|
//#endregion
|
|
38
45
|
export { DEFAULT_STORAGE_KEY, PersistedPreferences, prepaintScript };
|
package/dist/system/prepaint.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
+
import { DEFAULT_DENSITY, DEFAULT_FONT, DENSITIES, FONTS } 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)
|
|
17
|
+
return scriptSafe(JSON.stringify(value));
|
|
10
18
|
}
|
|
11
19
|
/**
|
|
12
20
|
* Build the blocking inline script that stamps `data-theme`, `data-density`,
|
|
13
|
-
* `data-mode` and `color-scheme` on `<html>` from localStorage
|
|
14
|
-
* paint.
|
|
21
|
+
* `data-font`, `data-mode` and `color-scheme` on `<html>` from localStorage
|
|
22
|
+
* before the first paint.
|
|
15
23
|
*
|
|
16
24
|
* Render it as `<script dangerouslySetInnerHTML={{ __html: prepaintScript() }} />`
|
|
17
25
|
* in `<head>`, above everything else. The output never contains `<\/script>` and
|
|
18
26
|
* never throws: private-mode localStorage failures degrade to the dark-first
|
|
19
|
-
* defaults (`cue` / `
|
|
20
|
-
* `@cueplusplus/tokens/theme.css` already paints.
|
|
27
|
+
* defaults (`cue` / `compact` / `system` / `dark`), which are also what bare
|
|
28
|
+
* `:root` in `@cueplusplus/tokens/theme.css` already paints.
|
|
21
29
|
*
|
|
22
30
|
* @param storageKey - localStorage key to read. Must match the `storageKey`
|
|
23
31
|
* passed to `<ThemeProvider>`. Defaults to {@link DEFAULT_STORAGE_KEY}.
|
|
@@ -25,13 +33,22 @@ function jsStringLiteral(value) {
|
|
|
25
33
|
* Must match the `theme` passed to `<ThemeProvider>`, or the first paint
|
|
26
34
|
* dresses `<html>` in one preset while the app renders in another. Defaults to
|
|
27
35
|
* `"cue"`.
|
|
36
|
+
* @param defaultDensity - Density level to stamp for a visitor with nothing
|
|
37
|
+
* persisted yet. Same rule as `defaultTheme`: it must match the `density`
|
|
38
|
+
* passed to `<ThemeProvider>` or the first frame is drawn at one rung and
|
|
39
|
+
* every frame after it at another. Defaults to `"compact"`, the library's own
|
|
40
|
+
* fallback, which is what an app that passes no `density` renders at.
|
|
28
41
|
* @returns One line of JavaScript, safe to inline verbatim.
|
|
29
42
|
* @example
|
|
30
|
-
* // An app whose provider is <ThemeProvider theme="terminal">:
|
|
31
|
-
* prepaintScript(DEFAULT_STORAGE_KEY, "terminal");
|
|
43
|
+
* // An app whose provider is <ThemeProvider theme="terminal" density="normal">:
|
|
44
|
+
* prepaintScript(DEFAULT_STORAGE_KEY, "terminal", "normal");
|
|
32
45
|
*/
|
|
33
|
-
function prepaintScript(storageKey = DEFAULT_STORAGE_KEY, defaultTheme = "cue") {
|
|
34
|
-
|
|
46
|
+
function prepaintScript(storageKey = DEFAULT_STORAGE_KEY, defaultTheme = "cue", defaultDensity = DEFAULT_DENSITY, defaultFont = DEFAULT_FONT) {
|
|
47
|
+
const key = jsStringLiteral(storageKey);
|
|
48
|
+
const fallbackTheme = jsStringLiteral(defaultTheme);
|
|
49
|
+
const fallbackDensity = jsStringLiteral(defaultDensity);
|
|
50
|
+
const fallbackFont = jsStringLiteral(defaultFont);
|
|
51
|
+
return `!function(){try{var k=${key},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:${fallbackTheme};var d=${scriptSafe(JSON.stringify([...DENSITIES]))}.indexOf(s.density)>-1?s.density:${fallbackDensity};var f=${scriptSafe(JSON.stringify([...FONTS]))}.indexOf(s.font)>-1?s.font:${fallbackFont};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-font",f);e.setAttribute("data-mode",m);e.style.colorScheme=m}catch(_){}}()`;
|
|
35
52
|
}
|
|
36
53
|
//#endregion
|
|
37
54
|
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,18 +21,36 @@ 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;
|
|
22
26
|
}
|
|
23
27
|
interface ThemeProviderProps {
|
|
24
28
|
/** Initial theme preset. Defaults to `"cue"`. Changing it after mount adopts the new value. */
|
|
25
29
|
theme?: ThemeName;
|
|
26
|
-
/** Initial density level. Defaults to `"
|
|
30
|
+
/** Initial density level. Defaults to `"compact"`. */
|
|
27
31
|
density?: Density;
|
|
28
32
|
/** Initial mode. Defaults to `"dark"`; `"system"` tracks `prefers-color-scheme`. */
|
|
29
33
|
mode?: Mode;
|
|
34
|
+
/**
|
|
35
|
+
* Initial font pairing. Defaults to `"system"` — the platform's own faces,
|
|
36
|
+
* nothing downloaded, and the theme keeps whatever monospace it authored.
|
|
37
|
+
*
|
|
38
|
+
* A pairing that needs delivering is a set of *names*: this library ships no
|
|
39
|
+
* font files, and a pairing nobody delivers falls through its stack to the
|
|
40
|
+
* platform rather than failing. See `FONT_PAIRINGS[font].faces` for the
|
|
41
|
+
* custom properties an app assigns to make one resolve.
|
|
42
|
+
*
|
|
43
|
+
* **Root-level only.** There is no font island: a nested provider forwards
|
|
44
|
+
* this axis to the root, ignores this prop, and its `setFont` drives the root.
|
|
45
|
+
* A page that changed face halfway down is a page with a bug, and a specimen
|
|
46
|
+
* that genuinely wants one — a picker row, a docs page showing all eight —
|
|
47
|
+
* needs nothing from this library but `data-font` on a `<div>`.
|
|
48
|
+
*/
|
|
49
|
+
font?: FontName;
|
|
30
50
|
/** Multiplier on every text token, published as `--cue-font-scale`. Defaults to `1`. */
|
|
31
51
|
fontScale?: number;
|
|
32
52
|
/**
|
|
33
|
-
* localStorage key holding `{ theme, density, mode }`. Defaults to `"cue-ui"`.
|
|
53
|
+
* localStorage key holding `{ theme, density, font, mode }`. Defaults to `"cue-ui"`.
|
|
34
54
|
* Only the outermost provider reads or writes it — a nested provider is an
|
|
35
55
|
* island whose explicit props are the point, not a second preference store.
|
|
36
56
|
*/
|
|
@@ -46,27 +66,33 @@ interface ThemeProviderProps {
|
|
|
46
66
|
}
|
|
47
67
|
/**
|
|
48
68
|
* Root of the theme system: stamps `data-theme`, `data-density` and `data-mode`
|
|
49
|
-
* so the token layer resolves,
|
|
50
|
-
* persisted user preference.
|
|
69
|
+
* so the token layer resolves, puts `data-font` on `<html>`, publishes
|
|
70
|
+
* `--cue-font-scale`, and owns the persisted user preference.
|
|
51
71
|
*
|
|
52
72
|
* 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
|
|
73
|
+
* (or the single child when `asChild`). The `theme`/`density`/`font`/`mode` props are
|
|
54
74
|
* *initial* values — the provider holds the state so `setTheme` and friends can
|
|
55
75
|
* drive it — but a changed prop is adopted after mount, so a controlling parent
|
|
56
76
|
* still works.
|
|
57
77
|
*
|
|
78
|
+
* The font pairing is the one axis that never lands on this element: it has no
|
|
79
|
+
* island form, `<html>` is its only home, and restating it here would shadow the
|
|
80
|
+
* pre-paint stamp for the whole page on the first frame. See the `stamp` object
|
|
81
|
+
* below.
|
|
82
|
+
*
|
|
58
83
|
* Persistence and the pre-paint contract: setters write
|
|
59
|
-
* `{ theme, density, mode }` to `localStorage[storageKey]`, which is exactly what
|
|
84
|
+
* `{ theme, density, font, mode }` to `localStorage[storageKey]`, which is exactly what
|
|
60
85
|
* {@link prepaintScript} reads to stamp `<html>` before the first paint. The
|
|
61
86
|
* outermost provider keeps `<html>` in sync while the app runs (and restores the
|
|
62
87
|
* previous stamp on unmount) so the page ground, UA scrollbars and form controls
|
|
63
|
-
* follow the theme; nested providers never touch `<html
|
|
88
|
+
* follow the theme; nested providers never touch `<html>`, and never own the
|
|
89
|
+
* font pairing — `useTheme().font` and `setFont` inside one are the root's.
|
|
64
90
|
*
|
|
65
91
|
* @example
|
|
66
|
-
* <ThemeProvider theme="terminal" density="ultra-compact" mode="system">
|
|
92
|
+
* <ThemeProvider theme="terminal" density="ultra-compact" font="plex" mode="system">
|
|
67
93
|
* <App />
|
|
68
94
|
* </ThemeProvider>
|
|
69
95
|
*/
|
|
70
|
-
declare function ThemeProvider({ theme: themeProp, density: densityProp, mode: modeProp, fontScale, storageKey, asChild, className, style: styleProp, children }: ThemeProviderProps): React.JSX.Element;
|
|
96
|
+
declare function ThemeProvider({ theme: themeProp, density: densityProp, mode: modeProp, font: fontProp, fontScale, storageKey, asChild, className, style: styleProp, children }: ThemeProviderProps): React.JSX.Element;
|
|
71
97
|
//#endregion
|
|
72
98
|
export { ResolvedMode, ThemeContextValue, ThemeProvider, ThemeProviderProps };
|