@cueplusplus/ui 0.18.1 → 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.
Files changed (157) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/brand/_motif-seed.js +20 -0
  3. package/dist/brand/gloss-object.d.ts +23 -0
  4. package/dist/brand/gloss-object.js +108 -0
  5. package/dist/brand/halftone.d.ts +23 -0
  6. package/dist/brand/halftone.js +54 -0
  7. package/dist/brand/index.d.ts +6 -1
  8. package/dist/brand/motif.d.ts +32 -0
  9. package/dist/brand/motif.js +71 -0
  10. package/dist/brand/particle-mark.d.ts +29 -0
  11. package/dist/brand/particle-mark.js +137 -0
  12. package/dist/brand/ribbon.d.ts +20 -0
  13. package/dist/brand/ribbon.js +54 -0
  14. package/dist/chat/message.js +1 -1
  15. package/dist/chrome/announcement-bar.d.ts +30 -0
  16. package/dist/chrome/announcement-bar.js +38 -0
  17. package/dist/chrome/app-bar.d.ts +4 -1
  18. package/dist/chrome/app-bar.js +7 -2
  19. package/dist/chrome/app-shell.js +8 -1
  20. package/dist/chrome/band.d.ts +3 -1
  21. package/dist/chrome/band.js +4 -3
  22. package/dist/chrome/hero.d.ts +61 -0
  23. package/dist/chrome/hero.js +108 -0
  24. package/dist/chrome/index.d.ts +6 -1
  25. package/dist/chrome/index.js +6 -1
  26. package/dist/chrome/keyword.d.ts +22 -0
  27. package/dist/chrome/keyword.js +28 -0
  28. package/dist/chrome/logo-strip.d.ts +23 -0
  29. package/dist/chrome/logo-strip.js +43 -0
  30. package/dist/chrome/page-shell.js +13 -1
  31. package/dist/chrome/stat-band.d.ts +32 -0
  32. package/dist/chrome/stat-band.js +39 -0
  33. package/dist/configurator/_overrides.js +7 -4
  34. package/dist/dmx/channel-matrix.js +5 -3
  35. package/dist/dmx/dmx-strip.js +5 -3
  36. package/dist/elements/animated-number.js +4 -1
  37. package/dist/elements/replay.d.ts +1 -1
  38. package/dist/elements/replay.js +5 -2
  39. package/dist/elements/scroll-anchor.js +4 -1
  40. package/dist/index.d.ts +13 -1
  41. package/dist/index.js +14 -2
  42. package/dist/instruments/index.js +1 -1
  43. package/dist/instruments/stat.js +1 -1
  44. package/dist/instruments/terminal-frame.js +5 -2
  45. package/dist/layout/grid.js +4 -2
  46. package/dist/layout/link.d.ts +1 -1
  47. package/dist/layout/link.js +1 -1
  48. package/dist/layout/preview.js +4 -188
  49. package/dist/layout/sidebar.js +88 -52
  50. package/dist/overlays/_surface.d.ts +2 -2
  51. package/dist/primitives/button.d.ts +1 -1
  52. package/dist/primitives/button.js +23 -4
  53. package/dist/primitives/chip.d.ts +1 -1
  54. package/dist/primitives/chip.js +62 -6
  55. package/dist/styles.css +26 -0
  56. package/dist/system/_paint-glass.js +1 -1
  57. package/dist/system/_paint-glow.js +1 -1
  58. package/dist/system/_scoped-pairing.js +18 -0
  59. package/dist/system/_stage-scope.js +199 -0
  60. package/dist/system/_storage.js +25 -0
  61. package/dist/system/_style-accordion-rule.js +1 -1
  62. package/dist/system/_style-app-bar.js +2 -2
  63. package/dist/system/_style-body-weight.js +1 -1
  64. package/dist/system/_style-check.js +1 -1
  65. package/dist/system/_style-chip.js +1 -1
  66. package/dist/system/_style-control.js +2 -2
  67. package/dist/system/_style-eyebrow.js +1 -1
  68. package/dist/system/_style-footer-rule.js +2 -2
  69. package/dist/system/_style-group-label.js +7 -3
  70. package/dist/system/_style-hero-title.js +41 -0
  71. package/dist/system/_style-kbd.js +1 -1
  72. package/dist/system/_style-label-case.js +2 -2
  73. package/dist/system/_style-label-ink.js +12 -5
  74. package/dist/system/_style-label-weight.js +2 -2
  75. package/dist/system/_style-link.js +1 -1
  76. package/dist/system/_style-menu.js +1 -1
  77. package/dist/system/_style-nav-voice.js +2 -2
  78. package/dist/system/_style-page-title.js +3 -3
  79. package/dist/system/_style-segmented-control.js +3 -3
  80. package/dist/system/_style-skeleton.js +1 -1
  81. package/dist/system/_style-tab.js +1 -1
  82. package/dist/system/_style-title.js +1 -1
  83. package/dist/system/font-scope.d.ts +38 -0
  84. package/dist/system/font-scope.js +69 -0
  85. package/dist/system/index.d.ts +3 -1
  86. package/dist/system/index.js +3 -1
  87. package/dist/system/portal.js +7 -1
  88. package/dist/system/still.d.ts +32 -0
  89. package/dist/system/still.js +47 -0
  90. package/dist/system/theme-provider.js +7 -4
  91. package/manifest/components/alert-dialog.json +1 -1
  92. package/manifest/components/announcement-bar.json +82 -0
  93. package/manifest/components/app-bar.json +3 -2
  94. package/manifest/components/app-shell.json +3 -0
  95. package/manifest/components/autocomplete.json +1 -0
  96. package/manifest/components/band.json +2 -1
  97. package/manifest/components/bento.json +12 -12
  98. package/manifest/components/button.json +1 -0
  99. package/manifest/components/channel-matrix.json +1 -0
  100. package/manifest/components/chart-tooltip-content.json +1 -0
  101. package/manifest/components/checkbox-group.json +1 -1
  102. package/manifest/components/checkbox.json +1 -1
  103. package/manifest/components/chip.json +1 -0
  104. package/manifest/components/combobox.json +1 -0
  105. package/manifest/components/command-palette.json +1 -0
  106. package/manifest/components/context-menu.json +1 -1
  107. package/manifest/components/density.json +6 -2
  108. package/manifest/components/dialog.json +1 -1
  109. package/manifest/components/dmx-bar.json +1 -1
  110. package/manifest/components/dmx-strip.json +1 -1
  111. package/manifest/components/drawer.json +1 -1
  112. package/manifest/components/dropdown-menu.json +1 -0
  113. package/manifest/components/font-scope.json +81 -0
  114. package/manifest/components/footer.json +0 -1
  115. package/manifest/components/gloss-object.json +81 -0
  116. package/manifest/components/grid.json +6 -6
  117. package/manifest/components/halftone.json +79 -0
  118. package/manifest/components/hero.json +204 -0
  119. package/manifest/components/hover-card.json +1 -1
  120. package/manifest/components/kbd.json +1 -0
  121. package/manifest/components/keyword.json +67 -0
  122. package/manifest/components/ledger.json +4 -4
  123. package/manifest/components/logo-strip.json +93 -0
  124. package/manifest/components/markdown-text.json +2 -2
  125. package/manifest/components/menubar.json +1 -1
  126. package/manifest/components/motif.json +90 -0
  127. package/manifest/components/multi-select.json +1 -0
  128. package/manifest/components/navigation-menu.json +1 -0
  129. package/manifest/components/page-shell.json +5 -0
  130. package/manifest/components/particle-mark.json +75 -0
  131. package/manifest/components/popover.json +1 -1
  132. package/manifest/components/preview.json +9 -4
  133. package/manifest/components/radio-group.json +0 -1
  134. package/manifest/components/radio.json +0 -1
  135. package/manifest/components/ribbon.json +65 -0
  136. package/manifest/components/scrollable-tabs-list.json +1 -0
  137. package/manifest/components/segmented-control.json +1 -0
  138. package/manifest/components/sheet.json +1 -1
  139. package/manifest/components/sidebar.json +1 -0
  140. package/manifest/components/skeleton.json +1 -0
  141. package/manifest/components/stack.json +4 -4
  142. package/manifest/components/stat-band.json +68 -0
  143. package/manifest/components/stat.json +1 -1
  144. package/manifest/components/still-provider.json +60 -0
  145. package/manifest/components/switch.json +0 -1
  146. package/manifest/components/table.json +1 -1
  147. package/manifest/components/tabs.json +1 -0
  148. package/manifest/components/tags-input.json +1 -0
  149. package/manifest/components/theme-configurator.json +1 -0
  150. package/manifest/components/theme-provider.json +9 -4
  151. package/manifest/components/toast.json +1 -0
  152. package/manifest/components/tooltip.json +1 -1
  153. package/manifest/fixtures.json +18 -0
  154. package/manifest/manifest.json +267 -55
  155. package/manifest/tokens.json +91 -3
  156. package/package.json +4 -4
  157. package/dist/system/_paint-ground-pattern.js +0 -30
