@epilot/spark-ui 0.10.0 → 0.11.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 (122) hide show
  1. package/README.md +9 -1
  2. package/dist/components/avatar.d.cts +1 -1
  3. package/dist/components/avatar.d.ts +1 -1
  4. package/dist/components/badge.cjs +1 -1
  5. package/dist/components/badge.cjs.map +1 -1
  6. package/dist/components/badge.d.cts +1 -1
  7. package/dist/components/badge.d.ts +1 -1
  8. package/dist/components/badge.js +1 -1
  9. package/dist/components/badge.js.map +1 -1
  10. package/dist/components/bar-chart.cjs +4 -7
  11. package/dist/components/bar-chart.cjs.map +1 -1
  12. package/dist/components/bar-chart.js +4 -7
  13. package/dist/components/bar-chart.js.map +1 -1
  14. package/dist/components/battery-progress-row.cjs +6 -7
  15. package/dist/components/battery-progress-row.cjs.map +1 -1
  16. package/dist/components/battery-progress-row.js +6 -7
  17. package/dist/components/battery-progress-row.js.map +1 -1
  18. package/dist/components/box.cjs +42 -0
  19. package/dist/components/box.cjs.map +1 -0
  20. package/dist/components/box.d.cts +35 -0
  21. package/dist/components/box.d.ts +35 -0
  22. package/dist/components/box.js +41 -0
  23. package/dist/components/box.js.map +1 -0
  24. package/dist/components/bubble.d.cts +1 -1
  25. package/dist/components/button-group.d.cts +1 -1
  26. package/dist/components/button-group.d.ts +1 -1
  27. package/dist/components/button.cjs +1 -1
  28. package/dist/components/button.cjs.map +1 -1
  29. package/dist/components/button.d.cts +2 -2
  30. package/dist/components/button.d.ts +2 -2
  31. package/dist/components/button.js +1 -1
  32. package/dist/components/button.js.map +1 -1
  33. package/dist/components/card.cjs +10 -14
  34. package/dist/components/card.cjs.map +1 -1
  35. package/dist/components/card.d.cts +6 -3
  36. package/dist/components/card.d.ts +6 -3
  37. package/dist/components/card.js +10 -14
  38. package/dist/components/card.js.map +1 -1
  39. package/dist/components/checkbox.cjs +4 -2
  40. package/dist/components/checkbox.cjs.map +1 -1
  41. package/dist/components/checkbox.d.cts +1 -1
  42. package/dist/components/checkbox.d.ts +1 -1
  43. package/dist/components/checkbox.js +4 -2
  44. package/dist/components/checkbox.js.map +1 -1
  45. package/dist/components/empty-state.cjs +4 -7
  46. package/dist/components/empty-state.cjs.map +1 -1
  47. package/dist/components/empty-state.js +4 -7
  48. package/dist/components/empty-state.js.map +1 -1
  49. package/dist/components/field.cjs +43 -10
  50. package/dist/components/field.cjs.map +1 -1
  51. package/dist/components/field.d.cts +19 -2
  52. package/dist/components/field.d.ts +19 -2
  53. package/dist/components/field.js +43 -11
  54. package/dist/components/field.js.map +1 -1
  55. package/dist/components/icon.cjs +12 -9
  56. package/dist/components/icon.cjs.map +1 -1
  57. package/dist/components/icon.d.cts +6 -2
  58. package/dist/components/icon.d.ts +6 -2
  59. package/dist/components/icon.js +12 -9
  60. package/dist/components/icon.js.map +1 -1
  61. package/dist/components/input.d.cts +1 -1
  62. package/dist/components/input.d.ts +1 -1
  63. package/dist/components/marker.d.cts +1 -1
  64. package/dist/components/marker.d.ts +1 -1
  65. package/dist/components/metering-row.cjs +28 -11
  66. package/dist/components/metering-row.cjs.map +1 -1
  67. package/dist/components/metering-row.js +28 -11
  68. package/dist/components/metering-row.js.map +1 -1
  69. package/dist/components/pill.cjs +1 -1
  70. package/dist/components/pill.cjs.map +1 -1
  71. package/dist/components/pill.d.cts +1 -1
  72. package/dist/components/pill.d.ts +1 -1
  73. package/dist/components/pill.js +1 -1
  74. package/dist/components/pill.js.map +1 -1
  75. package/dist/components/radio.cjs +4 -2
  76. package/dist/components/radio.cjs.map +1 -1
  77. package/dist/components/radio.d.cts +3 -1
  78. package/dist/components/radio.d.ts +3 -1
  79. package/dist/components/radio.js +4 -2
  80. package/dist/components/radio.js.map +1 -1
  81. package/dist/components/segmented-control.d.cts +1 -1
  82. package/dist/components/segmented-control.d.ts +1 -1
  83. package/dist/components/select.d.cts +1 -1
  84. package/dist/components/select.d.ts +1 -1
  85. package/dist/components/switch.cjs +3 -1
  86. package/dist/components/switch.cjs.map +1 -1
  87. package/dist/components/switch.d.cts +3 -1
  88. package/dist/components/switch.d.ts +3 -1
  89. package/dist/components/switch.js +3 -1
  90. package/dist/components/switch.js.map +1 -1
  91. package/dist/components/textarea.d.cts +1 -1
  92. package/dist/components/textarea.d.ts +1 -1
  93. package/dist/components/theme.cjs +35 -9
  94. package/dist/components/theme.cjs.map +1 -1
  95. package/dist/components/theme.d.cts +12 -1
  96. package/dist/components/theme.d.ts +12 -1
  97. package/dist/components/theme.js +36 -10
  98. package/dist/components/theme.js.map +1 -1
  99. package/dist/icons/material-symbol.cjs +1 -0
  100. package/dist/icons/material-symbol.cjs.map +1 -1
  101. package/dist/icons/material-symbol.js +1 -0
  102. package/dist/icons/material-symbol.js.map +1 -1
  103. package/dist/lib/accent-theme.cjs +102 -14
  104. package/dist/lib/accent-theme.cjs.map +1 -1
  105. package/dist/lib/accent-theme.d.cts +29 -4
  106. package/dist/lib/accent-theme.d.ts +29 -4
  107. package/dist/lib/accent-theme.js +100 -15
  108. package/dist/lib/accent-theme.js.map +1 -1
  109. package/dist/lib/container-chrome.cjs +53 -0
  110. package/dist/lib/container-chrome.cjs.map +1 -0
  111. package/dist/lib/container-chrome.d.cts +15 -0
  112. package/dist/lib/container-chrome.d.ts +15 -0
  113. package/dist/lib/container-chrome.js +48 -0
  114. package/dist/lib/container-chrome.js.map +1 -0
  115. package/dist/lib/input-box-variants.d.cts +1 -1
  116. package/dist/lib/input-box-variants.d.ts +1 -1
  117. package/dist/lib/theme-types.d.cts +1 -1
  118. package/dist/lib/theme-types.d.ts +1 -1
  119. package/dist/styles/spark-default.css +34 -8
  120. package/dist/{theme-types-8wjgiNA_.d.ts → theme-types-1yB0d8mp.d.cts} +6 -6
  121. package/dist/{theme-types-8wjgiNA_.d.cts → theme-types-1yB0d8mp.d.ts} +6 -6
  122. package/package.json +1 -1
@@ -33,24 +33,86 @@ const DEFAULTS = {
33
33
  background: "#000000"
34
34
  }
35
35
  };
36
+ /** Radix step-9 per neutral family — the generator's `gray` seed, so a regenerated
37
+ neutral lands on the family `[data-neutral-color]` would have selected statically. */
38
+ const NEUTRAL_BASE = {
39
+ slate: _radix_ui_colors.slate.slate9,
40
+ mauve: _radix_ui_colors.mauve.mauve9,
41
+ gray: _radix_ui_colors.gray.gray9,
42
+ sage: _radix_ui_colors.sage.sage9,
43
+ olive: _radix_ui_colors.olive.olive9,
44
+ sand: _radix_ui_colors.sand.sand9
45
+ };
46
+ /** Which appearance a canvas belongs to, by OKLCH lightness. */
47
+ function appearanceForBackground(background) {
48
+ const [lightness] = new colorjs_io.default(background).to("oklch").coords;
49
+ return (lightness ?? 1) >= .5 ? "light" : "dark";
50
+ }
51
+ /** deltaE2000 ≈ 2 is the just-noticeable bar — a good solve lands far inside it. */
52
+ const ALPHA_SOLVE_TOLERANCE = 2;
53
+ /**
54
+ * Replace alpha steps the solver could not actually solve with their opaque solid.
55
+ *
56
+ * Each `aN` is solved so that compositing it over the background reproduces step N
57
+ * (`target = bg·(1-α) + fg·α`). From a saturated canvas a desaturated ink target is
58
+ * unreachable within alpha bounds, and Radix's solver **degrades silently** rather than
59
+ * failing — on `#FFD400` it returns `#0000ff39` for slate a11, which composites to
60
+ * 1.66:1 body text where the solid step is a correct 9.03:1.
61
+ *
62
+ * So verify the solve and fall back to the opaque value where it doesn't hold. Only ever
63
+ * called for a custom background; on white/black every step solves exactly (deltaE ~0),
64
+ * so the default output — and portal parity — is untouched either way.
65
+ */
66
+ function opaqueWhereUnsolvable(background, solid, alpha) {
67
+ let bg;
68
+ try {
69
+ bg = new colorjs_io.default(background).to("srgb").coords;
70
+ } catch {
71
+ return alpha;
72
+ }
73
+ return alpha.map((step, i) => {
74
+ const target = solid[i];
75
+ if (!target) return step;
76
+ try {
77
+ const ink = new colorjs_io.default(step);
78
+ const fg = ink.to("srgb").coords;
79
+ const a = ink.alpha;
80
+ return new colorjs_io.default("srgb", [
81
+ 0,
82
+ 1,
83
+ 2
84
+ ].map((k) => bg[k] * (1 - a) + fg[k] * a)).deltaE(new colorjs_io.default(target), "2000") <= ALPHA_SOLVE_TOLERANCE ? step : target;
85
+ } catch {
86
+ return target;
87
+ }
88
+ });
89
+ }
36
90
  /** Generate the Radix-style accent scale for one appearance from a single accent color. */
