@epilot/spark-ui 0.25.1 → 0.26.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 (125) hide show
  1. package/dist/components/alert-dialog.cjs +4 -2
  2. package/dist/components/alert-dialog.cjs.map +1 -1
  3. package/dist/components/alert-dialog.d.cts +4 -1
  4. package/dist/components/alert-dialog.d.ts +4 -1
  5. package/dist/components/alert-dialog.js +4 -2
  6. package/dist/components/alert-dialog.js.map +1 -1
  7. package/dist/components/attachment.d.cts +1 -1
  8. package/dist/components/attachment.d.ts +1 -1
  9. package/dist/components/autocomplete.cjs +3 -2
  10. package/dist/components/autocomplete.cjs.map +1 -1
  11. package/dist/components/autocomplete.js +3 -2
  12. package/dist/components/autocomplete.js.map +1 -1
  13. package/dist/components/avatar.js +1 -1
  14. package/dist/components/badge.js +1 -1
  15. package/dist/components/banner.cjs +74 -0
  16. package/dist/components/banner.cjs.map +1 -0
  17. package/dist/components/banner.d.cts +27 -0
  18. package/dist/components/banner.d.ts +27 -0
  19. package/dist/components/banner.js +72 -0
  20. package/dist/components/banner.js.map +1 -0
  21. package/dist/components/box.js +1 -1
  22. package/dist/components/bubble.d.cts +1 -1
  23. package/dist/components/bubble.d.ts +1 -1
  24. package/dist/components/button-group.js +1 -1
  25. package/dist/components/button.d.cts +3 -3
  26. package/dist/components/button.d.ts +3 -3
  27. package/dist/components/button.js +1 -1
  28. package/dist/components/callout.d.cts +1 -1
  29. package/dist/components/callout.d.ts +1 -1
  30. package/dist/components/card.js +1 -1
  31. package/dist/components/checkbox.js +1 -1
  32. package/dist/components/data-list.cjs +39 -0
  33. package/dist/components/data-list.cjs.map +1 -0
  34. package/dist/components/data-list.d.cts +33 -0
  35. package/dist/components/data-list.d.ts +33 -0
  36. package/dist/components/data-list.js +35 -0
  37. package/dist/components/data-list.js.map +1 -0
  38. package/dist/components/dialog.cjs +4 -2
  39. package/dist/components/dialog.cjs.map +1 -1
  40. package/dist/components/dialog.d.cts +4 -1
  41. package/dist/components/dialog.d.ts +4 -1
  42. package/dist/components/dialog.js +4 -2
  43. package/dist/components/dialog.js.map +1 -1
  44. package/dist/components/drawer.cjs +12 -2
  45. package/dist/components/drawer.cjs.map +1 -1
  46. package/dist/components/drawer.d.cts +4 -1
  47. package/dist/components/drawer.d.ts +4 -1
  48. package/dist/components/drawer.js +13 -3
  49. package/dist/components/drawer.js.map +1 -1
  50. package/dist/components/dropdown-menu.cjs +5 -3
  51. package/dist/components/dropdown-menu.cjs.map +1 -1
  52. package/dist/components/dropdown-menu.d.cts +1 -0
  53. package/dist/components/dropdown-menu.d.ts +1 -0
  54. package/dist/components/dropdown-menu.js +5 -3
  55. package/dist/components/dropdown-menu.js.map +1 -1
  56. package/dist/components/empty-state.cjs +107 -25
  57. package/dist/components/empty-state.cjs.map +1 -1
  58. package/dist/components/empty-state.d.cts +27 -10
  59. package/dist/components/empty-state.d.ts +27 -10
  60. package/dist/components/empty-state.js +104 -26
  61. package/dist/components/empty-state.js.map +1 -1
  62. package/dist/components/field.cjs +1 -1
  63. package/dist/components/field.js +1 -1
  64. package/dist/components/icon.js +1 -1
  65. package/dist/components/info-block.d.cts +1 -1
  66. package/dist/components/info-block.d.ts +1 -1
  67. package/dist/components/input-otp.cjs +1 -1
  68. package/dist/components/input-otp.js +1 -1
  69. package/dist/components/input.cjs +1 -1
  70. package/dist/components/input.js +1 -1
  71. package/dist/components/kbd.cjs +14 -0
  72. package/dist/components/kbd.cjs.map +1 -0
  73. package/dist/components/kbd.d.cts +13 -0
  74. package/dist/components/kbd.d.ts +13 -0
  75. package/dist/components/kbd.js +13 -0
  76. package/dist/components/kbd.js.map +1 -0
  77. package/dist/components/marker.d.cts +1 -1
  78. package/dist/components/marker.d.ts +1 -1
  79. package/dist/components/message-scroller.cjs +1 -1
  80. package/dist/components/message-scroller.js +1 -1
  81. package/dist/components/pill.js +1 -1
  82. package/dist/components/popover.cjs +3 -2
  83. package/dist/components/popover.cjs.map +1 -1
  84. package/dist/components/popover.js +3 -2
  85. package/dist/components/popover.js.map +1 -1
  86. package/dist/components/radio.js +1 -1
  87. package/dist/components/select.cjs +4 -3
  88. package/dist/components/select.cjs.map +1 -1
  89. package/dist/components/select.js +4 -3
  90. package/dist/components/select.js.map +1 -1
  91. package/dist/components/separator.cjs +35 -17
  92. package/dist/components/separator.cjs.map +1 -1
  93. package/dist/components/separator.d.cts +1 -0
  94. package/dist/components/separator.d.ts +1 -0
  95. package/dist/components/separator.js +35 -17
  96. package/dist/components/separator.js.map +1 -1
  97. package/dist/components/sheet.cjs +4 -2
  98. package/dist/components/sheet.cjs.map +1 -1
  99. package/dist/components/sheet.d.cts +4 -1
  100. package/dist/components/sheet.d.ts +4 -1
  101. package/dist/components/sheet.js +4 -2
  102. package/dist/components/sheet.js.map +1 -1
  103. package/dist/components/switch.js +1 -1
  104. package/dist/components/textarea.cjs +1 -1
  105. package/dist/components/textarea.js +1 -1
  106. package/dist/components/theme.cjs +118 -24
  107. package/dist/components/theme.cjs.map +1 -1
  108. package/dist/components/theme.d.cts +7 -1
  109. package/dist/components/theme.d.ts +7 -1
  110. package/dist/components/theme.js +120 -27
  111. package/dist/components/theme.js.map +1 -1
  112. package/dist/components/toast.cjs +3 -2
  113. package/dist/components/toast.cjs.map +1 -1
  114. package/dist/components/toast.js +3 -2
  115. package/dist/components/toast.js.map +1 -1
  116. package/dist/components/toggle-group.js +1 -1
  117. package/dist/components/toggle.cjs +1 -1
  118. package/dist/components/toggle.js +2 -2
  119. package/dist/components/tooltip.cjs +5 -3
  120. package/dist/components/tooltip.cjs.map +1 -1
  121. package/dist/components/tooltip.d.cts +1 -0
  122. package/dist/components/tooltip.d.ts +1 -0
  123. package/dist/components/tooltip.js +5 -3
  124. package/dist/components/tooltip.js.map +1 -1
  125. package/package.json +1 -1