@@ -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: React.forwardRef(function SidebarRoot({ className, landmark = "navigation", collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, width = "14rem", collapsedWidth = "3rem", style, children, ...elementProps }, ref) {
67
- const Element = LANDMARK_ELEMENTS[landmark];
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-subtle)))] font-[number:var(--cue-style-label-weight,inherit)] [text-transform:var(--cue-style-label-case,uppercase)] select-none";
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. */
@@ -10,7 +10,7 @@ import { useRender } from "@base-ui/react/use-render";
10
10
  * A class concatenated onto it — by a template string, `clsx`, or that option —
11
11
  * does not replace the recipe's own; the two compete by the order Tailwind
12
12
  * emits them in, which is not a contract. The inline padding in particular is
13
- * a `px-[calc(…)]` reader of the style set's `style-control-pad`, and Tailwind
13
+ * a `px-[ calc(…) ]` reader of the style set's `style-control-pad`, and Tailwind
14
14
  * emits it after every numeric `px-*`, so a concatenated `px-2` no longer wins
15
15
  * the way it did over the `px-(--cue-space-*)` rung it replaced. To override a
16
16
  * class of the recipe, pass `className` to `Button` itself, which merges, or
@@ -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
  /**
@@ -72,7 +91,7 @@ const buttonTones = {
72
91
  * A class concatenated onto it — by a template string, `clsx`, or that option —
73
92
  * does not replace the recipe's own; the two compete by the order Tailwind
74
93
  * emits them in, which is not a contract. The inline padding in particular is
75
- * a `px-[calc(…)]` reader of the style set's `style-control-pad`, and Tailwind
94
+ * a `px-[ calc(…) ]` reader of the style set's `style-control-pad`, and Tailwind
76
95
  * emits it after every numeric `px-*`, so a concatenated `px-2` no longer wins
77
96
  * the way it did over the `px-(--cue-space-*)` rung it replaced. To override a
78
97
  * class of the recipe, pass `className` to `Button` itself, which merges, or
@@ -2,7 +2,7 @@ import { ClassProp, VariantProps } from "../lib/cva.js";
2
2
  import * as React from "react";
3
3
  //#region src/primitives/chip.d.ts
4
4
  declare const chipVariants: (props?: ({
5
- tone?: "info" | "warn" | "danger" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
5
+ tone?: "danger" | "info" | "warn" | "accent" | "ok" | "busy" | "neutral" | null | undefined;
6
6
  variant?: "outline" | "tinted" | "tag" | null | undefined;
7
7
  interactive?: boolean | null | undefined;
8
8
  } & ClassProp) | undefined) => string;
@@ -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} text-accent`,
15
- ok: `${CHIP_EDGE.ok} text-ok`,
16
- busy: `${CHIP_EDGE.busy} text-busy`,
17
- warn: `${CHIP_EDGE.warn} text-warn`,
18
- danger: `${CHIP_EDGE.danger} text-danger`,
19
- info: `${CHIP_EDGE.info} text-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
+ }
@@ -5,7 +5,7 @@
5
5
  * **Why this writes `--tw-backdrop-blur` instead of `backdrop-filter`.** Spec
6
6
  * §13.5, decided on measurements. Tailwind composes every backdrop utility into
7
7
  * ONE declaration — `backdrop-filter: var(--tw-backdrop-blur,)
8
- * var(--tw-backdrop-brightness,) …` — so a raw `[backdrop-filter:var(…)]` here
8
+ * var(--tw-backdrop-brightness,) …` — so a raw `[backdrop-filter: var(…)]` here
9
9
  * is a second, competing declaration. Measured: a consumer passing
10
10
  * `backdrop-brightness-50` through `className` then computes
11
11
  * `brightness(0.5)` and **no blur at all**. The glass does not narrow, it
@@ -27,7 +27,7 @@
27
27
  * adoption additive. The `outline` tone does carry one — `REST_SHADE_CLASS`,
28
28
  * the resting shade (fidelity plan R3) — so it must not also take
29
29
  * `GLOW_CLASSES`: one of the two would be dropped in silence. A halo there
30
- * would compose both into one `shadow-[…]` list instead.
30
+ * would compose both into one `shadow-[ … ]` list instead.
31
31
  *
32
32
  * @internal Not part of the public API.
33
33
  */