37
- function generateAccentScale(accent, appearance) {
91
+ function generateAccentScale(accent, appearance, opts) {
92
+ const background = opts?.background ?? DEFAULTS[appearance].background;
38
93
  const scale = require_lib_generate_radix_colors.generateRadixColors({
39
94
  appearance,
40
95
  accent,
41
- gray: DEFAULTS[appearance].gray,
42
- background: DEFAULTS[appearance].background
96
+ gray: opts?.neutral ? NEUTRAL_BASE[opts.neutral] : DEFAULTS[appearance].gray,
97
+ background
43
98
  });
44
- return {
99
+ const solved = opts?.background ? {
45
100
  ...scale,
46
- accentContrast: readableContrast(scale, MIN_TEXT_CONTRAST),
47
- accentOnSolid: readableContrast(scale, MIN_GRAPHIC_CONTRAST)
101
+ accentScaleAlpha: opaqueWhereUnsolvable(background, scale.accentScale, scale.accentScaleAlpha),
102
+ accentScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.accentScaleWideGamut, scale.accentScaleAlphaWideGamut),
103
+ grayScaleAlpha: opaqueWhereUnsolvable(background, scale.grayScale, scale.grayScaleAlpha),
104
+ grayScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut)
105
+ } : scale;
106
+ return {
107
+ ...solved,
108
+ accentContrast: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_TEXT_CONTRAST),
109
+ accentOnSolid: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_GRAPHIC_CONTRAST)
48
110
  };
49
111
  }
50
112
  const MIN_TEXT_CONTRAST = 4.5;
51
113
  const MIN_GRAPHIC_CONTRAST = 3;