@@ -14,6 +14,7 @@ let cn = require("cn");
14
14
  /** The epilot brand accent — the scale `spark-default.css` ships statically. */
15
15
  const DEFAULT_ACCENT = "#005EB4";
16
16
  const ThemeContext = react.createContext({});
17
+ const ThemeOutputContext = react.createContext(null);
17
18
  /** Merged theme values from enclosing `<Theme>` providers. */
18
19
  function useTheme() {
19
20
  return react.useContext(ThemeContext);
@@ -21,6 +22,7 @@ function useTheme() {
21
22
  /** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
22
23
  const Theme = react.forwardRef(function Theme({ accentColor, backgroundColor, neutralColor, radius, scaling, spacing, styleVariant, highContrast, inverted, labelPosition, inputStyle, inputColor, cardVariant, cardColor, appearance, fontHeading, fontBody, render, className, style, children, ...rest }, ref) {
23
24
  const parent = useTheme();
25
+ const parentOutput = react.useContext(ThemeOutputContext);
24
26
  const value = react.useMemo(() => ({
25
27
  accentColor: accentColor ?? parent.accentColor,
26
28
  backgroundColor: backgroundColor ?? parent.backgroundColor,
@@ -68,6 +70,8 @@ const Theme = react.forwardRef(function Theme({ accentColor, backgroundColor, ne
68
70
  const accent = react.useMemo(() => {
69
71
  const empty = {
70
72
  inlineVars: {},
73
+ portalVars: {},
74
+ portalScope: void 0,
71
75
  css: "",
72
76
  scoped: false
73
77
  };
@@ -89,21 +93,44 @@ const Theme = react.forwardRef(function Theme({ accentColor, backgroundColor, ne
89
93
  const build = (appearance) => {
90
94
  const opts = optsFor(appearance);
91
95
  const scale = require_lib_accent_theme.generateAccentScale(hex, appearance, opts);
92
- return emit(inverted ? require_lib_accent_theme.invertAccentScale(scale) : scale, opts != null);
93
- };
94
- if (value.appearance) return {
95
- inlineVars: toVars(build(value.appearance)),
96
- css: "",
97
- scoped: false
96
+ const rendered = emit(inverted ? require_lib_accent_theme.invertAccentScale(scale) : scale, opts != null);
97
+ return {
98
+ rendered,
99
+ plain: inverted ? emit(scale, opts != null) : rendered
100
+ };
98
101
  };
102
+ if (value.appearance) {
103
+ const { rendered, plain } = build(value.appearance);
104
+ const inlineVars = toVars(rendered);
105
+ return {
106
+ inlineVars,
107
+ portalVars: inverted ? toVars(plain) : inlineVars,
108
+ portalScope: void 0,
109
+ css: "",
110
+ scoped: false
111
+ };
112
+ }
99
113
  const offCanvasNeutrals = backgroundColor ? require_lib_accent_theme.neutralFamilyVars(resolvedNeutralColor ?? "slate") : [];
100
- const scaleFor = (appearance) => bgAppearance === appearance ? build(appearance) : [...build(appearance), ...offCanvasNeutrals];
114
+ const scaleFor = (appearance) => {
115
+ const { rendered, plain } = build(appearance);
116
+ const extra = bgAppearance === appearance ? [] : offCanvasNeutrals;
117
+ return {
118
+ rendered: [...rendered, ...extra],
119
+ plain: [...plain, ...extra]
120
+ };
121
+ };
101
122
  const light = scaleFor("light");
102
123
  const dark = scaleFor("dark");
103
- const sel = `[data-spark-scope="${scopeId}"]`;
124
+ const rule = (id, lightDecls, darkDecls) => {
125
+ const sel = `[data-spark-scope~="${id}"]`;
126
+ return `${sel}{${lightDecls.join("")}}:is(.dark,.dark-theme) ${sel}{${darkDecls.join("")}}`;
127
+ };
128
+ const portalScope = inverted ? `${scopeId}p` : scopeId;
104
129
  return {
105
130
  inlineVars: {},
106
- css: `${sel}{${light.join("")}}:is(.dark,.dark-theme) ${sel}{${dark.join("")}}`,
131
+ portalVars: {},
132
+ portalScope,
133
+ css: rule(scopeId, light.rendered, dark.rendered) + (inverted ? rule(portalScope, light.plain, dark.plain) : ""),
107
134
  scoped: true
108
135
  };
109
136
  } catch (error) {
@@ -119,41 +146,108 @@ const Theme = react.forwardRef(function Theme({ accentColor, backgroundColor, ne
119
146
  value.appearance,
120
147
  scopeId
121
148
  ]);
149
+ const attrs = react.useMemo(() => ({
150
+ "data-neutral-color": neutralColorAttr,
151
+ "data-radius": radius,
152
+ "data-scaling": scaling,
153
+ "data-spacing": spacing,
154
+ "data-style": styleVariant,
155
+ "data-high-contrast": highContrast ? "" : void 0,
156
+ "data-label-position": labelPosition,
157
+ "data-input-style": inputStyle,
158
+ "data-input-color": inputColor,
159
+ "data-card-variant": cardVariant,
160
+ "data-card-color": cardColor === "accent" ? "accent" : void 0
161
+ }), [
162
+ neutralColorAttr,
163
+ radius,
164
+ scaling,
165
+ spacing,
166
+ styleVariant,
167
+ highContrast,
168
+ labelPosition,
169
+ inputStyle,
170
+ inputColor,
171
+ cardVariant,
172
+ cardColor
173
+ ]);
174
+ const fontVars = react.useMemo(() => ({
175
+ ...fontHeading ? { "--spark-font-heading": fontHeading } : null,
176
+ ...fontBody ? { "--spark-font-body": fontBody } : null
177
+ }), [fontHeading, fontBody]);
122
178
  const element = (0, _base_ui_react_use_render.useRender)({
123
179
  defaultTagName: "div",
124
180
  render,
125
181
  ref,
126
182
  props: (0, _base_ui_react_merge_props.mergeProps)({
127
183
  className: (0, cn.cn)("spark-theme", appearance, className),
128
- "data-neutral-color": neutralColorAttr,
129
- "data-radius": radius,
130
- "data-scaling": scaling,
131
- "data-spacing": spacing,
132
- "data-style": styleVariant,
133
- "data-high-contrast": highContrast ? "" : void 0,
184
+ ...attrs,
134
185
  "data-inverted": inverted ? "" : void 0,
135
- "data-label-position": labelPosition,
136
- "data-input-style": inputStyle,
137
- "data-input-color": inputColor,
138
- "data-card-variant": cardVariant,
139
- "data-card-color": cardColor === "accent" ? "accent" : void 0,
140
186
  "data-spark-scope": accent.scoped ? scopeId : void 0,
141
187
  style: {
142
188
  ...accent.inlineVars,
143
189
  ...backgroundColor ? { backgroundColor: "var(--spark-background)" } : null,
144
- ...fontHeading ? { "--spark-font-heading": fontHeading } : null,
145
- ...fontBody ? { "--spark-font-body": fontBody } : null,
190
+ ...fontVars,
146
191
  ...style
147
192
  },
148
193
  children
149
194
  }, rest)
150
195
  });
151
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ThemeContext.Provider, {
196
+ const output = react.useMemo(() => {
197
+ const own = Object.fromEntries(Object.entries(attrs).filter((entry) => entry[1] !== void 0));
198
+ const parentScopes = parentOutput?.scopes ?? [];
199
+ return {
200
+ attrs: {
201
+ ...parentOutput?.attrs,
202
+ ...own
203
+ },
204
+ vars: {
205
+ ...parentOutput?.vars,
206
+ ...accent.portalVars,
207
+ ...fontVars
208
+ },
209
+ scopes: accent.portalScope ? [...parentScopes, accent.portalScope] : parentScopes
210
+ };
211
+ }, [
212
+ parentOutput,
213
+ attrs,
214
+ accent,
215
+ fontVars
216
+ ]);
217
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeContext.Provider, {
152
218
  value,
153
- children: [accent.css ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: accent.css }) : null, element]
219
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ThemeOutputContext.Provider, {
220
+ value: output,
221
+ children: [accent.css ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: accent.css }) : null, element]
222
+ })
154
223
  });
155
224
  });
225
+ /** Re-emits what the enclosing Themes rendered inside a portal, where CSS inheritance from them stops. */
226
+ function PortalTheme({ children }) {
227
+ const output = react.useContext(ThemeOutputContext);
228
+ const theme = useTheme();
229
+ const value = react.useMemo(() => ({
230
+ ...theme,
231
+ inverted: false
232
+ }), [theme]);
233
+ if (!output) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
234
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThemeContext.Provider, {
235
+ value,
236
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
237
+ "data-slot": "portal-theme",
238
+ className: (0, cn.cn)("spark-theme", theme.appearance),
239
+ ...output.attrs,
240
+ "data-spark-scope": output.scopes.length ? output.scopes.join(" ") : void 0,
241
+ style: {
242
+ display: "contents",
243
+ ...output.vars
244
+ },
245
+ children
246
+ })
247
+ });
248
+ }
156
249
  //#endregion