@@ -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 };
@@ -0,0 +1,25 @@
1
+ //#region src/system/_storage.ts
2
+ /**
3
+ * The page's `localStorage`, or `null` where there is none this code may use.
4
+ *
5
+ * `typeof localStorage === "undefined"` is not a safe probe. In a document with
6
+ * an opaque origin — a frame sandboxed without `allow-same-origin`, which is
7
+ * what the studio's canvas is — the accessor on `window` throws a
8
+ * `SecurityError` instead of answering `undefined`, and a `typeof` written
9
+ * outside a `try` throws with it, before the `try` meant to absorb storage
10
+ * failures is ever entered. The probe is taken inside one here, so every reader
11
+ * and writer of a preference degrades to "nothing stored", the way a private
12
+ * window's full quota already did.
13
+ *
14
+ * @returns The storage, or `null` on a server, under an opaque origin, or
15
+ * anywhere else reading the accessor throws.
16
+ */
17
+ function localStore() {
18
+ try {
19
+ return typeof localStorage === "undefined" ? null : localStorage;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ //#endregion
25
+ export { localStore };
@@ -6,7 +6,7 @@
6
6
  * `Accordion.Item` draws the rule with `border-t`, which writes
7
7
  * `border-top-style: var(--tw-border-style)`, a slot Tailwind registers with
8
8
  * `inherits: false` and `initial-value: solid`, so the section fills that slot
9
- * rather than a raw `[border-top-style:…]` — the footer's reason
9
+ * rather than a raw `[border-top-style: …]` — the footer's reason
10
10
  * (`_style-footer-rule.ts`): a caller's own `border-dotted` still wins. The
11
11
  * fallback is the slot's `solid`. stripe.com's accordion draws its rules 1px
12
12
  * dashed in the quiet border ink (`:where(.hds-accordion) .hds-details`), so
@@ -27,7 +27,7 @@
27
27
  * **The hover ground yields to a caller's own ground.** It is the one
28
28
  * declaration here the link never had: `Link` sets no background in any state,
29
29
  * so `<AppBar.Link className="bg-surface-2">` kept its ground under the pointer.
30
- * A plain `hover:bg-[…]` is `(0,2,0)` and would paint over it; this is written
30
+ * A plain `hover:bg-[ … ]` is `(0,2,0)` and would paint over it; this is written
31
31
  * `[@media(hover:hover)]:[:where(&:hover)]:`, which is `hover:` — the same
32
32
  * media query, the same state — at zero specificity, so any class a caller
33
33
  * passes for the ground still wins, hovered or not. The hover INK replaced
@@ -43,7 +43,7 @@
43
43
  * `@property` with `inherits: false` and `initial-value: solid` — which is the
44
44
  * solid rule the bar always drew. Filling that slot keeps the one declaration,
45
45
  * for the reason `_paint-glass.ts` fills `--tw-backdrop-blur`: a raw
46
- * `[border-bottom-style:…]` would be a second, competing declaration, and
46
+ * `[border-bottom-style: …]` would be a second, competing declaration, and
47
47
  * Tailwind emits it after `border-dashed`, so it would beat a caller's own
48
48
  * `border-dotted`. Through the slot a caller's style class still wins, because
49
49
  * it writes `border-style` itself, after `border-b`. The fallback is the