52
- function readableContrast(scale, min) {
53
- const step9 = new colorjs_io.default(scale.accentScale[8]);
114
+ function readableContrast(step9Hex, native, min) {
115
+ const step9 = new colorjs_io.default(step9Hex);
54
116
  const [, chroma, hue] = step9.to("oklch").coords;
55
117
  const tinted = (lightness) => new colorjs_io.default("oklch", [
56
118
  lightness,
@@ -58,7 +120,7 @@ function readableContrast(scale, min) {
58
120
  hue ?? 0
59
121
  ]).to("srgb").toString({ format: "hex" });
60
122
  const candidates = [
61
- scale.accentContrast,
123
+ native,
62
124
  tinted(.25),
63
125
  tinted(.15)
64
126
  ];
@@ -107,24 +169,50 @@ function getMatchingNeutral(accent) {
107
169
  return "slate";
108
170
  }
109
171
  }
172
+ /** The 27 tokens every scale family declares, under an arbitrary prefix. */
173
+ function scaleVars(prefix, steps, alpha, contrast, onSolid, surface) {
174
+ return [
175
+ ...steps.map((c, i) => `--${prefix}-${i + 1}: ${c};`),
176
+ ...alpha.map((c, i) => `--${prefix}-a${i + 1}: ${c};`),
177
+ `--${prefix}-contrast: ${contrast};`,
178
+ `--${prefix}-on-solid: ${onSolid};`,
179
+ `--${prefix}-surface: ${surface};`
180
+ ];
181
+ }
110
182
  function sparkVars(steps, alpha, contrast, onSolid, surface, background) {
111
183
  const indicator = steps[8];
112
184
  return [
113
- ...steps.map((c, i) => `--spark-${i + 1}: ${c};`),
114
- ...alpha.map((c, i) => `--spark-a${i + 1}: ${c};`),
115
- `--spark-contrast: ${contrast};`,
116
- `--spark-on-solid: ${onSolid};`,
117
- `--spark-surface: ${surface};`,
185
+ ...scaleVars("spark", steps, alpha, contrast, onSolid, surface),
118
186
  `--spark-background: ${background};`,
119
187
  `--spark-indicator: ${indicator};`,
120
188
  `--spark-track: ${indicator};`
121
189
  ];
122
190
  }
191
+ /**
192
+ * `--neutral-*` declarations from the gray scale the generator produces alongside the
193
+ * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
194
+ * static Radix family. Worth consuming only when the canvas is custom: the static
195
+ * families' alpha steps are pre-baked against white/black, so on any other background
196
+ * they composite wrong (every gray text/border reads through `--neutral-a*`).
197
+ *
198
+ * `--neutral-surface` is step 2, matching the static families' own convention rather
199
+ * than the generator's translucent `graySurface`.
200
+ */
201
+ function neutralSparkVars(scale) {
202
+ return scaleVars("neutral", scale.grayScale, scale.grayScaleAlpha, readableContrast(scale.grayScale[8], "#fff", MIN_TEXT_CONTRAST), readableContrast(scale.grayScale[8], "#fff", MIN_GRAPHIC_CONTRAST), scale.grayScale[1]);
203
+ }
204
+ /** Wide-gamut (P3) twin of `neutralSparkVars`. */
205
+ function neutralSparkVarsP3(scale) {
206
+ return scaleVars("neutral", scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut, readableContrast(scale.grayScale[8], "#fff", MIN_TEXT_CONTRAST), readableContrast(scale.grayScale[8], "#fff", MIN_GRAPHIC_CONTRAST), scale.grayScaleWideGamut[1]);
207
+ }
123
208
  //#endregion
124
209
  exports.CURATED_ACCENTS = CURATED_ACCENTS;
125
210
  exports.accentSparkVars = accentSparkVars;
126
211
  exports.accentSparkVarsP3 = accentSparkVarsP3;
212
+ exports.appearanceForBackground = appearanceForBackground;
127
213
  exports.generateAccentScale = generateAccentScale;
128
214
  exports.getMatchingNeutral = getMatchingNeutral;
215
+ exports.neutralSparkVars = neutralSparkVars;
216
+ exports.neutralSparkVarsP3 = neutralSparkVarsP3;
129
217
 
130
218
  //# sourceMappingURL=accent-theme.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"accent-theme.cjs","names":["RadixColors","generateRadixColors","Color"],"sources":["../../src/lib/accent-theme.ts"],"sourcesContent":["// Accent is the one runtime knob (colour math, not a CSS-var swap): generate the\n// `--spark-*` scale with the same Radix algorithm live portals use, apply as inline vars.\nimport * as RadixColors from '@radix-ui/colors'\nimport Color from 'colorjs.io'\nimport { generateRadixColors } from './generate-radix-colors'\nimport type { SparkNeutral } from './theme-types'\n\n/** Shape returned by the vendored Radix generator (it doesn't export a named type). */\nexport type GeneratedColors = ReturnType<typeof generateRadixColors>\n\n/**\n * generateAccentScale output: the raw scale, but with two readable foregrounds on the\n * solid (step-9) fill, each meeting a different WCAG bar:\n * - `accentContrast` — 4.5:1 (WCAG 1.4.3), for button/label **text**.\n * - `accentOnSolid` — 3:1 (WCAG 1.4.11), for a **graphical glyph** (the checkbox check).\n * A checkmark is graphical, not text, so it uses the looser-but-correct 3:1 bar: white\n * where it clears 3:1 (most accents, incl. neutral & red — the convention), and a dark\n * accent-tint where white is too weak (e.g. orange). Using the 4.5:1 text contrast for\n * the glyph over-darkened it (a black tick on a red/neutral checkbox).\n */\nexport type SparkAccentScale = GeneratedColors & { accentOnSolid: string }\n\nexport type Appearance = 'light' | 'dark'\n\n/** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */\nexport const CURATED_ACCENTS: Record<string, string> = {\n blue: RadixColors.blue.blue9,\n indigo: RadixColors.indigo.indigo9,\n violet: RadixColors.violet.violet9,\n purple: RadixColors.purple.purple9,\n cyan: RadixColors.cyan.cyan9,\n teal: RadixColors.teal.teal9,\n green: RadixColors.green.green9,\n grass: RadixColors.grass.grass9,\n orange: RadixColors.orange.orange9,\n red: RadixColors.red.red9,\n pink: RadixColors.pink.pink9,\n gray: RadixColors.gray.gray9,\n}\n\n/** ECP `DesignUtils` generator inputs, so a generated scale matches live portals. */\nconst DEFAULTS: Record<Appearance, { gray: string; background: string }> = {\n light: { gray: '#333333', background: '#ffffff' },\n // Matches ECP DesignUtils' DEFAULT_DARK_BACKGROUND_COLOR -- the alpha ramp\n // is composited against this, so it must match what's actually painted.\n dark: { gray: '#8b8d98', background: '#000000' },\n}\n\n/** Generate the Radix-style accent scale for one appearance from a single accent color. */\nexport function generateAccentScale(\n accent: string,\n appearance: Appearance,\n): SparkAccentScale {\n const scale = generateRadixColors({\n appearance,\n accent,\n gray: DEFAULTS[appearance].gray,\n background: DEFAULTS[appearance].background,\n })\n // accentContrast → 4.5:1 (button text); accentOnSolid → 3:1 (graphical glyph,\n // see SparkAccentScale). Same picker, different WCAG threshold.\n return {\n ...scale,\n accentContrast: readableContrast(scale, MIN_TEXT_CONTRAST),\n accentOnSolid: readableContrast(scale, MIN_GRAPHIC_CONTRAST),\n }\n}\n\nconst MIN_TEXT_CONTRAST = 4.5 // WCAG 1.4.3 — button / label text\nconst MIN_GRAPHIC_CONTRAST = 3 // WCAG 1.4.11 — a check / indicator glyph on a solid fill\n\n// Pick the readable foreground on step 9: prefer the generator's native contrast\n// (usually white), else fall to progressively darker accent-tinted inks until one\n// clears `min`. Text uses 4.5:1; a graphical glyph (the checkmark) uses 3:1 — which\n// keeps white on most accents (incl. neutral 3.32:1, red 3.91:1) but flips to a dark\n// tint where white is too weak (e.g. orange, 2.97:1). The vendored generator keeps\n// white at APCA Lc >= 40, which can sit below both WCAG thresholds — hence this guard.\nfunction readableContrast(scale: GeneratedColors, min: number): string {\n const step9 = new Color(scale.accentScale[8]!)\n const [, chroma, hue] = step9.to('oklch').coords\n const tinted = (lightness: number) =>\n new Color('oklch', [\n lightness,\n Math.max(0.08 * (chroma ?? 0), 0.04),\n hue ?? 0,\n ])\n .to('srgb')\n .toString({ format: 'hex' })\n const candidates = [scale.accentContrast, tinted(0.25), tinted(0.15)]\n const ratios = candidates.map((c) => new Color(c).contrast(step9, 'WCAG21'))\n const passing = ratios.findIndex((r) => r >= min)\n if (passing !== -1) return candidates[passing]!\n return candidates[ratios.indexOf(Math.max(...ratios))]!\n}\n\n/** sRGB `--spark-*` declarations for a generated accent scale. */\nexport function accentSparkVars(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScale,\n scale.accentScaleAlpha,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurface,\n scale.background,\n )\n}\n\n/** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */\nexport function accentSparkVarsP3(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurfaceWideGamut,\n scale.background,\n )\n}\n\n/** Radix's `grayColor=\"auto\"` pairing (accent family → gray), mapped to our 6 neutrals. */\nconst NEUTRAL_BY_ACCENT: Record<string, SparkNeutral> = {\n blue: 'slate',\n indigo: 'slate',\n cyan: 'slate',\n violet: 'mauve',\n purple: 'mauve',\n pink: 'mauve',\n red: 'mauve',\n teal: 'sage',\n green: 'sage',\n grass: 'olive',\n orange: 'sand',\n gray: 'gray',\n}\n\n/** Radix's \"auto\" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */\nexport function getMatchingNeutral(accent: string): SparkNeutral {\n const byName = NEUTRAL_BY_ACCENT[accent]\n if (byName) return byName\n try {\n const [, chroma, hueRaw] = new Color(accent).to('oklch').coords\n if (!chroma || chroma < 0.04) return 'gray'\n const hue = (((hueRaw ?? 0) % 360) + 360) % 360\n if (hue >= 300 || hue < 35) return 'mauve'\n if (hue < 115) return 'sand'\n if (hue < 145) return 'olive'\n if (hue < 200) return 'sage'\n return 'slate'\n } catch {\n return 'slate'\n }\n}\n\nfunction sparkVars(\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n background: string,\n): string[] {\n const indicator = steps[8]! // step 9 — the brand step (matches --spark-indicator/-track)\n return [\n ...steps.map((c, i) => `--spark-${i + 1}: ${c};`),\n ...alpha.map((c, i) => `--spark-a${i + 1}: ${c};`),\n `--spark-contrast: ${contrast};`,\n `--spark-on-solid: ${onSolid};`,\n `--spark-surface: ${surface};`,\n `--spark-background: ${background};`,\n `--spark-indicator: ${indicator};`,\n `--spark-track: ${indicator};`,\n ]\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"accent-theme.cjs","names":["RadixColors","Color","generateRadixColors"],"sources":["../../src/lib/accent-theme.ts"],"sourcesContent":["// Accent is the one runtime knob (colour math, not a CSS-var swap): generate the\n// `--spark-*` scale with the same Radix algorithm live portals use, apply as inline vars.\nimport * as RadixColors from '@radix-ui/colors'\nimport Color from 'colorjs.io'\nimport { generateRadixColors } from './generate-radix-colors'\nimport type { SparkNeutral } from './theme-types'\n\n/** Shape returned by the vendored Radix generator (it doesn't export a named type). */\nexport type GeneratedColors = ReturnType<typeof generateRadixColors>\n\n/**\n * generateAccentScale output: the raw scale, but with two readable foregrounds on the\n * solid (step-9) fill, each meeting a different WCAG bar:\n * - `accentContrast` — 4.5:1 (WCAG 1.4.3), for button/label **text**.\n * - `accentOnSolid` — 3:1 (WCAG 1.4.11), for a **graphical glyph** (the checkbox check).\n * A checkmark is graphical, not text, so it uses the looser-but-correct 3:1 bar: white\n * where it clears 3:1 (most accents, incl. neutral & red — the convention), and a dark\n * accent-tint where white is too weak (e.g. orange). Using the 4.5:1 text contrast for\n * the glyph over-darkened it (a black tick on a red/neutral checkbox).\n */\nexport type SparkAccentScale = GeneratedColors & { accentOnSolid: string }\n\nexport type Appearance = 'light' | 'dark'\n\n/** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */\nexport const CURATED_ACCENTS: Record<string, string> = {\n blue: RadixColors.blue.blue9,\n indigo: RadixColors.indigo.indigo9,\n violet: RadixColors.violet.violet9,\n purple: RadixColors.purple.purple9,\n cyan: RadixColors.cyan.cyan9,\n teal: RadixColors.teal.teal9,\n green: RadixColors.green.green9,\n grass: RadixColors.grass.grass9,\n orange: RadixColors.orange.orange9,\n red: RadixColors.red.red9,\n pink: RadixColors.pink.pink9,\n gray: RadixColors.gray.gray9,\n}\n\n/** ECP `DesignUtils` generator inputs, so a generated scale matches live portals. */\nconst DEFAULTS: Record<Appearance, { gray: string; background: string }> = {\n light: { gray: '#333333', background: '#ffffff' },\n // Matches ECP DesignUtils' DEFAULT_DARK_BACKGROUND_COLOR -- the alpha ramp\n // is composited against this, so it must match what's actually painted.\n dark: { gray: '#8b8d98', background: '#000000' },\n}\n\n/** Radix step-9 per neutral family — the generator's `gray` seed, so a regenerated\n neutral lands on the family `[data-neutral-color]` would have selected statically. */\nconst NEUTRAL_BASE: Record<Exclude<SparkNeutral, 'auto'>, string> = {\n slate: RadixColors.slate.slate9,\n mauve: RadixColors.mauve.mauve9,\n gray: RadixColors.gray.gray9,\n sage: RadixColors.sage.sage9,\n olive: RadixColors.olive.olive9,\n sand: RadixColors.sand.sand9,\n}\n\nexport interface GenerateScaleOptions {\n /**\n * Page canvas the scale is composited against. Drives the lightness curve of all 12\n * steps AND every alpha step (each is solved as `target = bg·(1-α) + fg·α`), so it\n * must match what's actually painted or the alpha ramp renders wrong.\n */\n background?: string\n /** Neutral family to regenerate against `background`. Pass a resolved family, not `'auto'`. */\n neutral?: Exclude<SparkNeutral, 'auto'>\n}\n\n/** Which appearance a canvas belongs to, by OKLCH lightness. */\nexport function appearanceForBackground(background: string): Appearance {\n const [lightness] = new Color(background).to('oklch').coords\n return (lightness ?? 1) >= 0.5 ? 'light' : 'dark'\n}\n\n/** deltaE2000 ≈ 2 is the just-noticeable bar — a good solve lands far inside it. */\nconst ALPHA_SOLVE_TOLERANCE = 2\n\n/**\n * Replace alpha steps the solver could not actually solve with their opaque solid.\n *\n * Each `aN` is solved so that compositing it over the background reproduces step N\n * (`target = bg·(1-α) + fg·α`). From a saturated canvas a desaturated ink target is\n * unreachable within alpha bounds, and Radix's solver **degrades silently** rather than\n * failing — on `#FFD400` it returns `#0000ff39` for slate a11, which composites to\n * 1.66:1 body text where the solid step is a correct 9.03:1.\n *\n * So verify the solve and fall back to the opaque value where it doesn't hold. Only ever\n * called for a custom background; on white/black every step solves exactly (deltaE ~0),\n * so the default output — and portal parity — is untouched either way.\n */\nfunction opaqueWhereUnsolvable(\n background: string,\n solid: string[],\n alpha: string[],\n): string[] {\n let bg: number[]\n try {\n bg = new Color(background).to('srgb').coords\n } catch {\n return alpha\n }\n return alpha.map((step, i) => {\n const target = solid[i]\n if (!target) return step\n try {\n const ink = new Color(step)\n const fg = ink.to('srgb').coords\n const a = ink.alpha as number\n const composited = new Color(\n 'srgb',\n [0, 1, 2].map((k) => bg[k]! * (1 - a) + fg[k]! * a) as [\n number,\n number,\n number,\n ],\n )\n return composited.deltaE(new Color(target), '2000') <=\n ALPHA_SOLVE_TOLERANCE\n ? step\n : target\n } catch {\n return target\n }\n })\n}\n\n/** Generate the Radix-style accent scale for one appearance from a single accent color. */\nexport function generateAccentScale(\n accent: string,\n appearance: Appearance,\n opts?: GenerateScaleOptions,\n): SparkAccentScale {\n const background = opts?.background ?? DEFAULTS[appearance].background\n const scale = generateRadixColors({\n appearance,\n accent,\n gray: opts?.neutral\n ? NEUTRAL_BASE[opts.neutral]\n : DEFAULTS[appearance].gray,\n background,\n })\n const solved = opts?.background\n ? {\n ...scale,\n accentScaleAlpha: opaqueWhereUnsolvable(\n background,\n scale.accentScale,\n scale.accentScaleAlpha,\n ) as typeof scale.accentScaleAlpha,\n accentScaleAlphaWideGamut: opaqueWhereUnsolvable(\n background,\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n ) as typeof scale.accentScaleAlphaWideGamut,\n grayScaleAlpha: opaqueWhereUnsolvable(\n background,\n scale.grayScale,\n scale.grayScaleAlpha,\n ) as typeof scale.grayScaleAlpha,\n grayScaleAlphaWideGamut: opaqueWhereUnsolvable(\n background,\n scale.grayScaleWideGamut,\n scale.grayScaleAlphaWideGamut,\n ) as typeof scale.grayScaleAlphaWideGamut,\n }\n : scale\n // accentContrast → 4.5:1 (button text); accentOnSolid → 3:1 (graphical glyph,\n // see SparkAccentScale). Same picker, different WCAG threshold.\n return {\n ...solved,\n accentContrast: readableContrast(\n solved.accentScale[8]!,\n solved.accentContrast,\n MIN_TEXT_CONTRAST,\n ),\n accentOnSolid: readableContrast(\n solved.accentScale[8]!,\n solved.accentContrast,\n MIN_GRAPHIC_CONTRAST,\n ),\n }\n}\n\nconst MIN_TEXT_CONTRAST = 4.5 // WCAG 1.4.3 — button / label text\nconst MIN_GRAPHIC_CONTRAST = 3 // WCAG 1.4.11 — a check / indicator glyph on a solid fill\n\n// Pick the readable foreground on step 9: prefer the generator's native contrast\n// (usually white), else fall to progressively darker accent-tinted inks until one\n// clears `min`. Text uses 4.5:1; a graphical glyph (the checkmark) uses 3:1 — which\n// keeps white on most accents (incl. neutral 3.32:1, red 3.91:1) but flips to a dark\n// tint where white is too weak (e.g. orange, 2.97:1). The vendored generator keeps\n// white at APCA Lc >= 40, which can sit below both WCAG thresholds — hence this guard.\nfunction readableContrast(\n step9Hex: string,\n native: string,\n min: number,\n): string {\n const step9 = new Color(step9Hex)\n const [, chroma, hue] = step9.to('oklch').coords\n const tinted = (lightness: number) =>\n new Color('oklch', [\n lightness,\n Math.max(0.08 * (chroma ?? 0), 0.04),\n hue ?? 0,\n ])\n .to('srgb')\n .toString({ format: 'hex' })\n const candidates = [native, tinted(0.25), tinted(0.15)]\n const ratios = candidates.map((c) => new Color(c).contrast(step9, 'WCAG21'))\n const passing = ratios.findIndex((r) => r >= min)\n if (passing !== -1) return candidates[passing]!\n return candidates[ratios.indexOf(Math.max(...ratios))]!\n}\n\n/** sRGB `--spark-*` declarations for a generated accent scale. */\nexport function accentSparkVars(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScale,\n scale.accentScaleAlpha,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurface,\n scale.background,\n )\n}\n\n/** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */\nexport function accentSparkVarsP3(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurfaceWideGamut,\n scale.background,\n )\n}\n\n/** Radix's `grayColor=\"auto\"` pairing (accent family → gray), mapped to our 6 neutrals. */\nconst NEUTRAL_BY_ACCENT: Record<string, Exclude<SparkNeutral, 'auto'>> = {\n blue: 'slate',\n indigo: 'slate',\n cyan: 'slate',\n violet: 'mauve',\n purple: 'mauve',\n pink: 'mauve',\n red: 'mauve',\n teal: 'sage',\n green: 'sage',\n grass: 'olive',\n orange: 'sand',\n gray: 'gray',\n}\n\n/** Radix's \"auto\" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */\nexport function getMatchingNeutral(\n accent: string,\n): Exclude<SparkNeutral, 'auto'> {\n const byName = NEUTRAL_BY_ACCENT[accent]\n if (byName) return byName\n try {\n const [, chroma, hueRaw] = new Color(accent).to('oklch').coords\n if (!chroma || chroma < 0.04) return 'gray'\n const hue = (((hueRaw ?? 0) % 360) + 360) % 360\n if (hue >= 300 || hue < 35) return 'mauve'\n if (hue < 115) return 'sand'\n if (hue < 145) return 'olive'\n if (hue < 200) return 'sage'\n return 'slate'\n } catch {\n return 'slate'\n }\n}\n\n/** The 27 tokens every scale family declares, under an arbitrary prefix. */\nfunction scaleVars(\n prefix: string,\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n): string[] {\n return [\n ...steps.map((c, i) => `--${prefix}-${i + 1}: ${c};`),\n ...alpha.map((c, i) => `--${prefix}-a${i + 1}: ${c};`),\n `--${prefix}-contrast: ${contrast};`,\n `--${prefix}-on-solid: ${onSolid};`,\n `--${prefix}-surface: ${surface};`,\n ]\n}\n\nfunction sparkVars(\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n background: string,\n): string[] {\n const indicator = steps[8]! // step 9 — the brand step (matches --spark-indicator/-track)\n return [\n ...scaleVars('spark', steps, alpha, contrast, onSolid, surface),\n `--spark-background: ${background};`,\n `--spark-indicator: ${indicator};`,\n `--spark-track: ${indicator};`,\n ]\n}\n\n/**\n * `--neutral-*` declarations from the gray scale the generator produces alongside the\n * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a\n * static Radix family. Worth consuming only when the canvas is custom: the static\n * families' alpha steps are pre-baked against white/black, so on any other background\n * they composite wrong (every gray text/border reads through `--neutral-a*`).\n *\n * `--neutral-surface` is step 2, matching the static families' own convention rather\n * than the generator's translucent `graySurface`.\n */\nexport function neutralSparkVars(scale: SparkAccentScale): string[] {\n return scaleVars(\n 'neutral',\n scale.grayScale,\n scale.grayScaleAlpha,\n readableContrast(scale.grayScale[8]!, '#fff', MIN_TEXT_CONTRAST),\n readableContrast(scale.grayScale[8]!, '#fff', MIN_GRAPHIC_CONTRAST),\n scale.grayScale[1]!,\n )\n}\n\n/** Wide-gamut (P3) twin of `neutralSparkVars`. */\nexport function neutralSparkVarsP3(scale: SparkAccentScale): string[] {\n return scaleVars(\n 'neutral',\n scale.grayScaleWideGamut,\n scale.grayScaleAlphaWideGamut,\n readableContrast(scale.grayScale[8]!, '#fff', MIN_TEXT_CONTRAST),\n readableContrast(scale.grayScale[8]!, '#fff', MIN_GRAPHIC_CONTRAST),\n scale.grayScaleWideGamut[1]!,\n )\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { o as SparkNeutral } from "../theme-types-8wjgiNA_.cjs";
3
+ import { o as SparkNeutral } from "../theme-types-1yB0d8mp.cjs";
4
4
  import { t as generateRadixColors } from "../generate-radix-colors-Bw-RYaV4.cjs";
5
5
 
6
6
  //#region src/lib/accent-theme.d.ts
@@ -22,14 +22,39 @@ type SparkAccentScale = GeneratedColors & {
22
22
  type Appearance = 'light' | 'dark';
23
23
  /** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */
24
24
  declare const CURATED_ACCENTS: Record<string, string>;
25
+ interface GenerateScaleOptions {
26
+ /**
27
+ * Page canvas the scale is composited against. Drives the lightness curve of all 12
28
+ * steps AND every alpha step (each is solved as `target = bg·(1-α) + fg·α`), so it
29
+ * must match what's actually painted or the alpha ramp renders wrong.
30
+ */
31
+ background?: string;
32
+ /** Neutral family to regenerate against `background`. Pass a resolved family, not `'auto'`. */
33
+ neutral?: Exclude<SparkNeutral, 'auto'>;
34
+ }
35
+ /** Which appearance a canvas belongs to, by OKLCH lightness. */
36
+ declare function appearanceForBackground(background: string): Appearance;
25
37
  /** Generate the Radix-style accent scale for one appearance from a single accent color. */
26
- declare function generateAccentScale(accent: string, appearance: Appearance): SparkAccentScale;
38
+ declare function generateAccentScale(accent: string, appearance: Appearance, opts?: GenerateScaleOptions): SparkAccentScale;
27
39
  /** sRGB `--spark-*` declarations for a generated accent scale. */
28
40
  declare function accentSparkVars(scale: SparkAccentScale): string[];
29
41
  /** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */
30
42
  declare function accentSparkVarsP3(scale: SparkAccentScale): string[];
31
43
  /** Radix's "auto" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */
32
- declare function getMatchingNeutral(accent: string): SparkNeutral;
44
+ declare function getMatchingNeutral(accent: string): Exclude<SparkNeutral, 'auto'>;
45
+ /**
46
+ * `--neutral-*` declarations from the gray scale the generator produces alongside the
47
+ * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
48
+ * static Radix family. Worth consuming only when the canvas is custom: the static
49
+ * families' alpha steps are pre-baked against white/black, so on any other background
50
+ * they composite wrong (every gray text/border reads through `--neutral-a*`).
51
+ *
52
+ * `--neutral-surface` is step 2, matching the static families' own convention rather
53
+ * than the generator's translucent `graySurface`.
54
+ */
55
+ declare function neutralSparkVars(scale: SparkAccentScale): string[];
56
+ /** Wide-gamut (P3) twin of `neutralSparkVars`. */
57
+ declare function neutralSparkVarsP3(scale: SparkAccentScale): string[];
33
58
  //#endregion
34
- export { Appearance, CURATED_ACCENTS, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, generateAccentScale, getMatchingNeutral };
59
+ export { Appearance, CURATED_ACCENTS, GenerateScaleOptions, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, neutralSparkVars, neutralSparkVarsP3 };
35
60
  //# sourceMappingURL=accent-theme.d.cts.map
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { o as SparkNeutral } from "../theme-types-8wjgiNA_.js";
3
+ import { o as SparkNeutral } from "../theme-types-1yB0d8mp.js";
4
4
  import { t as generateRadixColors } from "../generate-radix-colors-Bw-RYaV4.js";
5
5
 
6
6
  //#region src/lib/accent-theme.d.ts
@@ -22,14 +22,39 @@ type SparkAccentScale = GeneratedColors & {
22
22
  type Appearance = 'light' | 'dark';
23
23
  /** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */
24
24
  declare const CURATED_ACCENTS: Record<string, string>;
25
+ interface GenerateScaleOptions {
26
+ /**
27
+ * Page canvas the scale is composited against. Drives the lightness curve of all 12
28
+ * steps AND every alpha step (each is solved as `target = bg·(1-α) + fg·α`), so it
29
+ * must match what's actually painted or the alpha ramp renders wrong.
30
+ */
31
+ background?: string;
32
+ /** Neutral family to regenerate against `background`. Pass a resolved family, not `'auto'`. */
33
+ neutral?: Exclude<SparkNeutral, 'auto'>;
34
+ }
35
+ /** Which appearance a canvas belongs to, by OKLCH lightness. */
36
+ declare function appearanceForBackground(background: string): Appearance;
25
37
  /** Generate the Radix-style accent scale for one appearance from a single accent color. */
26
- declare function generateAccentScale(accent: string, appearance: Appearance): SparkAccentScale;
38
+ declare function generateAccentScale(accent: string, appearance: Appearance, opts?: GenerateScaleOptions): SparkAccentScale;
27
39
  /** sRGB `--spark-*` declarations for a generated accent scale. */
28
40
  declare function accentSparkVars(scale: SparkAccentScale): string[];
29
41
  /** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */
30
42
  declare function accentSparkVarsP3(scale: SparkAccentScale): string[];
31
43
  /** Radix's "auto" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */
32
- declare function getMatchingNeutral(accent: string): SparkNeutral;
44
+ declare function getMatchingNeutral(accent: string): Exclude<SparkNeutral, 'auto'>;
45
+ /**
46
+ * `--neutral-*` declarations from the gray scale the generator produces alongside the
47
+ * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
48
+ * static Radix family. Worth consuming only when the canvas is custom: the static
49
+ * families' alpha steps are pre-baked against white/black, so on any other background
50
+ * they composite wrong (every gray text/border reads through `--neutral-a*`).
51
+ *
52
+ * `--neutral-surface` is step 2, matching the static families' own convention rather
53
+ * than the generator's translucent `graySurface`.
54
+ */
55
+ declare function neutralSparkVars(scale: SparkAccentScale): string[];
56
+ /** Wide-gamut (P3) twin of `neutralSparkVars`. */
57
+ declare function neutralSparkVarsP3(scale: SparkAccentScale): string[];
33
58
  //#endregion
34
- export { Appearance, CURATED_ACCENTS, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, generateAccentScale, getMatchingNeutral };
59
+ export { Appearance, CURATED_ACCENTS, GenerateScaleOptions, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, neutralSparkVars, neutralSparkVarsP3 };
35
60
  //# sourceMappingURL=accent-theme.d.ts.map
@@ -29,24 +29,86 @@ const DEFAULTS = {
29
29
  background: "#000000"
30
30
  }
31
31
  };
32
+ /** Radix step-9 per neutral family — the generator's `gray` seed, so a regenerated
33
+ neutral lands on the family `[data-neutral-color]` would have selected statically. */
34
+ const NEUTRAL_BASE = {
35
+ slate: RadixColors.slate.slate9,
36
+ mauve: RadixColors.mauve.mauve9,
37
+ gray: RadixColors.gray.gray9,
38
+ sage: RadixColors.sage.sage9,
39
+ olive: RadixColors.olive.olive9,
40
+ sand: RadixColors.sand.sand9
41
+ };
42
+ /** Which appearance a canvas belongs to, by OKLCH lightness. */
43
+ function appearanceForBackground(background) {
44
+ const [lightness] = new Color(background).to("oklch").coords;
45
+ return (lightness ?? 1) >= .5 ? "light" : "dark";
46
+ }
47
+ /** deltaE2000 ≈ 2 is the just-noticeable bar — a good solve lands far inside it. */
48
+ const ALPHA_SOLVE_TOLERANCE = 2;
49
+ /**
50
+ * Replace alpha steps the solver could not actually solve with their opaque solid.
51
+ *
52
+ * Each `aN` is solved so that compositing it over the background reproduces step N
53
+ * (`target = bg·(1-α) + fg·α`). From a saturated canvas a desaturated ink target is
54
+ * unreachable within alpha bounds, and Radix's solver **degrades silently** rather than
55
+ * failing — on `#FFD400` it returns `#0000ff39` for slate a11, which composites to
56
+ * 1.66:1 body text where the solid step is a correct 9.03:1.
57
+ *
58
+ * So verify the solve and fall back to the opaque value where it doesn't hold. Only ever
59
+ * called for a custom background; on white/black every step solves exactly (deltaE ~0),
60
+ * so the default output — and portal parity — is untouched either way.
61
+ */
62
+ function opaqueWhereUnsolvable(background, solid, alpha) {
63
+ let bg;
64
+ try {
65
+ bg = new Color(background).to("srgb").coords;
66
+ } catch {
67
+ return alpha;
68
+ }
69
+ return alpha.map((step, i) => {
70
+ const target = solid[i];
71
+ if (!target) return step;
72
+ try {
73
+ const ink = new Color(step);
74
+ const fg = ink.to("srgb").coords;
75
+ const a = ink.alpha;
76
+ return new Color("srgb", [
77
+ 0,
78
+ 1,
79
+ 2
80
+ ].map((k) => bg[k] * (1 - a) + fg[k] * a)).deltaE(new Color(target), "2000") <= ALPHA_SOLVE_TOLERANCE ? step : target;
81
+ } catch {
82
+ return target;
83
+ }
84
+ });
85
+ }
32
86
  /** Generate the Radix-style accent scale for one appearance from a single accent color. */
33
- function generateAccentScale(accent, appearance) {
87
+ function generateAccentScale(accent, appearance, opts) {
88
+ const background = opts?.background ?? DEFAULTS[appearance].background;
34
89
  const scale = generateRadixColors({
35
90
  appearance,
36
91
  accent,
37
- gray: DEFAULTS[appearance].gray,
38
- background: DEFAULTS[appearance].background
92
+ gray: opts?.neutral ? NEUTRAL_BASE[opts.neutral] : DEFAULTS[appearance].gray,
93
+ background
39
94
  });
40
- return {
95
+ const solved = opts?.background ? {
41
96
  ...scale,
42
- accentContrast: readableContrast(scale, MIN_TEXT_CONTRAST),
43
- accentOnSolid: readableContrast(scale, MIN_GRAPHIC_CONTRAST)
97
+ accentScaleAlpha: opaqueWhereUnsolvable(background, scale.accentScale, scale.accentScaleAlpha),
98
+ accentScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.accentScaleWideGamut, scale.accentScaleAlphaWideGamut),
99
+ grayScaleAlpha: opaqueWhereUnsolvable(background, scale.grayScale, scale.grayScaleAlpha),
100
+ grayScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut)
101
+ } : scale;
102
+ return {
103
+ ...solved,
104
+ accentContrast: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_TEXT_CONTRAST),
105
+ accentOnSolid: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_GRAPHIC_CONTRAST)
44
106
  };
45
107
  }
46
108
  const MIN_TEXT_CONTRAST = 4.5;
47
109
  const MIN_GRAPHIC_CONTRAST = 3;
48
- function readableContrast(scale, min) {
49
- const step9 = new Color(scale.accentScale[8]);
110
+ function readableContrast(step9Hex, native, min) {
111
+ const step9 = new Color(step9Hex);
50
112
  const [, chroma, hue] = step9.to("oklch").coords;
51
113
  const tinted = (lightness) => new Color("oklch", [
52
114
  lightness,
@@ -54,7 +116,7 @@ function readableContrast(scale, min) {
54
116
  hue ?? 0
55
117
  ]).to("srgb").toString({ format: "hex" });
56
118
  const candidates = [
57
- scale.accentContrast,
119
+ native,
58
120
  tinted(.25),
59
121
  tinted(.15)
60
122
  ];
@@ -103,20 +165,43 @@ function getMatchingNeutral(accent) {
103
165
  return "slate";
104
166
  }
105
167
  }
168
+ /** The 27 tokens every scale family declares, under an arbitrary prefix. */
169
+ function scaleVars(prefix, steps, alpha, contrast, onSolid, surface) {
170
+ return [
171
+ ...steps.map((c, i) => `--${prefix}-${i + 1}: ${c};`),
172
+ ...alpha.map((c, i) => `--${prefix}-a${i + 1}: ${c};`),
173
+ `--${prefix}-contrast: ${contrast};`,
174
+ `--${prefix}-on-solid: ${onSolid};`,
175
+ `--${prefix}-surface: ${surface};`
176
+ ];
177
+ }
106
178
  function sparkVars(steps, alpha, contrast, onSolid, surface, background) {
107
179
  const indicator = steps[8];
108
180
  return [
109
- ...steps.map((c, i) => `--spark-${i + 1}: ${c};`),
110
- ...alpha.map((c, i) => `--spark-a${i + 1}: ${c};`),
111
- `--spark-contrast: ${contrast};`,
112
- `--spark-on-solid: ${onSolid};`,
113
- `--spark-surface: ${surface};`,
181
+ ...scaleVars("spark", steps, alpha, contrast, onSolid, surface),
114
182
  `--spark-background: ${background};`,
115
183
  `--spark-indicator: ${indicator};`,
116
184
  `--spark-track: ${indicator};`
117
185
  ];
118
186
  }
187
+ /**
188
+ * `--neutral-*` declarations from the gray scale the generator produces alongside the
189
+ * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
190
+ * static Radix family. Worth consuming only when the canvas is custom: the static
191
+ * families' alpha steps are pre-baked against white/black, so on any other background
192
+ * they composite wrong (every gray text/border reads through `--neutral-a*`).
193
+ *
194
+ * `--neutral-surface` is step 2, matching the static families' own convention rather
195
+ * than the generator's translucent `graySurface`.
196
+ */
197
+ function neutralSparkVars(scale) {
198
+ return scaleVars("neutral", scale.grayScale, scale.grayScaleAlpha, readableContrast(scale.grayScale[8], "#fff", MIN_TEXT_CONTRAST), readableContrast(scale.grayScale[8], "#fff", MIN_GRAPHIC_CONTRAST), scale.grayScale[1]);
199
+ }
200
+ /** Wide-gamut (P3) twin of `neutralSparkVars`. */
201
+ function neutralSparkVarsP3(scale) {
202
+ return scaleVars("neutral", scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut, readableContrast(scale.grayScale[8], "#fff", MIN_TEXT_CONTRAST), readableContrast(scale.grayScale[8], "#fff", MIN_GRAPHIC_CONTRAST), scale.grayScaleWideGamut[1]);
203
+ }
119
204
  //#endregion
120
- export { CURATED_ACCENTS, accentSparkVars, accentSparkVarsP3, generateAccentScale, getMatchingNeutral };
205
+ export { CURATED_ACCENTS, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, neutralSparkVars, neutralSparkVarsP3 };
121
206
 
122
207
  //# sourceMappingURL=accent-theme.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"accent-theme.js","names":[],"sources":["../../src/lib/accent-theme.ts"],"sourcesContent":["// Accent is the one runtime knob (colour math, not a CSS-var swap): generate the\n// `--spark-*` scale with the same Radix algorithm live portals use, apply as inline vars.\nimport * as RadixColors from '@radix-ui/colors'\nimport Color from 'colorjs.io'\nimport { generateRadixColors } from './generate-radix-colors'\nimport type { SparkNeutral } from './theme-types'\n\n/** Shape returned by the vendored Radix generator (it doesn't export a named type). */\nexport type GeneratedColors = ReturnType<typeof generateRadixColors>\n\n/**\n * generateAccentScale output: the raw scale, but with two readable foregrounds on the\n * solid (step-9) fill, each meeting a different WCAG bar:\n * - `accentContrast` — 4.5:1 (WCAG 1.4.3), for button/label **text**.\n * - `accentOnSolid` — 3:1 (WCAG 1.4.11), for a **graphical glyph** (the checkbox check).\n * A checkmark is graphical, not text, so it uses the looser-but-correct 3:1 bar: white\n * where it clears 3:1 (most accents, incl. neutral & red — the convention), and a dark\n * accent-tint where white is too weak (e.g. orange). Using the 4.5:1 text contrast for\n * the glyph over-darkened it (a black tick on a red/neutral checkbox).\n */\nexport type SparkAccentScale = GeneratedColors & { accentOnSolid: string }\n\nexport type Appearance = 'light' | 'dark'\n\n/** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */\nexport const CURATED_ACCENTS: Record<string, string> = {\n blue: RadixColors.blue.blue9,\n indigo: RadixColors.indigo.indigo9,\n violet: RadixColors.violet.violet9,\n purple: RadixColors.purple.purple9,\n cyan: RadixColors.cyan.cyan9,\n teal: RadixColors.teal.teal9,\n green: RadixColors.green.green9,\n grass: RadixColors.grass.grass9,\n orange: RadixColors.orange.orange9,\n red: RadixColors.red.red9,\n pink: RadixColors.pink.pink9,\n gray: RadixColors.gray.gray9,\n}\n\n/** ECP `DesignUtils` generator inputs, so a generated scale matches live portals. */\nconst DEFAULTS: Record<Appearance, { gray: string; background: string }> = {\n light: { gray: '#333333', background: '#ffffff' },\n // Matches ECP DesignUtils' DEFAULT_DARK_BACKGROUND_COLOR -- the alpha ramp\n // is composited against this, so it must match what's actually painted.\n dark: { gray: '#8b8d98', background: '#000000' },\n}\n\n/** Generate the Radix-style accent scale for one appearance from a single accent color. */\nexport function generateAccentScale(\n accent: string,\n appearance: Appearance,\n): SparkAccentScale {\n const scale = generateRadixColors({\n appearance,\n accent,\n gray: DEFAULTS[appearance].gray,\n background: DEFAULTS[appearance].background,\n })\n // accentContrast → 4.5:1 (button text); accentOnSolid → 3:1 (graphical glyph,\n // see SparkAccentScale). Same picker, different WCAG threshold.\n return {\n ...scale,\n accentContrast: readableContrast(scale, MIN_TEXT_CONTRAST),\n accentOnSolid: readableContrast(scale, MIN_GRAPHIC_CONTRAST),\n }\n}\n\nconst MIN_TEXT_CONTRAST = 4.5 // WCAG 1.4.3 — button / label text\nconst MIN_GRAPHIC_CONTRAST = 3 // WCAG 1.4.11 — a check / indicator glyph on a solid fill\n\n// Pick the readable foreground on step 9: prefer the generator's native contrast\n// (usually white), else fall to progressively darker accent-tinted inks until one\n// clears `min`. Text uses 4.5:1; a graphical glyph (the checkmark) uses 3:1 — which\n// keeps white on most accents (incl. neutral 3.32:1, red 3.91:1) but flips to a dark\n// tint where white is too weak (e.g. orange, 2.97:1). The vendored generator keeps\n// white at APCA Lc >= 40, which can sit below both WCAG thresholds — hence this guard.\nfunction readableContrast(scale: GeneratedColors, min: number): string {\n const step9 = new Color(scale.accentScale[8]!)\n const [, chroma, hue] = step9.to('oklch').coords\n const tinted = (lightness: number) =>\n new Color('oklch', [\n lightness,\n Math.max(0.08 * (chroma ?? 0), 0.04),\n hue ?? 0,\n ])\n .to('srgb')\n .toString({ format: 'hex' })\n const candidates = [scale.accentContrast, tinted(0.25), tinted(0.15)]\n const ratios = candidates.map((c) => new Color(c).contrast(step9, 'WCAG21'))\n const passing = ratios.findIndex((r) => r >= min)\n if (passing !== -1) return candidates[passing]!\n return candidates[ratios.indexOf(Math.max(...ratios))]!\n}\n\n/** sRGB `--spark-*` declarations for a generated accent scale. */\nexport function accentSparkVars(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScale,\n scale.accentScaleAlpha,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurface,\n scale.background,\n )\n}\n\n/** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */\nexport function accentSparkVarsP3(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurfaceWideGamut,\n scale.background,\n )\n}\n\n/** Radix's `grayColor=\"auto\"` pairing (accent family → gray), mapped to our 6 neutrals. */\nconst NEUTRAL_BY_ACCENT: Record<string, SparkNeutral> = {\n blue: 'slate',\n indigo: 'slate',\n cyan: 'slate',\n violet: 'mauve',\n purple: 'mauve',\n pink: 'mauve',\n red: 'mauve',\n teal: 'sage',\n green: 'sage',\n grass: 'olive',\n orange: 'sand',\n gray: 'gray',\n}\n\n/** Radix's \"auto\" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */\nexport function getMatchingNeutral(accent: string): SparkNeutral {\n const byName = NEUTRAL_BY_ACCENT[accent]\n if (byName) return byName\n try {\n const [, chroma, hueRaw] = new Color(accent).to('oklch').coords\n if (!chroma || chroma < 0.04) return 'gray'\n const hue = (((hueRaw ?? 0) % 360) + 360) % 360\n if (hue >= 300 || hue < 35) return 'mauve'\n if (hue < 115) return 'sand'\n if (hue < 145) return 'olive'\n if (hue < 200) return 'sage'\n return 'slate'\n } catch {\n return 'slate'\n }\n}\n\nfunction sparkVars(\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n background: string,\n): string[] {\n const indicator = steps[8]! // step 9 — the brand step (matches --spark-indicator/-track)\n return [\n ...steps.map((c, i) => `--spark-${i + 1}: ${c};`),\n ...alpha.map((c, i) => `--spark-a${i + 1}: ${c};`),\n `--spark-contrast: ${contrast};`,\n `--spark-on-solid: ${onSolid};`,\n `--spark-surface: ${surface};`,\n `--spark-background: ${background};`,\n `--spark-indicator: ${indicator};`,\n `--spark-track: ${indicator};`,\n ]\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"accent-theme.js","names":[],"sources":["../../src/lib/accent-theme.ts"],"sourcesContent":["// Accent is the one runtime knob (colour math, not a CSS-var swap): generate the\n// `--spark-*` scale with the same Radix algorithm live portals use, apply as inline vars.\nimport * as RadixColors from '@radix-ui/colors'\nimport Color from 'colorjs.io'\nimport { generateRadixColors } from './generate-radix-colors'\nimport type { SparkNeutral } from './theme-types'\n\n/** Shape returned by the vendored Radix generator (it doesn't export a named type). */\nexport type GeneratedColors = ReturnType<typeof generateRadixColors>\n\n/**\n * generateAccentScale output: the raw scale, but with two readable foregrounds on the\n * solid (step-9) fill, each meeting a different WCAG bar:\n * - `accentContrast` — 4.5:1 (WCAG 1.4.3), for button/label **text**.\n * - `accentOnSolid` — 3:1 (WCAG 1.4.11), for a **graphical glyph** (the checkbox check).\n * A checkmark is graphical, not text, so it uses the looser-but-correct 3:1 bar: white\n * where it clears 3:1 (most accents, incl. neutral & red — the convention), and a dark\n * accent-tint where white is too weak (e.g. orange). Using the 4.5:1 text contrast for\n * the glyph over-darkened it (a black tick on a red/neutral checkbox).\n */\nexport type SparkAccentScale = GeneratedColors & { accentOnSolid: string }\n\nexport type Appearance = 'light' | 'dark'\n\n/** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */\nexport const CURATED_ACCENTS: Record<string, string> = {\n blue: RadixColors.blue.blue9,\n indigo: RadixColors.indigo.indigo9,\n violet: RadixColors.violet.violet9,\n purple: RadixColors.purple.purple9,\n cyan: RadixColors.cyan.cyan9,\n teal: RadixColors.teal.teal9,\n green: RadixColors.green.green9,\n grass: RadixColors.grass.grass9,\n orange: RadixColors.orange.orange9,\n red: RadixColors.red.red9,\n pink: RadixColors.pink.pink9,\n gray: RadixColors.gray.gray9,\n}\n\n/** ECP `DesignUtils` generator inputs, so a generated scale matches live portals. */\nconst DEFAULTS: Record<Appearance, { gray: string; background: string }> = {\n light: { gray: '#333333', background: '#ffffff' },\n // Matches ECP DesignUtils' DEFAULT_DARK_BACKGROUND_COLOR -- the alpha ramp\n // is composited against this, so it must match what's actually painted.\n dark: { gray: '#8b8d98', background: '#000000' },\n}\n\n/** Radix step-9 per neutral family — the generator's `gray` seed, so a regenerated\n neutral lands on the family `[data-neutral-color]` would have selected statically. */\nconst NEUTRAL_BASE: Record<Exclude<SparkNeutral, 'auto'>, string> = {\n slate: RadixColors.slate.slate9,\n mauve: RadixColors.mauve.mauve9,\n gray: RadixColors.gray.gray9,\n sage: RadixColors.sage.sage9,\n olive: RadixColors.olive.olive9,\n sand: RadixColors.sand.sand9,\n}\n\nexport interface GenerateScaleOptions {\n /**\n * Page canvas the scale is composited against. Drives the lightness curve of all 12\n * steps AND every alpha step (each is solved as `target = bg·(1-α) + fg·α`), so it\n * must match what's actually painted or the alpha ramp renders wrong.\n */\n background?: string\n /** Neutral family to regenerate against `background`. Pass a resolved family, not `'auto'`. */\n neutral?: Exclude<SparkNeutral, 'auto'>\n}\n\n/** Which appearance a canvas belongs to, by OKLCH lightness. */\nexport function appearanceForBackground(background: string): Appearance {\n const [lightness] = new Color(background).to('oklch').coords\n return (lightness ?? 1) >= 0.5 ? 'light' : 'dark'\n}\n\n/** deltaE2000 ≈ 2 is the just-noticeable bar — a good solve lands far inside it. */\nconst ALPHA_SOLVE_TOLERANCE = 2\n\n/**\n * Replace alpha steps the solver could not actually solve with their opaque solid.\n *\n * Each `aN` is solved so that compositing it over the background reproduces step N\n * (`target = bg·(1-α) + fg·α`). From a saturated canvas a desaturated ink target is\n * unreachable within alpha bounds, and Radix's solver **degrades silently** rather than\n * failing — on `#FFD400` it returns `#0000ff39` for slate a11, which composites to\n * 1.66:1 body text where the solid step is a correct 9.03:1.\n *\n * So verify the solve and fall back to the opaque value where it doesn't hold. Only ever\n * called for a custom background; on white/black every step solves exactly (deltaE ~0),\n * so the default output — and portal parity — is untouched either way.\n */\nfunction opaqueWhereUnsolvable(\n background: string,\n solid: string[],\n alpha: string[],\n): string[] {\n let bg: number[]\n try {\n bg = new Color(background).to('srgb').coords\n } catch {\n return alpha\n }\n return alpha.map((step, i) => {\n const target = solid[i]\n if (!target) return step\n try {\n const ink = new Color(step)\n const fg = ink.to('srgb').coords\n const a = ink.alpha as number\n const composited = new Color(\n 'srgb',\n [0, 1, 2].map((k) => bg[k]! * (1 - a) + fg[k]! * a) as [\n number,\n number,\n number,\n ],\n )\n return composited.deltaE(new Color(target), '2000') <=\n ALPHA_SOLVE_TOLERANCE\n ? step\n : target\n } catch {\n return target\n }\n })\n}\n\n/** Generate the Radix-style accent scale for one appearance from a single accent color. */\nexport function generateAccentScale(\n accent: string,\n appearance: Appearance,\n opts?: GenerateScaleOptions,\n): SparkAccentScale {\n const background = opts?.background ?? DEFAULTS[appearance].background\n const scale = generateRadixColors({\n appearance,\n accent,\n gray: opts?.neutral\n ? NEUTRAL_BASE[opts.neutral]\n : DEFAULTS[appearance].gray,\n background,\n })\n const solved = opts?.background\n ? {\n ...scale,\n accentScaleAlpha: opaqueWhereUnsolvable(\n background,\n scale.accentScale,\n scale.accentScaleAlpha,\n ) as typeof scale.accentScaleAlpha,\n accentScaleAlphaWideGamut: opaqueWhereUnsolvable(\n background,\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n ) as typeof scale.accentScaleAlphaWideGamut,\n grayScaleAlpha: opaqueWhereUnsolvable(\n background,\n scale.grayScale,\n scale.grayScaleAlpha,\n ) as typeof scale.grayScaleAlpha,\n grayScaleAlphaWideGamut: opaqueWhereUnsolvable(\n background,\n scale.grayScaleWideGamut,\n scale.grayScaleAlphaWideGamut,\n ) as typeof scale.grayScaleAlphaWideGamut,\n }\n : scale\n // accentContrast → 4.5:1 (button text); accentOnSolid → 3:1 (graphical glyph,\n // see SparkAccentScale). Same picker, different WCAG threshold.\n return {\n ...solved,\n accentContrast: readableContrast(\n solved.accentScale[8]!,\n solved.accentContrast,\n MIN_TEXT_CONTRAST,\n ),\n accentOnSolid: readableContrast(\n solved.accentScale[8]!,\n solved.accentContrast,\n MIN_GRAPHIC_CONTRAST,\n ),\n }\n}\n\nconst MIN_TEXT_CONTRAST = 4.5 // WCAG 1.4.3 — button / label text\nconst MIN_GRAPHIC_CONTRAST = 3 // WCAG 1.4.11 — a check / indicator glyph on a solid fill\n\n// Pick the readable foreground on step 9: prefer the generator's native contrast\n// (usually white), else fall to progressively darker accent-tinted inks until one\n// clears `min`. Text uses 4.5:1; a graphical glyph (the checkmark) uses 3:1 — which\n// keeps white on most accents (incl. neutral 3.32:1, red 3.91:1) but flips to a dark\n// tint where white is too weak (e.g. orange, 2.97:1). The vendored generator keeps\n// white at APCA Lc >= 40, which can sit below both WCAG thresholds — hence this guard.\nfunction readableContrast(\n step9Hex: string,\n native: string,\n min: number,\n): string {\n const step9 = new Color(step9Hex)\n const [, chroma, hue] = step9.to('oklch').coords\n const tinted = (lightness: number) =>\n new Color('oklch', [\n lightness,\n Math.max(0.08 * (chroma ?? 0), 0.04),\n hue ?? 0,\n ])\n .to('srgb')\n .toString({ format: 'hex' })\n const candidates = [native, tinted(0.25), tinted(0.15)]\n const ratios = candidates.map((c) => new Color(c).contrast(step9, 'WCAG21'))\n const passing = ratios.findIndex((r) => r >= min)\n if (passing !== -1) return candidates[passing]!\n return candidates[ratios.indexOf(Math.max(...ratios))]!\n}\n\n/** sRGB `--spark-*` declarations for a generated accent scale. */\nexport function accentSparkVars(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScale,\n scale.accentScaleAlpha,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurface,\n scale.background,\n )\n}\n\n/** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */\nexport function accentSparkVarsP3(scale: SparkAccentScale): string[] {\n return sparkVars(\n scale.accentScaleWideGamut,\n scale.accentScaleAlphaWideGamut,\n scale.accentContrast,\n scale.accentOnSolid,\n scale.accentSurfaceWideGamut,\n scale.background,\n )\n}\n\n/** Radix's `grayColor=\"auto\"` pairing (accent family → gray), mapped to our 6 neutrals. */\nconst NEUTRAL_BY_ACCENT: Record<string, Exclude<SparkNeutral, 'auto'>> = {\n blue: 'slate',\n indigo: 'slate',\n cyan: 'slate',\n violet: 'mauve',\n purple: 'mauve',\n pink: 'mauve',\n red: 'mauve',\n teal: 'sage',\n green: 'sage',\n grass: 'olive',\n orange: 'sand',\n gray: 'gray',\n}\n\n/** Radix's \"auto\" gray: curated names use Radix's table; a custom hex is classified by OKLCH hue. */\nexport function getMatchingNeutral(\n accent: string,\n): Exclude<SparkNeutral, 'auto'> {\n const byName = NEUTRAL_BY_ACCENT[accent]\n if (byName) return byName\n try {\n const [, chroma, hueRaw] = new Color(accent).to('oklch').coords\n if (!chroma || chroma < 0.04) return 'gray'\n const hue = (((hueRaw ?? 0) % 360) + 360) % 360\n if (hue >= 300 || hue < 35) return 'mauve'\n if (hue < 115) return 'sand'\n if (hue < 145) return 'olive'\n if (hue < 200) return 'sage'\n return 'slate'\n } catch {\n return 'slate'\n }\n}\n\n/** The 27 tokens every scale family declares, under an arbitrary prefix. */\nfunction scaleVars(\n prefix: string,\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n): string[] {\n return [\n ...steps.map((c, i) => `--${prefix}-${i + 1}: ${c};`),\n ...alpha.map((c, i) => `--${prefix}-a${i + 1}: ${c};`),\n `--${prefix}-contrast: ${contrast};`,\n `--${prefix}-on-solid: ${onSolid};`,\n `--${prefix}-surface: ${surface};`,\n ]\n}\n\nfunction sparkVars(\n steps: string[],\n alpha: string[],\n contrast: string,\n onSolid: string,\n surface: string,\n background: string,\n): string[] {\n const indicator = steps[8]! // step 9 — the brand step (matches --spark-indicator/-track)\n return [\n ...scaleVars('spark', steps, alpha, contrast, onSolid, surface),\n `--spark-background: ${background};`,\n `--spark-indicator: ${indicator};`,\n `--spark-track: ${indicator};`,\n ]\n}\n\n/**\n * `--neutral-*` declarations from the gray scale the generator produces alongside the\n * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a\n * static Radix family. Worth consuming only when the canvas is custom: the static\n * families' alpha steps are pre-baked against white/black, so on any other background\n * they composite wrong (every gray text/border reads through `--neutral-a*`).\n *\n * `--neutral-surface` is step 2, matching the static families' own convention rather\n * than the generator's translucent `graySurface`.\n */\nexport function neutralSparkVars(scale: SparkAccentScale): string[] {\n return scaleVars(\n 'neutral',\n scale.grayScale,\n scale.grayScaleAlpha,\n readableContrast(scale.grayScale[8]!, '#fff', MIN_TEXT_CONTRAST),\n readableContrast(scale.grayScale[8]!, '#fff', MIN_GRAPHIC_CONTRAST),\n scale.grayScale[1]!,\n )\n}\n\n/** Wide-gamut (P3) twin of `neutralSparkVars`. */\nexport function neutralSparkVarsP3(scale: SparkAccentScale): string[] {\n return scaleVars(\n 'neutral',\n scale.grayScaleWideGamut,\n scale.grayScaleAlphaWideGamut,\n readableContrast(scale.grayScale[8]!, '#fff', MIN_TEXT_CONTRAST),\n readableContrast(scale.grayScale[8]!, '#fff', MIN_GRAPHIC_CONTRAST),\n scale.grayScaleWideGamut[1]!,\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ //#region src/lib/container-chrome.ts
4
+ const radiusClasses = {
5
+ none: "rounded-none",
6
+ sm: "rounded-sm",
7
+ md: "rounded-md",
8
+ lg: "rounded-lg",
9
+ xl: "rounded-xl",
10
+ "2xl": "rounded-2xl",
11
+ "3xl": "rounded-3xl",
12
+ full: "rounded-full"
13
+ };
14
+ const containerChrome = {
15
+ ambient: "border-(length:--spark-card-border-width,0px) border-(--spark-card-border,transparent) bg-(--spark-card-bg,var(--neutral-a3))",
16
+ soft: "bg-(--spark-card-c-soft,var(--neutral-a3))",
17
+ surface: "border border-(--spark-card-c-border,var(--neutral-a6)) bg-(--spark-card-c-surface,var(--neutral-2))",
18
+ outline: "border border-(--spark-card-c-border,var(--neutral-a6))",
19
+ ghost: "",
20
+ solid: "bg-(--spark-card-c-solid,var(--neutral-1)) [--spark-card-fg-muted:var(--spark-card-c-contrast,var(--neutral-contrast))] [--spark-card-fg:var(--spark-card-c-contrast,var(--neutral-contrast))] [--spark-card-icon:var(--spark-card-c-contrast,var(--neutral-contrast))]"
21
+ };
22
+ const containerPaddingX = {
23
+ ambient: "px-(--spark-card-px,1rem)",
24
+ soft: "px-4",
25
+ surface: "px-4",
26
+ outline: "px-4",
27
+ ghost: "",
28
+ solid: "px-4"
29
+ };
30
+ const containerPaddingAll = {
31
+ ambient: "p-(--spark-card-px,1rem)",
32
+ soft: "p-4",
33
+ surface: "p-4",
34
+ outline: "p-4",
35
+ ghost: "",
36
+ solid: "p-4"
37
+ };
38
+ function containerInteractiveClasses(resolved) {
39
+ return [
40
+ "cursor-pointer text-left no-underline transition-colors motion-reduce:transition-none",
41
+ "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden",
42
+ "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50",
43
+ (resolved === "ambient" || resolved === "soft") && "hover:bg-(--spark-card-c-soft-hover,var(--neutral-a4))"
44
+ ];
45
+ }
46
+ //#endregion
47
+ exports.containerChrome = containerChrome;
48
+ exports.containerInteractiveClasses = containerInteractiveClasses;
49
+ exports.containerPaddingAll = containerPaddingAll;
50
+ exports.containerPaddingX = containerPaddingX;
51
+ exports.radiusClasses = radiusClasses;
52
+
53
+ //# sourceMappingURL=container-chrome.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"container-chrome.cjs","names":[],"sources":["../../src/lib/container-chrome.ts"],"sourcesContent":["import type { SparkCardVariant } from './theme-types'\n\n// `ambient` is the internal cva key for an unset `variant` prop — not part of the public\n// SparkCardVariant union, which only lists variants a consumer can actually pass.\nexport type ContainerVariant = SparkCardVariant | 'ambient'\n\nexport type ContainerRadius =\n | 'none'\n | 'sm'\n | 'md'\n | 'lg'\n | 'xl'\n | '2xl'\n | '3xl'\n | 'full'\n\nexport const radiusClasses: Record<ContainerRadius, string> = {\n none: 'rounded-none',\n sm: 'rounded-sm',\n md: 'rounded-md',\n lg: 'rounded-lg',\n xl: 'rounded-xl',\n '2xl': 'rounded-2xl',\n '3xl': 'rounded-3xl',\n full: 'rounded-full',\n}\n\n// Fill/border/token-overrides only. Padding is a separate axis (containerPaddingX/All below) —\n// Card insets horizontally and owns its own vertical rhythm; Box has no such assumption.\nexport const containerChrome: Record<ContainerVariant, string> = {\n ambient:\n 'border-(length:--spark-card-border-width,0px) border-(--spark-card-border,transparent) bg-(--spark-card-bg,var(--neutral-a3))',\n soft: 'bg-(--spark-card-c-soft,var(--neutral-a3))',\n surface:\n 'border border-(--spark-card-c-border,var(--neutral-a6)) bg-(--spark-card-c-surface,var(--neutral-2))',\n outline: 'border border-(--spark-card-c-border,var(--neutral-a6))',\n ghost: '',\n solid:\n 'bg-(--spark-card-c-solid,var(--neutral-1)) [--spark-card-fg-muted:var(--spark-card-c-contrast,var(--neutral-contrast))] [--spark-card-fg:var(--spark-card-c-contrast,var(--neutral-contrast))] [--spark-card-icon:var(--spark-card-c-contrast,var(--neutral-contrast))]',\n}\n\nexport const containerPaddingX: Record<ContainerVariant, string> = {\n ambient: 'px-(--spark-card-px,1rem)',\n soft: 'px-4',\n surface: 'px-4',\n outline: 'px-4',\n ghost: '',\n solid: 'px-4',\n}\n\nexport const containerPaddingAll: Record<ContainerVariant, string> = {\n ambient: 'p-(--spark-card-px,1rem)',\n soft: 'p-4',\n surface: 'p-4',\n outline: 'p-4',\n ghost: '',\n solid: 'p-4',\n}\n\nexport function containerInteractiveClasses(resolved: ContainerVariant) {\n return [\n 'cursor-pointer text-left no-underline transition-colors motion-reduce:transition-none',\n 'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50',\n // hover deepens the fill one step; bordered/bare variants only ring\n (resolved === 'ambient' || resolved === 'soft') &&\n 'hover:bg-(--spark-card-c-soft-hover,var(--neutral-a4))',\n ]\n}\n"],"mappings":";;;AAgBA,MAAa,gBAAiD;CAC5D,MAAM;CACN,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACP,OAAO;CACP,MAAM;AACR;AAIA,MAAa,kBAAoD;CAC/D,SACE;CACF,MAAM;CACN,SACE;CACF,SAAS;CACT,OAAO;CACP,OACE;AACJ;AAEA,MAAa,oBAAsD;CACjE,SAAS;CACT,MAAM;CACN,SAAS;CACT,SAAS;CACT,OAAO;CACP,OAAO;AACT;AAEA,MAAa,sBAAwD;CACnE,SAAS;CACT,MAAM;CACN,SAAS;CACT,SAAS;CACT,OAAO;CACP,OAAO;AACT;AAEA,SAAgB,4BAA4B,UAA4B;CACtE,OAAO;EACL;EACA;EACA;GAEC,aAAa,aAAa,aAAa,WACtC;CACJ;AACF"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { n as SparkCardVariant } from "../theme-types-1yB0d8mp.cjs";
4
+
5
+ //#region src/lib/container-chrome.d.ts
6
+ type ContainerVariant = SparkCardVariant | 'ambient';
7
+ type ContainerRadius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full';
8
+ declare const radiusClasses: Record<ContainerRadius, string>;
9
+ declare const containerChrome: Record<ContainerVariant, string>;
10
+ declare const containerPaddingX: Record<ContainerVariant, string>;
11
+ declare const containerPaddingAll: Record<ContainerVariant, string>;
12
+ declare function containerInteractiveClasses(resolved: ContainerVariant): (string | false)[];
13
+ //#endregion
14
+ export { ContainerRadius, ContainerVariant, containerChrome, containerInteractiveClasses, containerPaddingAll, containerPaddingX, radiusClasses };
15
+ //# sourceMappingURL=container-chrome.d.cts.map