250
+ exports.PortalTheme = PortalTheme;
157
251
  exports.Theme = Theme;
158
252
  exports.useTheme = useTheme;
159
253
 
@@ -1 +1 @@
1
- {"version":3,"file":"theme.cjs","names":["React","resolveAutoNeutral","isSparkNeutralFamily","isParsableColor","getMatchingNeutral","CURATED_ACCENTS","appearanceForBackground","accentSparkVars","neutralSparkVars","generateAccentScale","invertAccentScale","neutralFamilyVars"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkNeutralFamily,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n return emit(inverted ? invertAccentScale(scale) : scale, opts != null)\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n return {\n inlineVars: toVars(build(value.appearance)),\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) =>\n bgAppearance === appearance\n ? build(appearance)\n : [...build(appearance), ...offCanvasNeutrals]\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const sel = `[data-spark-scope=\"${scopeId}\"]`\n const css = `${sel}{${light.join('')}}:is(.dark,.dark-theme) ${sel}{${dark.join('')}}`\n return { inlineVars: {} as React.CSSProperties, css, scoped: true }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-inverted': inverted ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n return (\n <ThemeContext.Provider value={value}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeContext.Provider>\n )\n },\n)\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme.cjs","names":["React","resolveAutoNeutral","isSparkNeutralFamily","isParsableColor","getMatchingNeutral","CURATED_ACCENTS","appearanceForBackground","accentSparkVars","neutralSparkVars","generateAccentScale","invertAccentScale","neutralFamilyVars"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkNeutralFamily,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** What a Theme actually wrote to the DOM, merged over its ancestors (nearest wins). */\ninterface ThemeOutput {\n attrs: Record<string, string>\n vars: Record<string, string>\n scopes: string[]\n}\n\nconst ThemeOutputContext = React.createContext<ThemeOutput | null>(null)\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n const parentOutput = React.useContext(ThemeOutputContext)\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n portalVars: {} as React.CSSProperties,\n portalScope: undefined as string | undefined,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n const rendered = emit(\n inverted ? invertAccentScale(scale) : scale,\n opts != null,\n )\n // Popups sit on their own surface, so they get the scale before the inverted re-cast.\n const plain = inverted ? emit(scale, opts != null) : rendered\n return { rendered, plain }\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n const { rendered, plain } = build(value.appearance)\n const inlineVars = toVars(rendered)\n return {\n inlineVars,\n portalVars: inverted ? toVars(plain) : inlineVars,\n portalScope: undefined,\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) => {\n const { rendered, plain } = build(appearance)\n const extra = bgAppearance === appearance ? [] : offCanvasNeutrals\n return {\n rendered: [...rendered, ...extra],\n plain: [...plain, ...extra],\n }\n }\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const rule = (\n id: string,\n lightDecls: string[],\n darkDecls: string[],\n ) => {\n const sel = `[data-spark-scope~=\"${id}\"]`\n return `${sel}{${lightDecls.join('')}}:is(.dark,.dark-theme) ${sel}{${darkDecls.join('')}}`\n }\n const portalScope = inverted ? `${scopeId}p` : scopeId\n const css =\n rule(scopeId, light.rendered, dark.rendered) +\n (inverted ? rule(portalScope, light.plain, dark.plain) : '')\n return {\n inlineVars: {} as React.CSSProperties,\n portalVars: {} as React.CSSProperties,\n portalScope,\n css,\n scoped: true,\n }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const attrs = React.useMemo(\n () => ({\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n }),\n [\n neutralColorAttr,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n ],\n )\n const fontVars = React.useMemo(\n () => ({\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n }),\n [fontHeading, fontBody],\n )\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n ...attrs,\n 'data-inverted': inverted ? '' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...fontVars,\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n const output = React.useMemo<ThemeOutput>(() => {\n // An unset own knob must not blank out the ancestor's value it inherits.\n const own = Object.fromEntries(\n Object.entries(attrs).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n )\n const parentScopes = parentOutput?.scopes ?? []\n return {\n attrs: { ...parentOutput?.attrs, ...own },\n vars: {\n ...parentOutput?.vars,\n ...(accent.portalVars as Record<string, string>),\n ...fontVars,\n },\n scopes: accent.portalScope\n ? [...parentScopes, accent.portalScope]\n : parentScopes,\n }\n }, [parentOutput, attrs, accent, fontVars])\n\n return (\n <ThemeContext.Provider value={value}>\n <ThemeOutputContext.Provider value={output}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeOutputContext.Provider>\n </ThemeContext.Provider>\n )\n },\n)\n\n/** Re-emits what the enclosing Themes rendered inside a portal, where CSS inheritance from them stops. */\nexport function PortalTheme({ children }: { children: React.ReactNode }) {\n const output = React.useContext(ThemeOutputContext)\n const theme = useTheme()\n // A popup sits on its own surface, never on the solid accent `inverted` is for.\n const value = React.useMemo(() => ({ ...theme, inverted: false }), [theme])\n if (!output) return <>{children}</>\n return (\n <ThemeContext.Provider value={value}>\n <div\n data-slot=\"portal-theme\"\n className={cn('spark-theme', theme.appearance)}\n {...output.attrs}\n data-spark-scope={\n output.scopes.length ? output.scopes.join(' ') : undefined\n }\n style={{ display: 'contents', ...output.vars }}\n >\n {children}\n </div>\n </ThemeContext.Provider>\n )\n}\n"],"mappings":"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"}
@@ -56,6 +56,12 @@ declare function useTheme(): ThemeValue;
56
56
  interface ThemeProps extends useRender.ComponentProps<'div'>, ThemeValue {}
57
57
  /** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
58
58
  declare const Theme: React$1.ForwardRefExoticComponent<Omit<ThemeProps, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
59
+ /** Re-emits what the enclosing Themes rendered inside a portal, where CSS inheritance from them stops. */
60
+ declare function PortalTheme({
61
+ children
62
+ }: {
63
+ children: React$1.ReactNode;
64
+ }): import("react/jsx-runtime").JSX.Element;
59
65
  //#endregion
60
- export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
66
+ export { PortalTheme, type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
61
67
  //# sourceMappingURL=theme.d.cts.map
@@ -56,6 +56,12 @@ declare function useTheme(): ThemeValue;
56
56
  interface ThemeProps extends useRender.ComponentProps<'div'>, ThemeValue {}
57
57
  /** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
58
58
  declare const Theme: React$1.ForwardRefExoticComponent<Omit<ThemeProps, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
59
+ /** Re-emits what the enclosing Themes rendered inside a portal, where CSS inheritance from them stops. */
60
+ declare function PortalTheme({
61
+ children
62
+ }: {
63
+ children: React$1.ReactNode;
64
+ }): import("react/jsx-runtime").JSX.Element;
59
65
  //#endregion
60
- export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
66
+ export { PortalTheme, type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
61
67
  //# sourceMappingURL=theme.d.ts.map
@@ -1,16 +1,17 @@
1
1
  "use client";
2
2
  "use client";
3
+ import { cn } from "../utils/cn.js";
3
4
  import { isSparkNeutralFamily } from "../lib/theme-types.js";
4
5
  import { CURATED_ACCENTS, accentSparkVars, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, resolveAutoNeutral } from "../lib/accent-theme.js";
5
- import { cn } from "../utils/cn.js";
6
6
  import * as React$1 from "react";
7
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { mergeProps } from "@base-ui/react/merge-props";
9
9
  import { useRender } from "@base-ui/react/use-render";
10
10
  //#region src/components/theme.tsx
11
11
  /** The epilot brand accent — the scale `spark-default.css` ships statically. */
12
12
  const DEFAULT_ACCENT = "#005EB4";
13
13
  const ThemeContext = React$1.createContext({});
14
+ const ThemeOutputContext = React$1.createContext(null);
14
15
  /** Merged theme values from enclosing `<Theme>` providers. */
15
16
  function useTheme() {
16
17
  return React$1.useContext(ThemeContext);
@@ -18,6 +19,7 @@ function useTheme() {
18
19
  /** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
19
20
  const Theme = React$1.forwardRef(function Theme({ accentColor, backgroundColor, neutralColor, radius, scaling, spacing, styleVariant, highContrast, inverted, labelPosition, inputStyle, inputColor, cardVariant, cardColor, appearance, fontHeading, fontBody, render, className, style, children, ...rest }, ref) {
20
21
  const parent = useTheme();
22
+ const parentOutput = React$1.useContext(ThemeOutputContext);
21
23
  const value = React$1.useMemo(() => ({
22
24
  accentColor: accentColor ?? parent.accentColor,
23
25
  backgroundColor: backgroundColor ?? parent.backgroundColor,
@@ -65,6 +67,8 @@ const Theme = React$1.forwardRef(function Theme({ accentColor, backgroundColor,
65
67
  const accent = React$1.useMemo(() => {
66
68
  const empty = {
67
69
  inlineVars: {},
70
+ portalVars: {},
71
+ portalScope: void 0,
68
72
  css: "",
69
73
  scoped: false
70
74
  };
@@ -86,21 +90,44 @@ const Theme = React$1.forwardRef(function Theme({ accentColor, backgroundColor,
86
90
  const build = (appearance) => {
87
91
  const opts = optsFor(appearance);
88
92
  const scale = generateAccentScale(hex, appearance, opts);
89
- return emit(inverted ? invertAccentScale(scale) : scale, opts != null);
90
- };
91
- if (value.appearance) return {
92
- inlineVars: toVars(build(value.appearance)),
93
- css: "",
94
- scoped: false
93
+ const rendered = emit(inverted ? invertAccentScale(scale) : scale, opts != null);
94
+ return {
95
+ rendered,
96
+ plain: inverted ? emit(scale, opts != null) : rendered
97
+ };
95
98
  };
99
+ if (value.appearance) {
100
+ const { rendered, plain } = build(value.appearance);
101
+ const inlineVars = toVars(rendered);
102
+ return {
103
+ inlineVars,
104
+ portalVars: inverted ? toVars(plain) : inlineVars,
105
+ portalScope: void 0,
106
+ css: "",
107
+ scoped: false
108
+ };
109
+ }
96
110
  const offCanvasNeutrals = backgroundColor ? neutralFamilyVars(resolvedNeutralColor ?? "slate") : [];
97
- const scaleFor = (appearance) => bgAppearance === appearance ? build(appearance) : [...build(appearance), ...offCanvasNeutrals];
111
+ const scaleFor = (appearance) => {
112
+ const { rendered, plain } = build(appearance);
113
+ const extra = bgAppearance === appearance ? [] : offCanvasNeutrals;
114
+ return {
115
+ rendered: [...rendered, ...extra],
116
+ plain: [...plain, ...extra]
117
+ };
118
+ };
98
119
  const light = scaleFor("light");
99
120
  const dark = scaleFor("dark");
100
- const sel = `[data-spark-scope="${scopeId}"]`;
121
+ const rule = (id, lightDecls, darkDecls) => {
122
+ const sel = `[data-spark-scope~="${id}"]`;
123
+ return `${sel}{${lightDecls.join("")}}:is(.dark,.dark-theme) ${sel}{${darkDecls.join("")}}`;
124
+ };
125
+ const portalScope = inverted ? `${scopeId}p` : scopeId;
101
126
  return {
102
127
  inlineVars: {},
103
- css: `${sel}{${light.join("")}}:is(.dark,.dark-theme) ${sel}{${dark.join("")}}`,
128
+ portalVars: {},
129
+ portalScope,
130
+ css: rule(scopeId, light.rendered, dark.rendered) + (inverted ? rule(portalScope, light.plain, dark.plain) : ""),
104
131
  scoped: true
105
132
  };
106
133
  } catch (error) {
@@ -116,41 +143,107 @@ const Theme = React$1.forwardRef(function Theme({ accentColor, backgroundColor,
116
143
  value.appearance,
117
144
  scopeId
118
145
  ]);
146
+ const attrs = React$1.useMemo(() => ({
147
+ "data-neutral-color": neutralColorAttr,
148
+ "data-radius": radius,
149
+ "data-scaling": scaling,
150
+ "data-spacing": spacing,
151
+ "data-style": styleVariant,
152
+ "data-high-contrast": highContrast ? "" : void 0,
153
+ "data-label-position": labelPosition,
154
+ "data-input-style": inputStyle,
155
+ "data-input-color": inputColor,
156
+ "data-card-variant": cardVariant,
157
+ "data-card-color": cardColor === "accent" ? "accent" : void 0
158
+ }), [
159
+ neutralColorAttr,
160
+ radius,
161
+ scaling,
162
+ spacing,
163
+ styleVariant,
164
+ highContrast,
165
+ labelPosition,
166
+ inputStyle,
167
+ inputColor,
168
+ cardVariant,
169
+ cardColor
170
+ ]);
171
+ const fontVars = React$1.useMemo(() => ({
172
+ ...fontHeading ? { "--spark-font-heading": fontHeading } : null,
173
+ ...fontBody ? { "--spark-font-body": fontBody } : null
174
+ }), [fontHeading, fontBody]);
119
175
  const element = useRender({
120
176
  defaultTagName: "div",
121
177
  render,
122
178
  ref,
123
179
  props: mergeProps({
124
180
  className: cn("spark-theme", appearance, className),
125
- "data-neutral-color": neutralColorAttr,
126
- "data-radius": radius,
127
- "data-scaling": scaling,
128
- "data-spacing": spacing,
129
- "data-style": styleVariant,
130
- "data-high-contrast": highContrast ? "" : void 0,
181
+ ...attrs,
131
182
  "data-inverted": inverted ? "" : void 0,
132
- "data-label-position": labelPosition,
133
- "data-input-style": inputStyle,
134
- "data-input-color": inputColor,
135
- "data-card-variant": cardVariant,
136
- "data-card-color": cardColor === "accent" ? "accent" : void 0,
137
183
  "data-spark-scope": accent.scoped ? scopeId : void 0,
138
184
  style: {
139
185
  ...accent.inlineVars,
140
186
  ...backgroundColor ? { backgroundColor: "var(--spark-background)" } : null,
141
- ...fontHeading ? { "--spark-font-heading": fontHeading } : null,
142
- ...fontBody ? { "--spark-font-body": fontBody } : null,
187
+ ...fontVars,
143
188
  ...style
144
189
  },
145
190
  children
146
191
  }, rest)
147
192
  });
148
- return /* @__PURE__ */ jsxs(ThemeContext.Provider, {
193
+ const output = React$1.useMemo(() => {
194
+ const own = Object.fromEntries(Object.entries(attrs).filter((entry) => entry[1] !== void 0));
195
+ const parentScopes = parentOutput?.scopes ?? [];
196
+ return {
197
+ attrs: {
198
+ ...parentOutput?.attrs,
199
+ ...own
200
+ },
201
+ vars: {
202
+ ...parentOutput?.vars,
203
+ ...accent.portalVars,
204
+ ...fontVars
205
+ },
206
+ scopes: accent.portalScope ? [...parentScopes, accent.portalScope] : parentScopes
207
+ };
208
+ }, [
209
+ parentOutput,
210
+ attrs,
211
+ accent,
212
+ fontVars
213
+ ]);
214
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, {
149
215
  value,
150
- children: [accent.css ? /* @__PURE__ */ jsx("style", { children: accent.css }) : null, element]
216
+ children: /* @__PURE__ */ jsxs(ThemeOutputContext.Provider, {
217
+ value: output,
218
+ children: [accent.css ? /* @__PURE__ */ jsx("style", { children: accent.css }) : null, element]
219
+ })
151
220
  });
152
221
  });
222
+ /** Re-emits what the enclosing Themes rendered inside a portal, where CSS inheritance from them stops. */
223
+ function PortalTheme({ children }) {
224
+ const output = React$1.useContext(ThemeOutputContext);
225
+ const theme = useTheme();
226
+ const value = React$1.useMemo(() => ({
227
+ ...theme,
228
+ inverted: false
229
+ }), [theme]);
230
+ if (!output) return /* @__PURE__ */ jsx(Fragment, { children });
231
+ return /* @__PURE__ */ jsx(ThemeContext.Provider, {
232
+ value,
233
+ children: /* @__PURE__ */ jsx("div", {
234
+ "data-slot": "portal-theme",
235
+ className: cn("spark-theme", theme.appearance),
236
+ ...output.attrs,
237
+ "data-spark-scope": output.scopes.length ? output.scopes.join(" ") : void 0,
238
+ style: {
239
+ display: "contents",
240
+ ...output.vars
241
+ },
242
+ children
243
+ })
244
+ });
245
+ }
153
246
  //#endregion
154
- export { Theme, useTheme };
247
+ export { PortalTheme, Theme, useTheme };
155
248
 
156
249
  //# sourceMappingURL=theme.js.map