@cueplusplus/ui 0.18.2 → 0.19.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 +61 -0
- package/dist/brand/_motif-seed.js +20 -0
- package/dist/brand/gloss-object.d.ts +23 -0
- package/dist/brand/gloss-object.js +108 -0
- package/dist/brand/halftone.d.ts +23 -0
- package/dist/brand/halftone.js +54 -0
- package/dist/brand/index.d.ts +6 -1
- package/dist/brand/motif.d.ts +32 -0
- package/dist/brand/motif.js +71 -0
- package/dist/brand/particle-mark.d.ts +29 -0
- package/dist/brand/particle-mark.js +137 -0
- package/dist/brand/ribbon.d.ts +20 -0
- package/dist/brand/ribbon.js +54 -0
- package/dist/chrome/announcement-bar.d.ts +30 -0
- package/dist/chrome/announcement-bar.js +38 -0
- package/dist/chrome/app-bar.d.ts +4 -1
- package/dist/chrome/app-bar.js +7 -2
- package/dist/chrome/app-shell.js +8 -1
- package/dist/chrome/band.d.ts +3 -1
- package/dist/chrome/band.js +4 -3
- package/dist/chrome/hero.d.ts +61 -0
- package/dist/chrome/hero.js +108 -0
- package/dist/chrome/index.d.ts +6 -1
- package/dist/chrome/index.js +6 -1
- package/dist/chrome/keyword.d.ts +22 -0
- package/dist/chrome/keyword.js +28 -0
- package/dist/chrome/logo-strip.d.ts +23 -0
- package/dist/chrome/logo-strip.js +43 -0
- package/dist/chrome/page-shell.js +13 -1
- package/dist/chrome/stat-band.d.ts +32 -0
- package/dist/chrome/stat-band.js +39 -0
- package/dist/dmx/channel-matrix.js +5 -3
- package/dist/dmx/dmx-strip.js +5 -3
- package/dist/elements/animated-number.js +4 -1
- package/dist/elements/replay.d.ts +1 -1
- package/dist/elements/replay.js +5 -2
- package/dist/elements/scroll-anchor.js +4 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +14 -2
- package/dist/instruments/index.js +1 -1
- package/dist/instruments/stat.js +1 -1
- package/dist/instruments/terminal-frame.js +5 -2
- package/dist/layout/grid.js +4 -2
- package/dist/layout/preview.js +4 -188
- package/dist/layout/sidebar.js +88 -52
- package/dist/overlays/_surface.d.ts +2 -2
- package/dist/primitives/button.js +22 -3
- package/dist/primitives/chip.js +62 -6
- package/dist/styles.css +26 -0
- package/dist/system/_scoped-pairing.js +18 -0
- package/dist/system/_stage-scope.js +199 -0
- package/dist/system/_style-check.js +1 -1
- package/dist/system/_style-chip.js +1 -1
- package/dist/system/_style-group-label.js +7 -3
- package/dist/system/_style-hero-title.js +41 -0
- package/dist/system/_style-kbd.js +1 -1
- package/dist/system/_style-label-ink.js +12 -5
- package/dist/system/_style-menu.js +1 -1
- package/dist/system/_style-page-title.js +1 -1
- package/dist/system/_style-segmented-control.js +1 -1
- package/dist/system/_style-skeleton.js +1 -1
- package/dist/system/_style-tab.js +1 -1
- package/dist/system/font-scope.d.ts +38 -0
- package/dist/system/font-scope.js +69 -0
- package/dist/system/index.d.ts +3 -1
- package/dist/system/index.js +3 -1
- package/dist/system/portal.js +7 -1
- package/dist/system/still.d.ts +32 -0
- package/dist/system/still.js +47 -0
- package/manifest/components/alert-dialog.json +1 -1
- package/manifest/components/announcement-bar.json +82 -0
- package/manifest/components/app-bar.json +3 -2
- package/manifest/components/app-shell.json +3 -0
- package/manifest/components/autocomplete.json +1 -0
- package/manifest/components/band.json +2 -1
- package/manifest/components/bento.json +12 -12
- package/manifest/components/button.json +1 -0
- package/manifest/components/channel-matrix.json +1 -0
- package/manifest/components/chart-tooltip-content.json +1 -0
- package/manifest/components/checkbox-group.json +1 -1
- package/manifest/components/checkbox.json +1 -1
- package/manifest/components/chip.json +1 -0
- package/manifest/components/combobox.json +1 -0
- package/manifest/components/command-palette.json +1 -0
- package/manifest/components/context-menu.json +1 -1
- package/manifest/components/density.json +6 -2
- package/manifest/components/dialog.json +1 -1
- package/manifest/components/dmx-bar.json +1 -1
- package/manifest/components/dmx-strip.json +1 -1
- package/manifest/components/drawer.json +1 -1
- package/manifest/components/dropdown-menu.json +1 -0
- package/manifest/components/font-scope.json +81 -0
- package/manifest/components/footer.json +0 -1
- package/manifest/components/gloss-object.json +81 -0
- package/manifest/components/grid.json +6 -6
- package/manifest/components/halftone.json +79 -0
- package/manifest/components/hero.json +204 -0
- package/manifest/components/hover-card.json +1 -1
- package/manifest/components/kbd.json +1 -0
- package/manifest/components/keyword.json +67 -0
- package/manifest/components/ledger.json +4 -4
- package/manifest/components/logo-strip.json +93 -0
- package/manifest/components/markdown-text.json +2 -2
- package/manifest/components/menubar.json +1 -1
- package/manifest/components/motif.json +90 -0
- package/manifest/components/multi-select.json +1 -0
- package/manifest/components/navigation-menu.json +1 -0
- package/manifest/components/page-shell.json +5 -0
- package/manifest/components/particle-mark.json +75 -0
- package/manifest/components/popover.json +1 -1
- package/manifest/components/preview.json +9 -4
- package/manifest/components/radio-group.json +0 -1
- package/manifest/components/radio.json +0 -1
- package/manifest/components/ribbon.json +65 -0
- package/manifest/components/scrollable-tabs-list.json +1 -0
- package/manifest/components/segmented-control.json +1 -0
- package/manifest/components/sheet.json +1 -1
- package/manifest/components/sidebar.json +1 -0
- package/manifest/components/skeleton.json +1 -0
- package/manifest/components/stack.json +4 -4
- package/manifest/components/stat-band.json +68 -0
- package/manifest/components/stat.json +1 -1
- package/manifest/components/still-provider.json +60 -0
- package/manifest/components/switch.json +0 -1
- package/manifest/components/table.json +1 -1
- package/manifest/components/tabs.json +1 -0
- package/manifest/components/tags-input.json +1 -0
- package/manifest/components/theme-configurator.json +1 -0
- package/manifest/components/theme-provider.json +9 -4
- package/manifest/components/toast.json +1 -0
- package/manifest/components/tooltip.json +1 -1
- package/manifest/fixtures.json +18 -0
- package/manifest/manifest.json +267 -55
- package/manifest/tokens.json +91 -3
- package/package.json +4 -4
- package/dist/system/_paint-ground-pattern.js +0 -30
package/dist/layout/sidebar.js
CHANGED
|
@@ -51,6 +51,91 @@ const LANDMARK_ELEMENTS = {
|
|
|
51
51
|
complementary: "aside"
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
|
+
* The navigation column: a `<nav>` on a hairline, with a collapse valve and a
|
|
55
|
+
* scroller of its own.
|
|
56
|
+
*
|
|
57
|
+
* Width is an inline length rather than a class, because it is the one piece of
|
|
58
|
+
* console geometry a user is expected to change and persist; the transition
|
|
59
|
+
* between the two widths is CSS and switches off under `prefers-reduced-motion`.
|
|
60
|
+
*
|
|
61
|
+
* **The column scrolls, the rail does not.** Thirty rows of navigation is an
|
|
62
|
+
* ordinary amount for a documentation site or a settings screen, and a column
|
|
63
|
+
* that cannot scroll simply loses the last of them off the bottom of the frame.
|
|
64
|
+
* So the sections go in an inner scroller and the root keeps the height — which
|
|
65
|
+
* is also what saves {@link SidebarRail}: it is `absolute inset-y-0`, its
|
|
66
|
+
* containing block is the root rather than the scroller, and an absolutely
|
|
67
|
+
* positioned box whose containing block is outside a scroll container neither
|
|
68
|
+
* scrolls with it nor is clipped by it. Put the scroller on the root instead
|
|
69
|
+
* and the collapse rail slides away with the third screenful.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* <Sidebar.Root aria-label="Main">
|
|
73
|
+
* <Sidebar.Section label="Console">
|
|
74
|
+
* <Sidebar.Item icon={Sliders} active>Patch</Sidebar.Item>
|
|
75
|
+
* <Sidebar.Item icon={List}>Cues</Sidebar.Item>
|
|
76
|
+
* </Sidebar.Section>
|
|
77
|
+
* <Sidebar.Rail />
|
|
78
|
+
* </Sidebar.Root>
|
|
79
|
+
*/
|
|
80
|
+
const SidebarRoot = React.forwardRef(function SidebarRoot({ className, landmark = "navigation", collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, width = "14rem", collapsedWidth = "3rem", style, children, ...elementProps }, ref) {
|
|
81
|
+
const Element = LANDMARK_ELEMENTS[landmark];
|
|
82
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultCollapsed);
|
|
83
|
+
const collapsed = collapsedProp ?? uncontrolled;
|
|
84
|
+
const toggle = React.useCallback(() => {
|
|
85
|
+
const next = !(collapsedProp ?? uncontrolled);
|
|
86
|
+
if (collapsedProp === void 0) setUncontrolled(next);
|
|
87
|
+
onCollapsedChange?.(next);
|
|
88
|
+
}, [
|
|
89
|
+
collapsedProp,
|
|
90
|
+
uncontrolled,
|
|
91
|
+
onCollapsedChange
|
|
92
|
+
]);
|
|
93
|
+
const context = React.useMemo(() => ({
|
|
94
|
+
collapsed,
|
|
95
|
+
toggle
|
|
96
|
+
}), [collapsed, toggle]);
|
|
97
|
+
return /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
98
|
+
value: context,
|
|
99
|
+
children: /* @__PURE__ */ jsx(Element, {
|
|
100
|
+
ref,
|
|
101
|
+
"data-slot": "sidebar",
|
|
102
|
+
"data-collapsed": collapsed ? "" : void 0,
|
|
103
|
+
className: cn("group/sidebar relative flex h-full min-h-0 shrink-0 flex-col border-r border-border bg-surface-1 transition-[width] duration-150 ease-out motion-reduce:transition-none", "[counter-reset:cue-rail]", className),
|
|
104
|
+
style: {
|
|
105
|
+
width: collapsed ? collapsedWidth : width,
|
|
106
|
+
...style
|
|
107
|
+
},
|
|
108
|
+
...elementProps,
|
|
109
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
110
|
+
"data-slot": "sidebar-scroller",
|
|
111
|
+
className: "flex min-h-0 flex-1 flex-col gap-(--cue-space-4) overflow-y-auto py-(--cue-space-4)",
|
|
112
|
+
children
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
/** One group of items, under an optional caption. */
|
|
118
|
+
const SidebarSection = React.forwardRef(function SidebarSection({ className, label, children, ...elementProps }, ref) {
|
|
119
|
+
const { collapsed } = useSidebar();
|
|
120
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
121
|
+
ref,
|
|
122
|
+
"data-slot": "sidebar-section",
|
|
123
|
+
className: cn("flex flex-col gap-(--cue-space-1) px-(--cue-space-2)", className),
|
|
124
|
+
...elementProps,
|
|
125
|
+
children: [label === void 0 || collapsed ? null : /* @__PURE__ */ jsx("div", {
|
|
126
|
+
"data-slot": "sidebar-section-label",
|
|
127
|
+
className: cn("flex h-control-sm items-center px-(--cue-space-2)", LABEL_SIZE, LABEL_INK_SUBTLE, LABEL_WEIGHT, LABEL_FACE, LABEL_TRACKING, LABEL_CASE),
|
|
128
|
+
children: label
|
|
129
|
+
}), children]
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* A style set's numbered rail (structural Looks, spec 2026-10-02 §3.5): every item
|
|
134
|
+
* counts, and draws its number before the icon only when the set derives
|
|
135
|
+
* `style-rail-counter-content` — `none` unset, which generates no box at all.
|
|
136
|
+
*/
|
|
137
|
+
const RAIL_COUNTER = "[counter-increment:cue-rail] before:content-[var(--cue-style-rail-counter-content,none)] before:shrink-0 before:font-mono before:text-(length:--cue-text-label) before:text-fg-subtle";
|
|
138
|
+
/**
|
|
54
139
|
* The console's navigation column: sections of items, a scroller of its own,
|
|
55
140
|
* and a rail that collapses the whole thing to icons.
|
|
56
141
|
*
|
|
@@ -63,57 +148,8 @@ const LANDMARK_ELEMENTS = {
|
|
|
63
148
|
* </Sidebar.Root>
|
|
64
149
|
*/
|
|
65
150
|
const Sidebar = {
|
|
66
|
-
Root:
|
|
67
|
-
|
|
68
|
-
const [uncontrolled, setUncontrolled] = React.useState(defaultCollapsed);
|
|
69
|
-
const collapsed = collapsedProp ?? uncontrolled;
|
|
70
|
-
const toggle = React.useCallback(() => {
|
|
71
|
-
const next = !(collapsedProp ?? uncontrolled);
|
|
72
|
-
if (collapsedProp === void 0) setUncontrolled(next);
|
|
73
|
-
onCollapsedChange?.(next);
|
|
74
|
-
}, [
|
|
75
|
-
collapsedProp,
|
|
76
|
-
uncontrolled,
|
|
77
|
-
onCollapsedChange
|
|
78
|
-
]);
|
|
79
|
-
const context = React.useMemo(() => ({
|
|
80
|
-
collapsed,
|
|
81
|
-
toggle
|
|
82
|
-
}), [collapsed, toggle]);
|
|
83
|
-
return /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
84
|
-
value: context,
|
|
85
|
-
children: /* @__PURE__ */ jsx(Element, {
|
|
86
|
-
ref,
|
|
87
|
-
"data-slot": "sidebar",
|
|
88
|
-
"data-collapsed": collapsed ? "" : void 0,
|
|
89
|
-
className: cn("group/sidebar relative flex h-full min-h-0 shrink-0 flex-col border-r border-border bg-surface-1 transition-[width] duration-150 ease-out motion-reduce:transition-none", className),
|
|
90
|
-
style: {
|
|
91
|
-
width: collapsed ? collapsedWidth : width,
|
|
92
|
-
...style
|
|
93
|
-
},
|
|
94
|
-
...elementProps,
|
|
95
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
96
|
-
"data-slot": "sidebar-scroller",
|
|
97
|
-
className: "flex min-h-0 flex-1 flex-col gap-(--cue-space-4) overflow-y-auto py-(--cue-space-4)",
|
|
98
|
-
children
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
});
|
|
102
|
-
}),
|
|
103
|
-
Section: React.forwardRef(function SidebarSection({ className, label, children, ...elementProps }, ref) {
|
|
104
|
-
const { collapsed } = useSidebar();
|
|
105
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
106
|
-
ref,
|
|
107
|
-
"data-slot": "sidebar-section",
|
|
108
|
-
className: cn("flex flex-col gap-(--cue-space-1) px-(--cue-space-2)", className),
|
|
109
|
-
...elementProps,
|
|
110
|
-
children: [label === void 0 || collapsed ? null : /* @__PURE__ */ jsx("div", {
|
|
111
|
-
"data-slot": "sidebar-section-label",
|
|
112
|
-
className: cn("flex h-control-sm items-center px-(--cue-space-2)", LABEL_SIZE, LABEL_INK_SUBTLE, LABEL_WEIGHT, LABEL_FACE, LABEL_TRACKING, LABEL_CASE),
|
|
113
|
-
children: label
|
|
114
|
-
}), children]
|
|
115
|
-
});
|
|
116
|
-
}),
|
|
151
|
+
Root: SidebarRoot,
|
|
152
|
+
Section: SidebarSection,
|
|
117
153
|
Item: React.forwardRef(function SidebarItem({ className, icon: Icon, active = false, badge, render, type, children, ...elementProps }, ref) {
|
|
118
154
|
return useRender({
|
|
119
155
|
render,
|
|
@@ -125,7 +161,7 @@ const Sidebar = {
|
|
|
125
161
|
"data-slot": "sidebar-item",
|
|
126
162
|
"data-active": active ? "" : void 0,
|
|
127
163
|
"aria-current": active ? "page" : void 0,
|
|
128
|
-
className: cn(`relative flex h-control-md w-full cursor-pointer items-center gap-(--cue-space-3) rounded-control border-0 bg-transparent px-(--cue-space-3) text-left font-mono text-(length:--cue-text-ui) no-underline transition-colors duration-150 outline-none select-none hover:bg-surface-2 hover:text-fg ${FOCUS_RING_INSET} ${NAV_ITEM} disabled:pointer-events-none disabled:opacity-40`, className),
|
|
164
|
+
className: cn(`relative flex h-control-md w-full cursor-pointer items-center gap-(--cue-space-3) rounded-control border-0 bg-transparent px-(--cue-space-3) text-left font-mono text-(length:--cue-text-ui) no-underline transition-colors duration-150 outline-none select-none hover:bg-surface-2 hover:text-fg ${FOCUS_RING_INSET} ${NAV_ITEM} ${RAIL_COUNTER} disabled:pointer-events-none disabled:opacity-40`, className),
|
|
129
165
|
children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
130
166
|
Icon === void 0 ? null : /* @__PURE__ */ jsx(Icon, {
|
|
131
167
|
"aria-hidden": "true",
|
|
@@ -117,9 +117,9 @@ declare const overlayFooterClasses = "flex items-center justify-end gap-(--cue-s
|
|
|
117
117
|
* and the highlight are the style set's, with those as their defaults
|
|
118
118
|
* (`_style-menu.ts`).
|
|
119
119
|
*/
|
|
120
|
-
declare const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-[var(--cue-style-menu-item-radius,calc(var(--cue-radius-control)*var(--cue-radius-scale)))] px-(--cue-space-3) font-[family-name:var(--cue-style-control-face,var(--cue-font-mono))] text-(length:--cue-text-ui) text-[color:var(--cue-style-quiet-ink,var(--cue-fg-muted))] data-[highlighted]:bg-[color:var(--cue-style-menu-highlight,var(--cue-accent-soft))] outline-none select-none data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--cue-style-row-disabled-opacity,0.4)]";
|
|
120
|
+
declare const menuItemClasses = "flex h-control-sm cursor-default items-center gap-(--cue-space-2) rounded-[var(--cue-style-menu-item-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))] px-(--cue-space-3) font-[family-name:var(--cue-style-control-face,var(--cue-font-mono))] text-(length:--cue-text-ui) text-[color:var(--cue-style-quiet-ink,var(--cue-fg-muted))] data-[highlighted]:bg-[color:var(--cue-style-menu-highlight,var(--cue-accent-soft))] outline-none select-none data-[highlighted]:text-fg data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--cue-style-row-disabled-opacity,0.4)]";
|
|
121
121
|
/** The uppercase mono caption over a group of menu rows — by default. */
|
|
122
|
-
declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) py-[var(--cue-style-group-label-pad,0px)] font-[family-name:var(--cue-style-label-face,var(--cue-font-mono))] text-[length:var(--cue-style-group-label-size,var(--cue-style-label-size,var(--cue-text-label)))] tracking-[var(--cue-style-label-tracking,var(--cue-tracking-caps))] text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-
|
|
122
|
+
declare const menuLabelClasses = "flex h-control-sm items-center px-(--cue-space-3) py-[var(--cue-style-group-label-pad,0px)] font-[family-name:var(--cue-style-label-face,var(--cue-font-mono))] text-[length:var(--cue-style-group-label-size,var(--cue-style-label-size,var(--cue-text-label)))] tracking-[var(--cue-style-label-tracking,var(--cue-tracking-caps))] text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-muted)))] font-[number:var(--cue-style-label-weight,inherit)] [text-transform:var(--cue-style-label-case,uppercase)] select-none";
|
|
123
123
|
/** The hairline between menu groups. */
|
|
124
124
|
declare const menuSeparatorClasses = "my-(--cue-space-1) mx-[var(--cue-style-menu-separator-inset,0px)] h-px bg-border";
|
|
125
125
|
/** Padding of a menu popup — a single space unit so rows reach the rim. */
|
|
@@ -40,6 +40,25 @@ import { useRender } from "@base-ui/react/use-render";
|
|
|
40
40
|
*/
|
|
41
41
|
const buttonChassis = `inline-flex select-none items-center justify-center ${CONTROL_GAP} whitespace-nowrap rounded-control ${CONTROL_FACE} ${CONTROL_WEIGHT} ${CONTROL_TRACKING} ${CONTROL_TRIM} ${CONTROL_CASE} transition-colors duration-150 outline-none ${FOCUS_RING_PAINT} focus-visible:outline-offset-[var(--cue-style-focus-offset,1px)] disabled:pointer-events-none ${DISABLED_OPACITY}`;
|
|
42
42
|
/**
|
|
43
|
+
* A style set's chevron cut (structural Looks, spec 2026-10-02 §3.3), for the tones
|
|
44
|
+
* that are a filled shape: primary and danger always, secondary only where the set
|
|
45
|
+
* fills its ground (the build derives `--cue-style-secondary-cut` as `none` for a set
|
|
46
|
+
* whose secondary ground is `transparent`). Outline, ghost and danger-outline are
|
|
47
|
+
* drawn by their edge or their ink, and a hollow cut is geometry this chassis does
|
|
48
|
+
* not take on, so they keep the rectangle.
|
|
49
|
+
*
|
|
50
|
+
* The cut is a `clip-path` on the element: a filled tone's border is transparent,
|
|
51
|
+
* so nothing is cut but fill and exterior glow. Two things give way, on purpose:
|
|
52
|
+
* the glow outside a cut button is clipped (a set with a cut is worn with a pack
|
|
53
|
+
* that has none), and under `:focus-visible` and in forced colours the clip is
|
|
54
|
+
* removed, so the focus ring and the system border are never cut. The element stays
|
|
55
|
+
* a rectangle throughout: hit area, ring, `render`, refs, sizing and keyboard.
|
|
56
|
+
* Unset, the clip is `none` and the button renders exactly as before. The rules
|
|
57
|
+
* live in `styles.css` (`.cue-cut`, `.cue-cut-secondary`): as arbitrary utilities
|
|
58
|
+
* they were ~105 characters on every button a page renders.
|
|
59
|
+
*/
|
|
60
|
+
const CUT_FILLED = "cue-cut";
|
|
61
|
+
/**
|
|
43
62
|
* The shared colour vocabulary. `accent` is theme-controlled, so `primary`
|
|
44
63
|
* reads as white-on-black in `cue` and cyan-on-near-black in `terminal` with no
|
|
45
64
|
* per-theme branching here.
|
|
@@ -57,11 +76,11 @@ const buttonChassis = `inline-flex select-none items-center justify-center ${CON
|
|
|
57
76
|
* @internal Not part of the public API; imported by `icon-button.tsx`.
|
|
58
77
|
*/
|
|
59
78
|
const buttonTones = {
|
|
60
|
-
primary: `bg-accent-fill text-accent-fill-fg hover:bg-accent-fill-hover border border-transparent ${GLOW_CLASSES} ${DISABLED_TONES.primary}`,
|
|
61
|
-
secondary: `border ${SECONDARY_EDGE} ${SECONDARY_INK} hover:text-[color:var(--cue-style-secondary-hover-ink,var(--cue-fg))] hover:border-[color:var(--cue-style-secondary-hover-edge,var(--cue-border-strong))] ${SECONDARY_GROUND} hover:bg-[color:var(--cue-style-secondary-hover-ground,var(--cue-style-secondary-ground,var(--cue-surface-2)))] ${DISABLED_TONES.secondary}`,
|
|
79
|
+
primary: `bg-accent-fill [background-image:var(--cue-style-primary-fill,none)] text-accent-fill-fg hover:bg-accent-fill-hover hover:[background-image:none] disabled:[background-image:none] border border-transparent ${GLOW_CLASSES} ${CUT_FILLED} ${DISABLED_TONES.primary}`,
|
|
80
|
+
secondary: `border ${SECONDARY_EDGE} ${SECONDARY_INK} hover:text-[color:var(--cue-style-secondary-hover-ink,var(--cue-fg))] hover:border-[color:var(--cue-style-secondary-hover-edge,var(--cue-border-strong))] ${SECONDARY_GROUND} hover:bg-[color:var(--cue-style-secondary-hover-ground,var(--cue-style-secondary-ground,var(--cue-surface-2)))] cue-cut-secondary ${DISABLED_TONES.secondary}`,
|
|
62
81
|
ghost: `${GHOST_INK} hover:text-fg hover:bg-surface-2 border border-transparent ${DISABLED_TONES.ghost}`,
|
|
63
82
|
outline: `border ${OUTLINE_EDGE} ${OUTLINE_INK} ${OUTLINE_HOVER_EDGE} hover:text-accent bg-transparent ${OUTLINE_HOVER_GROUND} ${REST_SHADE_CLASS} ${DISABLED_TONES.outline}`,
|
|
64
|
-
danger: `bg-danger text-danger-fg hover:opacity-90 border border-transparent ${GLOW_DANGER_CLASSES} ${DISABLED_TONES.danger}`,
|
|
83
|
+
danger: `bg-danger text-danger-fg hover:opacity-90 border border-transparent ${GLOW_DANGER_CLASSES} ${CUT_FILLED} ${DISABLED_TONES.danger}`,
|
|
65
84
|
"danger-outline": `border text-danger bg-transparent hover:bg-danger/10 border-danger/40 ${DISABLED_TONES["danger-outline"]}`
|
|
66
85
|
};
|
|
67
86
|
/**
|
package/dist/primitives/chip.js
CHANGED
|
@@ -7,16 +7,72 @@ import { LABEL_FACE } from "../system/_style-label-face.js";
|
|
|
7
7
|
import { LABEL_WEIGHT } from "../system/_style-label-weight.js";
|
|
8
8
|
import * as React from "react";
|
|
9
9
|
import { jsx } from "react/jsx-runtime";
|
|
10
|
+
//#region src/primitives/chip.tsx
|
|
11
|
+
/**
|
|
12
|
+
* The tinted-chip recipe: `bg-<tone>/10 border-<tone>/35` and the tone's ink
|
|
13
|
+
* (mixed toward `fg`, see `CHIP_TONE_INK`), and the
|
|
14
|
+
* two quieter readings of it — `outline`, which drops the fill, and `tag`,
|
|
15
|
+
* which drops the fill and the voice with it.
|
|
16
|
+
*
|
|
17
|
+
* Every property that varies is emitted from exactly one place: one rim, one
|
|
18
|
+
* ink, one fill, one gutter, one type size, always. Emitting `bg-<tone>/10`
|
|
19
|
+
* from the tone map and then cancelling it with `bg-transparent` would leave
|
|
20
|
+
* two competing utilities in the class string, and `chipVariants` is exported —
|
|
21
|
+
* a caller who uses it without `cn` gets a raw string where the winner is
|
|
22
|
+
* decided by stylesheet order rather than by which utility was written last.
|
|
23
|
+
* `h-chip h-4` proves the point at the other end: `tailwind-merge` cannot even
|
|
24
|
+
* see that as a conflict, because `chip` is a token name and not a scale step.
|
|
25
|
+
* So nothing here cancels anything. What varies moves into `variants` or
|
|
26
|
+
* `compoundVariants` until only one of it is ever emitted.
|
|
27
|
+
*
|
|
28
|
+
* That is why `neutral` carries no rim of its own. The other six tones rim
|
|
29
|
+
* themselves at `/35` in all three variants, but the neutral line has two
|
|
30
|
+
* weights in this system: `--cue-border` is a box edge, and a chip is a box;
|
|
31
|
+
* `--cue-hair-strong` is the rule between things, and a tag is a mark on the
|
|
32
|
+
* page rather than a container. The rim is therefore a compound of tone and
|
|
33
|
+
* variant, exactly as the fill is.
|
|
34
|
+
*
|
|
35
|
+
* **The recipe reads the style set** (spec 2026-09-26-looks-fidelity §3.6,
|
|
36
|
+
* `system/_style-chip.ts`): the six tones' fill and rim are
|
|
37
|
+
* `color-mix(in oklab, var(--cue-<tone>) var(--cue-style-chip-fill, 10%), transparent)`
|
|
38
|
+
* and the same with `--cue-style-chip-edge` at `35%` — exactly `bg-<tone>/10`
|
|
39
|
+
* and `border-<tone>/35` under no set, and a set's own shares inside one. The
|
|
40
|
+
* case, tracking, radius, size and weight read `--cue-style-chip-case`,
|
|
41
|
+
* `-tracking`, `-radius`, `-size` and `--cue-style-label-weight` the same way.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* <span className={chipVariants({ tone: "danger", variant: "outline" })}>fault</span>
|
|
45
|
+
* @example
|
|
46
|
+
* <span className={chipVariants({ variant: "tag" })}>mcp</span>
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* A toned chip's ink: its tone, mixed 30% toward the page's own `fg`.
|
|
50
|
+
*
|
|
51
|
+
* A status ink is cut to be seen as a dot or a rim (3:1), and a chip sets it
|
|
52
|
+
* as text: small, uppercase and on its own 10% tint, where the bare tone fell
|
|
53
|
+
* to 4.2–4.4:1 in light under most themes and 3.6:1 under Kiln (structural
|
|
54
|
+
* Looks §4.1's rendered sweep, phase 5). Mixing toward `fg` raises it in both
|
|
55
|
+
* modes, because `fg` is the ink furthest from the ground in either, and
|
|
56
|
+
* keeps the hue a reader reads the tone by.
|
|
57
|
+
*/
|
|
58
|
+
const CHIP_TONE_INK = {
|
|
59
|
+
accent: "text-[color:color-mix(in_oklch,var(--cue-accent)_70%,var(--cue-fg))]",
|
|
60
|
+
ok: "text-[color:color-mix(in_oklch,var(--cue-ok)_70%,var(--cue-fg))]",
|
|
61
|
+
busy: "text-[color:color-mix(in_oklch,var(--cue-busy)_70%,var(--cue-fg))]",
|
|
62
|
+
warn: "text-[color:color-mix(in_oklch,var(--cue-warn)_70%,var(--cue-fg))]",
|
|
63
|
+
danger: "text-[color:color-mix(in_oklch,var(--cue-danger)_70%,var(--cue-fg))]",
|
|
64
|
+
info: "text-[color:color-mix(in_oklch,var(--cue-info)_70%,var(--cue-fg))]"
|
|
65
|
+
};
|
|
10
66
|
const chipVariants = cva(`inline-flex ${CHIP_HEIGHT} select-none items-center justify-center ${CHIP_GAP} whitespace-nowrap ${CHIP_RADIUS} border ${LABEL_FACE} ${LABEL_WEIGHT} ${CHIP_CASE} outline-none ${FOCUS_RING}`, {
|
|
11
67
|
variants: {
|
|
12
68
|
tone: {
|
|
13
69
|
neutral: CHIP_INK_NEUTRAL,
|
|
14
|
-
accent: `${CHIP_EDGE.accent}
|
|
15
|
-
ok: `${CHIP_EDGE.ok}
|
|
16
|
-
busy: `${CHIP_EDGE.busy}
|
|
17
|
-
warn: `${CHIP_EDGE.warn}
|
|
18
|
-
danger: `${CHIP_EDGE.danger}
|
|
19
|
-
info: `${CHIP_EDGE.info}
|
|
70
|
+
accent: `${CHIP_EDGE.accent} ${CHIP_TONE_INK.accent}`,
|
|
71
|
+
ok: `${CHIP_EDGE.ok} ${CHIP_TONE_INK.ok}`,
|
|
72
|
+
busy: `${CHIP_EDGE.busy} ${CHIP_TONE_INK.busy}`,
|
|
73
|
+
warn: `${CHIP_EDGE.warn} ${CHIP_TONE_INK.warn}`,
|
|
74
|
+
danger: `${CHIP_EDGE.danger} ${CHIP_TONE_INK.danger}`,
|
|
75
|
+
info: `${CHIP_EDGE.info} ${CHIP_TONE_INK.info}`
|
|
20
76
|
},
|
|
21
77
|
variant: {
|
|
22
78
|
tinted: `${CHIP_PAD} ${CHIP_SIZE} ${CHIP_LEADING} ${CHIP_TRACKING}`,
|
package/dist/styles.css
CHANGED
|
@@ -95,3 +95,29 @@
|
|
|
95
95
|
animation: none;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
/* A style set's chevron cut on filled buttons (structural Looks, spec 2026-10-02
|
|
100
|
+
* §3.3), as two bespoke classes for the plussie's reason and one more: written as
|
|
101
|
+
* Tailwind arbitrary properties it was some 105 characters on every button a page
|
|
102
|
+
* renders, and `/kitchen-sink`'s document grew 1.5% for it. Unset, the clip is
|
|
103
|
+
* `none` and nothing changes. Focus and forced colours drop the clip, so the ring
|
|
104
|
+
* and the system border are never cut; unlayered, so a utility cannot undo it. */
|
|
105
|
+
.cue-cut {
|
|
106
|
+
clip-path: var(--cue-style-control-cut-clip, none);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.cue-cut-secondary {
|
|
110
|
+
clip-path: var(--cue-style-secondary-cut, none);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.cue-cut:focus-visible,
|
|
114
|
+
.cue-cut-secondary:focus-visible {
|
|
115
|
+
clip-path: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@media (forced-colors: active) {
|
|
119
|
+
.cue-cut,
|
|
120
|
+
.cue-cut-secondary {
|
|
121
|
+
clip-path: none;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/system/_scoped-pairing.ts
|
|
3
|
+
/**
|
|
4
|
+
* Whether the subtree's pairing is a scope's own rather than the root's.
|
|
5
|
+
*
|
|
6
|
+
* A portal mounts on `<body>`, whose `font-family` was computed from the root's
|
|
7
|
+
* pairing, and a computed family does not follow a token. So a portal opened
|
|
8
|
+
* under a scope that names a pairing asks for the family again
|
|
9
|
+
* (`portal.tsx`), and one opened anywhere else leaves it alone. Set by
|
|
10
|
+
* `StageScope` (`_stage-scope.tsx`) for `Preview` and `FontScope`. Its own
|
|
11
|
+
* module, with nothing but React behind it, so the portal can read it without
|
|
12
|
+
* importing the provider it is stamped from.
|
|
13
|
+
*
|
|
14
|
+
* @internal Not part of the public API.
|
|
15
|
+
*/
|
|
16
|
+
const ScopedPairingContext = React.createContext(false);
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ScopedPairingContext };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { usePublish, useRead } from "./published.js";
|
|
3
|
+
import { AmbientThemeContext, DensityContext, PaintContext, RootAxesContext, StyleSetContext, rootAxes } from "./density.js";
|
|
4
|
+
import { OverridesContext } from "./overrides.js";
|
|
5
|
+
import { ScopedPairingContext } from "./_scoped-pairing.js";
|
|
6
|
+
import { NO_PAIRING_SEED, PairingSeedContext, ThemeContext } from "./theme-provider.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
//#region src/system/_stage-scope.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The attributes the stage carries — and, when nothing is asked, none at all.
|
|
12
|
+
*
|
|
13
|
+
* Inheriting is the *absence* of attributes, which is why every branch below
|
|
14
|
+
* is conditional. Three branches add a second attribute, each for a reason in
|
|
15
|
+
* the stylesheets rather than in taste.
|
|
16
|
+
*
|
|
17
|
+
* A theme's light block is the compound selector
|
|
18
|
+
* `[data-theme="t"][data-mode="light"]`, so a mode with no theme restates the
|
|
19
|
+
* page's theme on the same element. Read the other way, a theme with no mode
|
|
20
|
+
* repeats the page's mode. Otherwise the stage would match the theme's dark
|
|
21
|
+
* block under a light page, while every popup opened on it came up light:
|
|
22
|
+
* popups are stamped from the context `StageScope` publishes, which carries
|
|
23
|
+
* the page's mode.
|
|
24
|
+
*
|
|
25
|
+
* A theme's density overrides are addressed through `data-cue-theme`, the
|
|
26
|
+
* stamp `<Density>` writes, so a density names the nearest theme the same way.
|
|
27
|
+
*
|
|
28
|
+
* A stage that ends up naming a theme and no rung — its own theme, or the
|
|
29
|
+
* page's restated beside a mode (`scoped`, below) — restates the rung in force
|
|
30
|
+
* around it, as `data-density` with the theme as `data-cue-theme` (spec
|
|
31
|
+
* 2026-09-26-looks-fidelity §4.1.1). A theme's rung retunes are keyed on the
|
|
32
|
+
* theme and the rung on one element, or a rung below the theme, so a stage that
|
|
33
|
+
* re-themes with no `data-density` of its own inherits the rung already
|
|
34
|
+
* resolved against the PAGE's theme, and its own theme's retune of that rung
|
|
35
|
+
* never applies. `/looks` opens every stage this way: a theme and no rung. It
|
|
36
|
+
* is what a nested `ThemeProvider` that names a theme already does — theme,
|
|
37
|
+
* rung and mode together. The rung is the one `DensityContext` carries: `null`
|
|
38
|
+
* outside every provider, where nothing is restated, and never `useDensity()`'s
|
|
39
|
+
* `compact` fallback, which would invent one.
|
|
40
|
+
*
|
|
41
|
+
* The style set adds no second attribute, and that is the difference worth
|
|
42
|
+
* writing down beside the three that do: a `[data-style]` block is a single
|
|
43
|
+
* attribute selector naming no other axis, so a set stamped alone matches on
|
|
44
|
+
* its own.
|
|
45
|
+
*
|
|
46
|
+
* But a stage that re-themes, re-modes or re-faces restates the set and the pack
|
|
47
|
+
* in force around it (`around`), for a reason in custom properties rather than
|
|
48
|
+
* in selectors. Their blocks declare names whose values read those axes — a
|
|
49
|
+
* pack's stroke stops are the theme's borders, a set's band rule and ground are
|
|
50
|
+
* the theme's border and surface — and a custom property's `var()`s are
|
|
51
|
+
* substituted where it is declared. Declared only on the page, they arrive at
|
|
52
|
+
* the stage already resolved against the page's theme and pairing; restated on
|
|
53
|
+
* the stage, beside its own `data-theme` or `data-font`, they resolve against
|
|
54
|
+
* those. A rung alone changes no ink and restates nothing. `around` is `null`
|
|
55
|
+
* until the stage may restate: under a root that persists, not before the stage
|
|
56
|
+
* has mounted, when the set and pack in force are the root's props rather than
|
|
57
|
+
* the visitor's (see `useRestateReady`).
|
|
58
|
+
*
|
|
59
|
+
* The page's mode and rung, restated beside a stage theme — and the page's
|
|
60
|
+
* theme, restated beside a stage mode or as a stage rung's `data-cue-theme` —
|
|
61
|
+
* wait for the same mount on the same terms (tracking plan F13): under a
|
|
62
|
+
* persisting root each is the root's, whose value on the server is its prop
|
|
63
|
+
* while `<html>` carries the visitor's. So until then `followed` holds it back,
|
|
64
|
+
* and `data-cue-follows` names it instead — the theme stylesheets' blocks each
|
|
65
|
+
* have a twin keyed on `<html>`'s value and that marker (a followed rung
|
|
66
|
+
* included: the base rung's twin in `axes.css`, and each theme's retune of it
|
|
67
|
+
* keyed on the stage's own `data-theme`), and a rung node with no
|
|
68
|
+
* `data-cue-theme` is reached by the rung rules' structural form, so the stage
|
|
69
|
+
* paints the visitor's theme, mode and rung from the first frame (see
|
|
70
|
+
* `useFollowedAxes`). A theme, mode or rung the stage names, or one named
|
|
71
|
+
* between it and the root, is stamped as it always was. A followed rung takes
|
|
72
|
+
* its `data-cue-theme` with it: the stamp addresses a `data-density` on the
|
|
73
|
+
* same element, and there is none to address until the rung is stamped.
|
|
74
|
+
*/
|
|
75
|
+
function stageAttributes(asked, page, around, aroundDensity, followed) {
|
|
76
|
+
const attributes = {};
|
|
77
|
+
const scoped = asked.theme ?? (asked.mode === void 0 ? null : page?.theme ?? null);
|
|
78
|
+
if (scoped !== null && (asked.theme !== void 0 || !followed.theme)) attributes["data-theme"] = scoped;
|
|
79
|
+
const moded = asked.mode ?? (asked.theme === void 0 ? void 0 : page?.mode);
|
|
80
|
+
if (moded !== void 0 && !followed.mode) attributes["data-mode"] = moded;
|
|
81
|
+
if (asked.density !== void 0) {
|
|
82
|
+
attributes["data-density"] = asked.density;
|
|
83
|
+
const nearest = asked.theme ?? page?.theme ?? null;
|
|
84
|
+
if (nearest !== null && (asked.theme !== void 0 || !followed.theme)) attributes["data-cue-theme"] = nearest;
|
|
85
|
+
} else if (scoped !== null && aroundDensity !== null && !followed.density) {
|
|
86
|
+
attributes["data-density"] = aroundDensity;
|
|
87
|
+
if (asked.theme !== void 0 || !followed.theme) attributes["data-cue-theme"] = scoped;
|
|
88
|
+
}
|
|
89
|
+
if (followed.marker !== null) attributes["data-cue-follows"] = followed.marker;
|
|
90
|
+
if (asked.font !== void 0) attributes["data-font"] = asked.font;
|
|
91
|
+
if (asked.styleSet !== void 0) attributes["data-style"] = asked.styleSet;
|
|
92
|
+
if (asked.paint !== void 0) attributes["data-paint"] = asked.paint;
|
|
93
|
+
const restates = around !== null && (scoped !== null || moded !== void 0 || asked.font !== void 0);
|
|
94
|
+
if (restates && asked.styleSet === void 0 && around.styleSet !== null) attributes["data-style"] = around.styleSet;
|
|
95
|
+
if (restates && asked.paint === void 0 && around.paint !== null) attributes["data-paint"] = around.paint;
|
|
96
|
+
return attributes;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The contexts the stage republishes, so an overlay opened on it follows it.
|
|
100
|
+
*
|
|
101
|
+
* `useCuePortalProps()` stamps a portal container from `ThemeContext` and
|
|
102
|
+
* `DensityContext`; a popup mounts on `<body>` and inherits nothing from the
|
|
103
|
+
* stage's DOM. So the stage's subtree reads a theme context with the stage's
|
|
104
|
+
* theme and mode, and a density context with the stage's rung. The tools and
|
|
105
|
+
* footer are outside this subtree, so what they open follows the page. A theme
|
|
106
|
+
* the provider did not register leaves `manifest` null, which is what
|
|
107
|
+
* `useTheme()` already reports for a name nobody registered.
|
|
108
|
+
*
|
|
109
|
+
* The style set travels with the density for the same reason and by the same
|
|
110
|
+
* route: `useCuePortalProps()` stamps a portal container from `StyleSetContext`,
|
|
111
|
+
* and a popup opened on the stage inherits nothing from the stage's DOM.
|
|
112
|
+
*
|
|
113
|
+
* And the pairing travels in the theme context, because that is where the
|
|
114
|
+
* portal stamp reads `data-font` from (`theme?.font`). It used not to: the
|
|
115
|
+
* stage stamped `data-font` on its own element and republished nothing, so a
|
|
116
|
+
* popover opened on a `plex` stage came up in the root's pairing — a limit the
|
|
117
|
+
* `font` prop's docblock stated rather than a rule anybody chose. A pairing on
|
|
118
|
+
* the stage is the stage's, exactly as its theme is; `setFont` read here still
|
|
119
|
+
* drives the root, because the pairing's setter has no island form.
|
|
120
|
+
*/
|
|
121
|
+
function StageScope({ theme, mode, density, font, styleSet, paint, children }) {
|
|
122
|
+
const outer = useRead(ThemeContext);
|
|
123
|
+
const outerOverrides = useRead(OverridesContext);
|
|
124
|
+
const outerDensity = useRead(DensityContext);
|
|
125
|
+
const outerAmbient = useRead(AmbientThemeContext);
|
|
126
|
+
const outerStyleSet = useRead(StyleSetContext);
|
|
127
|
+
const outerPaint = useRead(PaintContext);
|
|
128
|
+
const value = React.useMemo(() => {
|
|
129
|
+
if (outer === null || theme === void 0 && mode === void 0 && font === void 0) return outer;
|
|
130
|
+
const scoped = theme ?? outer.theme;
|
|
131
|
+
return {
|
|
132
|
+
...outer,
|
|
133
|
+
theme: scoped,
|
|
134
|
+
manifest: scoped === outer.theme ? outer.manifest : null,
|
|
135
|
+
mode: mode ?? outer.mode,
|
|
136
|
+
font: font ?? outer.font,
|
|
137
|
+
resolvedMode: mode === "dark" || mode === "light" ? mode : outer.resolvedMode
|
|
138
|
+
};
|
|
139
|
+
}, [
|
|
140
|
+
outer,
|
|
141
|
+
theme,
|
|
142
|
+
mode,
|
|
143
|
+
font
|
|
144
|
+
]);
|
|
145
|
+
const stamped = theme !== void 0 || mode !== void 0;
|
|
146
|
+
const inputs = [
|
|
147
|
+
theme,
|
|
148
|
+
mode,
|
|
149
|
+
density,
|
|
150
|
+
font,
|
|
151
|
+
styleSet,
|
|
152
|
+
paint,
|
|
153
|
+
React.useContext(ThemeContext),
|
|
154
|
+
React.useContext(OverridesContext),
|
|
155
|
+
React.useContext(DensityContext),
|
|
156
|
+
React.useContext(AmbientThemeContext),
|
|
157
|
+
React.useContext(StyleSetContext),
|
|
158
|
+
React.useContext(PaintContext)
|
|
159
|
+
];
|
|
160
|
+
const publishedAmbient = usePublish(theme ?? outerAmbient, inputs);
|
|
161
|
+
const publishedDensity = usePublish(density ?? outerDensity, inputs);
|
|
162
|
+
const publishedStyleSet = usePublish(styleSet ?? outerStyleSet, inputs);
|
|
163
|
+
const publishedPaint = usePublish(paint ?? outerPaint, inputs);
|
|
164
|
+
const publishedOverrides = usePublish(stamped ? null : outerOverrides, inputs);
|
|
165
|
+
const publishedValue = usePublish(value, inputs);
|
|
166
|
+
const outerPairingSeed = React.useContext(PairingSeedContext);
|
|
167
|
+
const outerScopedPairing = React.useContext(ScopedPairingContext);
|
|
168
|
+
const outerRootAxes = React.useContext(RootAxesContext);
|
|
169
|
+
return /* @__PURE__ */ jsx(RootAxesContext.Provider, {
|
|
170
|
+
value: rootAxes(outerRootAxes.theme && theme === void 0, outerRootAxes.mode && mode === void 0, outerRootAxes.density && density === void 0),
|
|
171
|
+
children: /* @__PURE__ */ jsx(AmbientThemeContext.Provider, {
|
|
172
|
+
value: publishedAmbient,
|
|
173
|
+
children: /* @__PURE__ */ jsx(DensityContext.Provider, {
|
|
174
|
+
value: publishedDensity,
|
|
175
|
+
children: /* @__PURE__ */ jsx(StyleSetContext.Provider, {
|
|
176
|
+
value: publishedStyleSet,
|
|
177
|
+
children: /* @__PURE__ */ jsx(PaintContext.Provider, {
|
|
178
|
+
value: publishedPaint,
|
|
179
|
+
children: /* @__PURE__ */ jsx(OverridesContext.Provider, {
|
|
180
|
+
value: publishedOverrides,
|
|
181
|
+
children: /* @__PURE__ */ jsx(PairingSeedContext.Provider, {
|
|
182
|
+
value: font === void 0 ? outerPairingSeed : NO_PAIRING_SEED,
|
|
183
|
+
children: /* @__PURE__ */ jsx(ScopedPairingContext.Provider, {
|
|
184
|
+
value: font !== void 0 || outerScopedPairing,
|
|
185
|
+
children: /* @__PURE__ */ jsx(ThemeContext.Provider, {
|
|
186
|
+
value: publishedValue,
|
|
187
|
+
children
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
export { StageScope, stageAttributes };
|
|
@@ -26,7 +26,7 @@ const CHECK_EDGE = "border-[color:var(--cue-style-check-edge,var(--cue-border))]
|
|
|
26
26
|
* box is 18px on a phone and 16px on a desktop, rounded at 0.4 of its control
|
|
27
27
|
* corner (4px). The `sm` box keeps its `icon-sm`.
|
|
28
28
|
*/
|
|
29
|
-
const CHECK_RADIUS = "rounded-[var(--cue-style-check-radius,calc(var(--cue-radius-control)*var(--cue-radius-scale)))]";
|
|
29
|
+
const CHECK_RADIUS = "rounded-[var(--cue-style-check-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
|
|
30
30
|
/** @see CHECK_RADIUS */
|
|
31
31
|
const CHECK_SIZE_MD = "size-[var(--cue-style-check-size,var(--cue-icon-md))]";
|
|
32
32
|
/**
|
|
@@ -69,7 +69,7 @@ const CHIP_TRACKING = "tracking-[var(--cue-style-chip-tracking,var(--cue-trackin
|
|
|
69
69
|
/** @see CHIP_CASE — the `tag` variant's tracking; its own closed `0.06em` by default. */
|
|
70
70
|
const CHIP_TRACKING_TAG = "tracking-[var(--cue-style-chip-tracking,0.06em)]";
|
|
71
71
|
/** @see CHIP_CASE — the chip's radius; what `rounded-control` compiles to by default. */
|
|
72
|
-
const CHIP_RADIUS = "rounded-[var(--cue-style-chip-radius,calc(var(--cue-radius-control)*var(--cue-radius-scale)))]";
|
|
72
|
+
const CHIP_RADIUS = "rounded-[var(--cue-style-chip-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
|
|
73
73
|
/** @see CHIP_CASE — each tone's fill: `var(--cue-style-chip-fill, 10%)` of the tone over transparent. */
|
|
74
74
|
const CHIP_FILL = {
|
|
75
75
|
accent: "bg-[color:color-mix(in_oklab,var(--cue-accent)_var(--cue-style-chip-fill,10%),transparent)]",
|
|
@@ -4,21 +4,25 @@
|
|
|
4
4
|
* `style-group-label-size`, `style-group-label-ink`): a menu's and a command
|
|
5
5
|
* palette's group labels. A menu's falls back through `style-label-size` and
|
|
6
6
|
* `style-label-ink` to the rung and ink it drew; the palette's, which read no
|
|
7
|
-
* label name, to its `text-micro
|
|
7
|
+
* label name, to its `text-micro`. coss.com/ui sets both at
|
|
8
8
|
* 12px in `fg-muted` (P163.label-size, P152.group-label, P152.group-label-ink),
|
|
9
9
|
* where its field labels are 14px `fg`.
|
|
10
10
|
*
|
|
11
11
|
* **Optional** (spec §3.0). Its own module for docgen's reason: one token per
|
|
12
12
|
* file, so only the components that read it are credited with it.
|
|
13
13
|
*
|
|
14
|
+
* Both inks fall back to `fg-muted`, not `fg-subtle`: a group label is label
|
|
15
|
+
* text, held to 4.5:1 (structural Looks, spec 2026-10-02 §4.1; see
|
|
16
|
+
* `_style-label-ink.ts`).
|
|
17
|
+
*
|
|
14
18
|
* @internal Not part of the public API.
|
|
15
19
|
*/
|
|
16
20
|
const GROUP_LABEL_SIZE = "text-[length:var(--cue-style-group-label-size,var(--cue-style-label-size,var(--cue-text-label)))]";
|
|
17
21
|
/** @see GROUP_LABEL_SIZE */
|
|
18
|
-
const GROUP_LABEL_INK = "text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-
|
|
22
|
+
const GROUP_LABEL_INK = "text-[color:var(--cue-style-group-label-ink,var(--cue-style-label-ink,var(--cue-fg-muted)))]";
|
|
19
23
|
/** @see GROUP_LABEL_SIZE — the palette's, which read no label name. */
|
|
20
24
|
const GROUP_LABEL_SIZE_MICRO = "text-[length:var(--cue-style-group-label-size,var(--cue-text-micro))]";
|
|
21
25
|
/** @see GROUP_LABEL_SIZE */
|
|
22
|
-
const GROUP_LABEL_INK_SUBTLE = "text-[color:var(--cue-style-group-label-ink,var(--cue-fg-
|
|
26
|
+
const GROUP_LABEL_INK_SUBTLE = "text-[color:var(--cue-style-group-label-ink,var(--cue-fg-muted))]";
|
|
23
27
|
//#endregion
|
|
24
28
|
export { GROUP_LABEL_INK, GROUP_LABEL_INK_SUBTLE, GROUP_LABEL_SIZE, GROUP_LABEL_SIZE_MICRO };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/system/_style-hero-title.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `Hero` title's voice: the style set's **display** rung, falling back to
|
|
4
|
+
* the page title's voice (`_style-page-title.ts`) name by name, and through it
|
|
5
|
+
* to the class the page title replaced.
|
|
6
|
+
*
|
|
7
|
+
* Spec 2026-10-02-structural-looks §16 (revision 8). A hero is a page's display
|
|
8
|
+
* line, and the sources say so: Tender's opens at 48px where its page titles
|
|
9
|
+
* are 32px, Porcelain's at 48px against 36px, Zine's at 96px. The display rung
|
|
10
|
+
* is where every measured set already records that size, so the hero reads it
|
|
11
|
+
* first. `style-hero-title-size` sits in front of the size for the one source
|
|
12
|
+
* whose hero is larger than its display rung: Graphite's is 56px, and its
|
|
13
|
+
* display rung is its 46px section heading.
|
|
14
|
+
*
|
|
15
|
+
* | property | reads, outermost first |
|
|
16
|
+
* |-------------------------|------------------------|
|
|
17
|
+
* | font-family | `style-display-face` → `style-page-title-face` → `var(--cue-font-mono)` |
|
|
18
|
+
* | font-size | `style-hero-title-size` → `style-display` → `style-page-title-size` → `var(--cue-text-title)` |
|
|
19
|
+
* | font-weight | `style-display-weight` → `style-page-title-weight` → `500` |
|
|
20
|
+
* | letter-spacing | `style-tracking-display` → `style-page-title-tracking` → `normal` |
|
|
21
|
+
* | line-height | `style-leading-display` → `style-page-title-leading` → `inherit` |
|
|
22
|
+
* | font-variation-settings | `style-display-variation` → `normal` |
|
|
23
|
+
* | color | `style-heading-ink` → `var(--cue-fg)` |
|
|
24
|
+
* | text-transform | `style-display-case` → `none` |
|
|
25
|
+
*
|
|
26
|
+
* **Who moves.** A set that declares none of the display names draws its hero
|
|
27
|
+
* exactly as before, because each chain ends in the page title's reader. The
|
|
28
|
+
* four structural sets of phase 3 declare both rungs equal. `editorial` and
|
|
29
|
+
* `marketing` declare a display size and no page-title size, so their heroes
|
|
30
|
+
* grow to it (22 → 52px and 22 → 64px at scale 1); that is what a display rung
|
|
31
|
+
* is for. `tests/browser/hero-voice.spec.ts` pins every set.
|
|
32
|
+
*
|
|
33
|
+
* **Its own module**, for the reason `_style-page-title.ts` gives: `@repo/docgen`
|
|
34
|
+
* attributes a token to a component by the file its class string lives in, and
|
|
35
|
+
* the display names belong to the hero.
|
|
36
|
+
*
|
|
37
|
+
* @internal Not part of the public API.
|
|
38
|
+
*/
|
|
39
|
+
const HERO_TITLE_VOICE = "font-[family-name:var(--cue-style-display-face,var(--cue-style-page-title-face,var(--cue-font-mono)))] text-[length:var(--cue-style-hero-title-size,var(--cue-style-display,var(--cue-style-page-title-size,var(--cue-text-title))))] font-[number:var(--cue-style-display-weight,var(--cue-style-page-title-weight,500))] tracking-[var(--cue-style-tracking-display,var(--cue-style-page-title-tracking,normal))] leading-[var(--cue-style-leading-display,var(--cue-style-page-title-leading,inherit))] [font-variation-settings:var(--cue-style-display-variation,normal)] text-[color:var(--cue-style-heading-ink,var(--cue-fg))] [text-transform:var(--cue-style-display-case,none)] [overflow-wrap:anywhere]";
|
|
40
|
+
//#endregion
|
|
41
|
+
export { HERO_TITLE_VOICE };
|
|
@@ -41,7 +41,7 @@ const KBD_SIZE = "text-[length:var(--cue-style-kbd-size,var(--cue-style-chip-siz
|
|
|
41
41
|
* where its key caps stay 12px. It falls back through the chip's size, so a
|
|
42
42
|
* Look that names only the chip's keeps its key caps beside it.
|
|
43
43
|
*/
|
|
44
|
-
const KBD_BOX = "h-[var(--cue-style-kbd-height,var(--cue-chip-h))] min-w-[var(--cue-style-kbd-height,var(--cue-chip-h))] px-[var(--cue-style-kbd-pad,var(--cue-space-2))] rounded-[var(--cue-style-kbd-radius,calc(var(--cue-radius-control)*var(--cue-radius-scale)))]";
|
|
44
|
+
const KBD_BOX = "h-[var(--cue-style-kbd-height,var(--cue-chip-h))] min-w-[var(--cue-style-kbd-height,var(--cue-chip-h))] px-[var(--cue-style-kbd-pad,var(--cue-space-2))] rounded-[var(--cue-style-kbd-radius,calc(var(--cue-style-control-radius,var(--cue-radius-control))*var(--cue-radius-scale)))]";
|
|
45
45
|
/**
|
|
46
46
|
* A key's leading (round 6, spec §11, lane R6.P: `style-kbd-leading`), falling
|
|
47
47
|
* back to the `leading-none` it drew. coss.com/ui's keys set 12px on a 16px line
|