@epilot/spark-ui 0.11.1 → 0.12.1

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/avatar.d.cts +1 -1
  2. package/dist/components/avatar.d.ts +1 -1
  3. package/dist/components/badge.d.cts +1 -1
  4. package/dist/components/badge.d.ts +1 -1
  5. package/dist/components/bar-chart.js +1 -1
  6. package/dist/components/box.cjs +3 -2
  7. package/dist/components/box.cjs.map +1 -1
  8. package/dist/components/box.d.cts +8 -1
  9. package/dist/components/box.d.ts +8 -1
  10. package/dist/components/box.js +3 -2
  11. package/dist/components/box.js.map +1 -1
  12. package/dist/components/button-group.d.cts +1 -1
  13. package/dist/components/button-group.d.ts +1 -1
  14. package/dist/components/button.d.cts +2 -2
  15. package/dist/components/button.d.ts +2 -2
  16. package/dist/components/calendar.cjs +2 -2
  17. package/dist/components/calendar.cjs.map +1 -1
  18. package/dist/components/calendar.js +2 -2
  19. package/dist/components/calendar.js.map +1 -1
  20. package/dist/components/callout.cjs +1 -1
  21. package/dist/components/callout.cjs.map +1 -1
  22. package/dist/components/callout.d.cts +3 -3
  23. package/dist/components/callout.d.ts +3 -3
  24. package/dist/components/callout.js +1 -1
  25. package/dist/components/callout.js.map +1 -1
  26. package/dist/components/card.cjs +3 -2
  27. package/dist/components/card.cjs.map +1 -1
  28. package/dist/components/card.d.cts +7 -1
  29. package/dist/components/card.d.ts +7 -1
  30. package/dist/components/card.js +3 -2
  31. package/dist/components/card.js.map +1 -1
  32. package/dist/components/checkbox.d.cts +1 -1
  33. package/dist/components/checkbox.d.ts +1 -1
  34. package/dist/components/field.d.cts +1 -1
  35. package/dist/components/field.d.ts +1 -1
  36. package/dist/components/icon.cjs +1 -1
  37. package/dist/components/icon.js +1 -1
  38. package/dist/components/input-group.cjs +1 -1
  39. package/dist/components/input-group.cjs.map +1 -1
  40. package/dist/components/input-group.js +1 -1
  41. package/dist/components/input-group.js.map +1 -1
  42. package/dist/components/input.d.cts +1 -1
  43. package/dist/components/input.d.ts +1 -1
  44. package/dist/components/line-chart.cjs +1 -1
  45. package/dist/components/line-chart.js +2 -2
  46. package/dist/components/list.d.cts +1 -1
  47. package/dist/components/list.d.ts +1 -1
  48. package/dist/components/marker.d.cts +1 -1
  49. package/dist/components/marker.d.ts +1 -1
  50. package/dist/components/pill.cjs +20 -13
  51. package/dist/components/pill.cjs.map +1 -1
  52. package/dist/components/pill.d.cts +1 -1
  53. package/dist/components/pill.d.ts +1 -1
  54. package/dist/components/pill.js +20 -13
  55. package/dist/components/pill.js.map +1 -1
  56. package/dist/components/power-history-chart.cjs +1 -1
  57. package/dist/components/power-history-chart.js +2 -2
  58. package/dist/components/progress.cjs +1 -1
  59. package/dist/components/progress.cjs.map +1 -1
  60. package/dist/components/progress.js +1 -1
  61. package/dist/components/progress.js.map +1 -1
  62. package/dist/components/radio-card-group.cjs.map +1 -1
  63. package/dist/components/radio-card-group.js.map +1 -1
  64. package/dist/components/radio.d.cts +1 -1
  65. package/dist/components/radio.d.ts +1 -1
  66. package/dist/components/schedule-list.cjs +10 -7
  67. package/dist/components/schedule-list.cjs.map +1 -1
  68. package/dist/components/schedule-list.js +10 -7
  69. package/dist/components/schedule-list.js.map +1 -1
  70. package/dist/components/segmented-control.d.cts +1 -1
  71. package/dist/components/segmented-control.d.ts +1 -1
  72. package/dist/components/select.cjs +1 -1
  73. package/dist/components/select.d.cts +1 -1
  74. package/dist/components/select.d.ts +1 -1
  75. package/dist/components/select.js +1 -1
  76. package/dist/components/solar-forecast-chart.cjs +1 -1
  77. package/dist/components/solar-forecast-chart.js +2 -2
  78. package/dist/components/switch.d.cts +1 -1
  79. package/dist/components/switch.d.ts +1 -1
  80. package/dist/components/textarea.d.cts +1 -1
  81. package/dist/components/textarea.d.ts +1 -1
  82. package/dist/components/theme.cjs +18 -6
  83. package/dist/components/theme.cjs.map +1 -1
  84. package/dist/components/theme.d.cts +16 -2
  85. package/dist/components/theme.d.ts +16 -2
  86. package/dist/components/theme.js +19 -7
  87. package/dist/components/theme.js.map +1 -1
  88. package/dist/{generate-radix-colors-Bw-RYaV4.d.ts → generate-radix-colors-BY4Wjsm7.d.cts} +4 -2
  89. package/dist/{generate-radix-colors-Bw-RYaV4.d.cts → generate-radix-colors-BY4Wjsm7.d.ts} +4 -2
  90. package/dist/icons/material-symbol.cjs +1 -1
  91. package/dist/icons/material-symbol.cjs.map +1 -1
  92. package/dist/icons/material-symbol.js +1 -1
  93. package/dist/icons/material-symbol.js.map +1 -1
  94. package/dist/lib/accent-theme.cjs +367 -16
  95. package/dist/lib/accent-theme.cjs.map +1 -1
  96. package/dist/lib/accent-theme.d.cts +75 -6
  97. package/dist/lib/accent-theme.d.ts +75 -6
  98. package/dist/lib/accent-theme.js +365 -18
  99. package/dist/lib/accent-theme.js.map +1 -1
  100. package/dist/lib/container-chrome.cjs +9 -7
  101. package/dist/lib/container-chrome.cjs.map +1 -1
  102. package/dist/lib/container-chrome.d.cts +4 -3
  103. package/dist/lib/container-chrome.d.ts +4 -3
  104. package/dist/lib/container-chrome.js +9 -8
  105. package/dist/lib/container-chrome.js.map +1 -1
  106. package/dist/lib/generate-radix-colors.cjs +7 -0
  107. package/dist/lib/generate-radix-colors.cjs.map +1 -1
  108. package/dist/lib/generate-radix-colors.d.cts +2 -2
  109. package/dist/lib/generate-radix-colors.d.ts +2 -2
  110. package/dist/lib/generate-radix-colors.js +6 -1
  111. package/dist/lib/generate-radix-colors.js.map +1 -1
  112. package/dist/lib/input-box-variants.d.cts +1 -1
  113. package/dist/lib/input-box-variants.d.ts +1 -1
  114. package/dist/lib/theme-types.cjs +19 -0
  115. package/dist/lib/theme-types.cjs.map +1 -0
  116. package/dist/lib/theme-types.d.cts +2 -2
  117. package/dist/lib/theme-types.d.ts +2 -2
  118. package/dist/lib/theme-types.js +17 -1
  119. package/dist/lib/theme-types.js.map +1 -0
  120. package/dist/styles/material-symbol.css +3 -0
  121. package/dist/styles/spark-default.css +148 -0
  122. package/dist/styles/utilities.css +9 -8
  123. package/dist/{theme-types-1yB0d8mp.d.ts → theme-types-Bn5E15-P.d.cts} +12 -3
  124. package/dist/{theme-types-1yB0d8mp.d.cts → theme-types-Bn5E15-P.d.ts} +12 -3
  125. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  "use client";
2
- import { generateRadixColors } from "./generate-radix-colors.js";
2
+ import { generateRadixColors, getAlphaColorP3, getAlphaColorSrgb } from "./generate-radix-colors.js";
3
+ import { isSparkNeutralFamily } from "./theme-types.js";
3
4
  import * as RadixColors from "@radix-ui/colors";
4
5
  import Color from "colorjs.io";
5
6
  //#region src/lib/accent-theme.ts
@@ -47,22 +48,45 @@ function appearanceForBackground(background) {
47
48
  /** deltaE2000 ≈ 2 is the just-noticeable bar — a good solve lands far inside it. */
48
49
  const ALPHA_SOLVE_TOLERANCE = 2;
49
50
  /**
50
- * Replace alpha steps the solver could not actually solve with their opaque solid.
51
+ * OKLCH chroma below which a color reads as neutral rather than tinted — shared by every
52
+ * "is this canvas/accent actually chromatic" check (getMatchingNeutral, resolveAutoNeutral,
53
+ * accentScaleForCanvas).
54
+ */
55
+ const CHROMATIC_THRESHOLD = .04;
56
+ /**
57
+ * An alpha step past this needs to cover the canvas almost completely to reproduce its
58
+ * target — at that point it isn't a tint anymore, and the opaque solid is both cheaper and
59
+ * more predictable. Only applied to the accent ramp's canvas-tinted low steps (gap #12a,
60
+ * ask 2 — specifically the steps `tintLowStepsFromCanvas` retargets; steps 6-12 are by-design
61
+ * high-alpha ink/solid tokens even on plain white — capping *those* would be a regression, not
62
+ * a fix). Same shape as the deltaE check below, widened from "could not solve" to "solved, but
63
+ * not usefully".
64
+ */
65
+ const ALPHA_CAP = .5;
66
+ /**
67
+ * Replace alpha steps the solver could not actually solve — or, within `capAlphaSteps` leading
68
+ * steps, solved only at a near-opaque alpha — with their opaque solid.
51
69
  *
52
70
  * Each `aN` is solved so that compositing it over the background reproduces step N
53
71
  * (`target = bg·(1-α) + fg·α`). From a saturated canvas a desaturated ink target is
54
72
  * unreachable within alpha bounds, and Radix's solver **degrades silently** rather than
55
73
  * 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.
74
+ * 1.66:1 body text where the solid step is a correct 9.03:1. Separately, even a solve that
75
+ * *succeeds* can need alpha past `ALPHA_CAP` to get there — technically correct only over
76
+ * that exact background, and no longer a usable "tint" anywhere else (gap #12a, ask 2).
77
+ *
78
+ * `space` must match whatever space `alpha` was actually solved in — `getAlphaColorP3` solves
79
+ * per-channel in P3, and verifying that against an sRGB composite rejects perfectly good
80
+ * solves (they were never trying to reproduce the sRGB composite in the first place).
57
81
  *
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),
82
+ * So verify the solve and fall back to the opaque value where either check fails. Only ever
83
+ * called for a custom background; on white/black every step solves exactly at low alpha,
60
84
  * so the default output — and portal parity — is untouched either way.
61
85
  */
62
- function opaqueWhereUnsolvable(background, solid, alpha) {
86
+ function opaqueWhereUnsolvable(background, solid, alpha, capAlphaSteps = 0, space = "srgb") {
63
87
  let bg;
64
88
  try {
65
- bg = new Color(background).to("srgb").coords;
89
+ bg = new Color(background).to(space).coords;
66
90
  } catch {
67
91
  return alpha;
68
92
  }
@@ -71,9 +95,10 @@ function opaqueWhereUnsolvable(background, solid, alpha) {
71
95
  if (!target) return step;
72
96
  try {
73
97
  const ink = new Color(step);
74
- const fg = ink.to("srgb").coords;
98
+ const fg = ink.to(space).coords;
75
99
  const a = ink.alpha;
76
- return new Color("srgb", [
100
+ if (i < capAlphaSteps && a > ALPHA_CAP) return target;
101
+ return new Color(space, [
77
102
  0,
78
103
  1,
79
104
  2
@@ -83,21 +108,139 @@ function opaqueWhereUnsolvable(background, solid, alpha) {
83
108
  }
84
109
  });
85
110
  }
111
+ /**
112
+ * Weight (0–1) each of the accent scale's first 5 steps is pulled toward the canvas's own
113
+ * hue/chroma; step 6 and up (borders, solid, text) stay the true accent untouched.
114
+ */
115
+ const CANVAS_TINT_WEIGHTS = [
116
+ 1,
117
+ .8,
118
+ .6,
119
+ .4,
120
+ .2
121
+ ];
122
+ /**
123
+ * Pull the accent scale's low steps toward the canvas's own color (gap #12a, ask 1): Radix's
124
+ * generator sets every step's hue/chroma from the accent itself and only re-curves lightness
125
+ * against the background, so on a chromatic canvas the low steps (app/component background
126
+ * territory) stay a flat wash of the accent instead of a tint of the page — right where the
127
+ * now-canvas-tinted neutral scale (gap #12) visibly disagrees with it. Step 9 (the solid,
128
+ * brand-defining step) and everything above step 5 is left exactly as Radix generated it.
129
+ *
130
+ * Mixed in OKLab (rectangular a/b), not OKLCH (polar hue + chroma): a circular hue
131
+ * interpolation between two far-apart hues (a blue accent on a yellow-green canvas, say) has
132
+ * to sweep through whatever unrelated hues sit on the "shorter" arc between them — a vivid
133
+ * green/teal/cyan that belongs to neither endpoint. Linearly mixing a/b instead moves along
134
+ * the straight line between the two points, which naturally desaturates toward neutral as the
135
+ * two colors pull in more different directions, rather than swinging through a third, unrelated
136
+ * hue. Lightness (shared between OKLab and OKLCH) is untouched either way — Radix's own
137
+ * background-aware curve already owns it.
138
+ *
139
+ * Takes/returns `Color` objects rather than strings so callers can reuse the same tinted
140
+ * coordinates for both the sRGB and wide-gamut representations without a lossy round trip.
141
+ */
142
+ function tintLowStepsFromCanvas(solidScale, bg) {
143
+ const [, bgA, bgB] = bg.to("oklab").coords;
144
+ return solidScale.map((step, i) => {
145
+ const weight = CANVAS_TINT_WEIGHTS[i];
146
+ if (!weight) return step;
147
+ const [lightness, a, b] = step.to("oklab").coords;
148
+ return new Color("oklab", [
149
+ lightness ?? 0,
150
+ (a ?? 0) * (1 - weight) + (bgA ?? 0) * weight,
151
+ (b ?? 0) * (1 - weight) + (bgB ?? 0) * weight
152
+ ]);
153
+ });
154
+ }
155
+ /** Mirrors generate-radix-colors.tsx's private toOklchString (not exported by the vendored file). */
156
+ function toOklchWideGamutString(color) {
157
+ const L = +((color.coords[0] ?? 0) * 100).toFixed(1);
158
+ return color.to("oklch").toString({ precision: 4 }).replace(/(\S+)(.+)/, `oklch(${L}%$2`);
159
+ }
160
+ /** Same fixed alpha the vendored generator uses for `accentSurface` — a stylistic choice, not a solve. */
161
+ function surfaceAlphaFor(appearance) {
162
+ return appearance === "light" ? .8 : .5;
163
+ }
164
+ /**
165
+ * Canvas-tinted low steps for the accent scale, or the untouched scale (and `tinted: false`)
166
+ * when the canvas isn't chromatic enough to bother (gap #12a, ask 1). Only indices
167
+ * `0..CANVAS_TINT_WEIGHTS.length-1` are ever replaced — steps 6-12 come back byte-identical to
168
+ * what Radix generated, in both representations (no lossy sRGB round trip for the untouched
169
+ * wide-gamut steps).
170
+ *
171
+ * `tinted` tells the caller whether to scope `opaqueWhereUnsolvable`'s alpha cap in at all —
172
+ * the cap only makes sense for steps this function actually retargeted; a caller that applied
173
+ * it unconditionally on any custom `background` would wrongly force perfectly fine low-alpha
174
+ * steps opaque on an *achromatic* custom background (an explicit `#000000`/`#f5f5f5`, say),
175
+ * where nothing here ever ran.
176
+ *
177
+ * Re-solves alpha for the tinted indices too: the generator's own `accentScaleAlpha` was
178
+ * solved against the *pre-tint* solid, so once that solid moves the old alpha value no longer
179
+ * reproduces it — left alone, every tinted step would fail `opaqueWhereUnsolvable`'s deltaE
180
+ * check and go opaque, defeating the point (the `soft`-card fill this exists for would never
181
+ * actually be translucent on a tinted canvas). Reuses Radix's own alpha solver so the result
182
+ * matches what the generator itself would have produced had it been given this target directly.
183
+ *
184
+ * `accentSurface` (the `surface` card variant's fill) gets the same treatment: the generator
185
+ * computes it as a fixed-alpha composite of step 2 — index 1, always tinted — over the
186
+ * background, so recomputing it from the *tinted* step 2 at that same fixed alpha (0.8 light,
187
+ * 0.5 dark; not a solve) keeps it in sync with `soft`'s fill instead of staying the old,
188
+ * untinted wash (review-findings.md #13).
189
+ */
190
+ function accentScaleForCanvas(scale, background, appearance) {
191
+ try {
192
+ const bg = new Color(background).to("oklch");
193
+ const [, bgChroma] = bg.coords;
194
+ if (!bgChroma || bgChroma < CHROMATIC_THRESHOLD) return {
195
+ ...scale,
196
+ tinted: false
197
+ };
198
+ const tinted = tintLowStepsFromCanvas(scale.accentScale.map((hex) => new Color(hex).to("oklch")), bg);
199
+ const accentScale = scale.accentScale.map((original, i) => CANVAS_TINT_WEIGHTS[i] ? tinted[i].to("srgb").toString({ format: "hex" }) : original);
200
+ const accentScaleWideGamut = scale.accentScaleWideGamut.map((original, i) => CANVAS_TINT_WEIGHTS[i] ? toOklchWideGamutString(tinted[i]) : original);
201
+ const accentScaleAlpha = scale.accentScaleAlpha.map((original, i) => CANVAS_TINT_WEIGHTS[i] ? getAlphaColorSrgb(accentScale[i], scale.background) : original);
202
+ const accentScaleAlphaWideGamut = scale.accentScaleAlphaWideGamut.map((original, i) => CANVAS_TINT_WEIGHTS[i] ? getAlphaColorP3(accentScaleWideGamut[i], scale.background) : original);
203
+ const surfaceAlpha = surfaceAlphaFor(appearance);
204
+ return {
205
+ accentScale,
206
+ accentScaleWideGamut,
207
+ accentScaleAlpha,
208
+ accentScaleAlphaWideGamut,
209
+ accentSurface: getAlphaColorSrgb(accentScale[1], scale.background, surfaceAlpha),
210
+ accentSurfaceWideGamut: getAlphaColorP3(accentScaleWideGamut[1], scale.background, surfaceAlpha),
211
+ tinted: true
212
+ };
213
+ } catch {
214
+ return {
215
+ ...scale,
216
+ tinted: false
217
+ };
218
+ }
219
+ }
86
220
  /** Generate the Radix-style accent scale for one appearance from a single accent color. */
87
221
  function generateAccentScale(accent, appearance, opts) {
88
222
  const background = opts?.background ?? DEFAULTS[appearance].background;
89
223
  const scale = generateRadixColors({
90
224
  appearance,
91
225
  accent,
92
- gray: opts?.neutral ? NEUTRAL_BASE[opts.neutral] : DEFAULTS[appearance].gray,
226
+ gray: opts?.neutral ? isSparkNeutralFamily(opts.neutral) ? NEUTRAL_BASE[opts.neutral] : opts.neutral : DEFAULTS[appearance].gray,
93
227
  background
94
228
  });
229
+ const { accentScale, accentScaleWideGamut, accentScaleAlpha: canvasAccentScaleAlpha, accentScaleAlphaWideGamut: canvasAccentScaleAlphaWideGamut, accentSurface, accentSurfaceWideGamut, tinted } = opts?.background ? accentScaleForCanvas(scale, background, appearance) : {
230
+ ...scale,
231
+ tinted: false
232
+ };
233
+ const capAlphaSteps = tinted ? CANVAS_TINT_WEIGHTS.length : 0;
95
234
  const solved = opts?.background ? {
96
235
  ...scale,
97
- accentScaleAlpha: opaqueWhereUnsolvable(background, scale.accentScale, scale.accentScaleAlpha),
98
- accentScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.accentScaleWideGamut, scale.accentScaleAlphaWideGamut),
236
+ accentScale,
237
+ accentScaleWideGamut,
238
+ accentSurface,
239
+ accentSurfaceWideGamut,
240
+ accentScaleAlpha: opaqueWhereUnsolvable(background, accentScale, canvasAccentScaleAlpha, capAlphaSteps),
241
+ accentScaleAlphaWideGamut: opaqueWhereUnsolvable(background, accentScaleWideGamut, canvasAccentScaleAlphaWideGamut, capAlphaSteps, "p3"),
99
242
  grayScaleAlpha: opaqueWhereUnsolvable(background, scale.grayScale, scale.grayScaleAlpha),
100
- grayScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut)
243
+ grayScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut, 0, "p3")
101
244
  } : scale;
102
245
  return {
103
246
  ...solved,
@@ -115,15 +258,28 @@ function readableContrast(step9Hex, native, min) {
115
258
  Math.max(.08 * (chroma ?? 0), .04),
116
259
  hue ?? 0
117
260
  ]).to("srgb").toString({ format: "hex" });
261
+ const ratioOf = (hex) => new Color(hex).contrast(step9, "WCAG21");
118
262
  const candidates = [
119
263
  native,
120
264
  tinted(.25),
121
265
  tinted(.15)
122
266
  ];
123
- const ratios = candidates.map((c) => new Color(c).contrast(step9, "WCAG21"));
124
- const passing = ratios.findIndex((r) => r >= min);
267
+ const passing = candidates.map(ratioOf).findIndex((r) => r >= min);
125
268
  if (passing !== -1) return candidates[passing];
126
- return candidates[ratios.indexOf(Math.max(...ratios))];
269
+ const lightness9 = step9.to("oklch").coords[0] ?? .5;
270
+ const toward = ratioOf(tinted(0)) >= ratioOf(tinted(1)) ? 0 : 1;
271
+ let fails = lightness9;
272
+ let passes = toward;
273
+ for (let i = 0; i < 16; i++) {
274
+ const mid = (fails + passes) / 2;
275
+ if (ratioOf(tinted(mid)) >= min) passes = mid;
276
+ else fails = mid;
277
+ }
278
+ const found = tinted(passes);
279
+ if (ratioOf(found) >= min) return found;
280
+ const all = [...candidates, tinted(toward)];
281
+ const allRatios = all.map(ratioOf);
282
+ return all[allRatios.indexOf(Math.max(...allRatios))];
127
283
  }
128
284
  /** sRGB `--spark-*` declarations for a generated accent scale. */
129
285
  function accentSparkVars(scale) {
@@ -154,7 +310,7 @@ function getMatchingNeutral(accent) {
154
310
  if (byName) return byName;
155
311
  try {
156
312
  const [, chroma, hueRaw] = new Color(accent).to("oklch").coords;
157
- if (!chroma || chroma < .04) return "gray";
313
+ if (!chroma || chroma < CHROMATIC_THRESHOLD) return "gray";
158
314
  const hue = ((hueRaw ?? 0) % 360 + 360) % 360;
159
315
  if (hue >= 300 || hue < 35) return "mauve";
160
316
  if (hue < 115) return "sand";
@@ -165,6 +321,180 @@ function getMatchingNeutral(accent) {
165
321
  return "slate";
166
322
  }
167
323
  }
324
+ /**
325
+ * Fill steps (Radix roles 1-5), measured from the surface toward the *scrim* — the pole
326
+ * opposite the ink. Magnitudes are unchanged from the original ink-directed ramp; only
327
+ * the direction flips, so a fill and the text on it move apart instead of together.
328
+ */
329
+ const INVERTED_FILL_RAMP = [
330
+ .06,
331
+ .12,
332
+ .2,
333
+ .3,
334
+ .4
335
+ ];
336
+ /** Strongest border step (8, the hover/active edge). 6 is solved per accent; 7 splits them. */
337
+ const INVERTED_BORDER_TOP = .82;
338
+ /**
339
+ * Ink steps (9-12), measured from the surface toward the ink. Step 9 (the solid fill)
340
+ * lands on the ink itself; 10 eases back for hover; 11-12 are full-strength ink.
341
+ */
342
+ const INVERTED_INK_RAMP = [
343
+ 1,
344
+ .88,
345
+ 1,
346
+ 1
347
+ ];
348
+ /**
349
+ * Lowest alpha at which the ink, laid over the surface, is 3:1 distinguishable from it
350
+ * (WCAG 1.4.11) — the floor for a border that has to read as an edge at all.
351
+ *
352
+ * Solved rather than hardcoded because it moves with the accent (0.57-0.72 across the
353
+ * curated set). Contrast against the surface rises monotonically with `t`, so bisection
354
+ * finds the boundary.
355
+ *
356
+ * Targets 1% above the floor rather than landing on it: the step ships as `#rrggbbaa`, and
357
+ * 8-bit rounding on any of the four channels can drop an exact solve back under 3:1.
358
+ */
359
+ function solveBorderFloor(surface, ink) {
360
+ const target = MIN_GRAPHIC_CONTRAST * 1.01;
361
+ const ratioAt = (t) => new Color(Color.mix(surface, ink, t, { space: "srgb" })).contrast(surface, "WCAG21");
362
+ let fails = 0;
363
+ let passes = 1;
364
+ for (let i = 0; i < 16; i++) {
365
+ const mid = (fails + passes) / 2;
366
+ if (ratioAt(mid) >= target) passes = mid;
367
+ else fails = mid;
368
+ }
369
+ return passes;
370
+ }
371
+ /** OKLCH string in the wide-gamut format the generator emits (`oklch(96.1% 0.0155 254.2)`). */
372
+ function oklchString(color) {
373
+ const oklch = color.to("oklch");
374
+ const lightness = +((oklch.coords[0] ?? 0) * 100).toFixed(1);
375
+ return oklch.toString({ precision: 4 }).replace(/(\S+)(.+)/, `oklch(${lightness}%$2`);
376
+ }
377
+ /**
378
+ * Re-cast a scale for use ON its own solid (step-9) fill — the `inverted` knob.
379
+ *
380
+ * A solid accent control on a solid accent surface is invisible: both paint `--spark-9`.
381
+ * Inverting swaps that pair — step 9 and its contrast ink trade places — so the solid fill
382
+ * and its text are contrast-safe **by construction, for any accent**: `accentContrast` is
383
+ * already the WCAG-checked readable ink for step 9 (`readableContrast`).
384
+ *
385
+ * The non-solid styles need a second idea, because the ink is also the text colour. A fill
386
+ * blended surface→ink moves the background *toward* the foreground, so ink-on-fill contrast
387
+ * falls monotonically with `t` — and measured across the curated set, the highest `t` that
388
+ * still clears 4.5:1 (0.174) sits below the lowest `t` that is 3:1 distinguishable from the
389
+ * surface (0.543). On that line no fill can be both legible and visible, on any accent.
390
+ *
391
+ * So fills blend the other way, toward the **scrim** — the lightness pole opposite the ink,
392
+ * carrying the surface's own hue. Contrast against the ink now *rises* with `t`, which puts
393
+ * every non-solid style at or above the solid pair's ratio for free. Borders keep stepping
394
+ * toward the ink (a scrim-side border caps at ~2:1 on light accents like orange, so it can
395
+ * never clear 1.4.11), starting at the solved 3:1 floor.
396
+ *
397
+ * Appearance-stable on purpose. The generator pins step 9 to the accent itself, so the
398
+ * surface is identical in light and dark — which makes the whole inverted scale identical
399
+ * too, and `[data-inverted]` needs no `.dark` twin.
400
+ *
401
+ * Two things this is deliberately NOT:
402
+ * - **A positional mirror** (`1↔12`). It leaves the solid fill only 2.04:1 from the card in
403
+ * dark and 2.23:1 on a light accent like orange — under the 3:1 graphical floor, i.e. still
404
+ * invisible, which is the bug this exists to fix.
405
+ * - **A regenerated on-surface ramp.** Feeding the surface back in as `background` produces
406
+ * off-hue overlays (a warm wash on a blue card) and ink at 3.35:1.
407
+ *
408
+ * Values are literals, not `var(--spark-N)` aliases: a scale can't re-point to itself
409
+ * (`--spark-1: var(--spark-12)` next to `--spark-12: var(--spark-1)` is a cycle, and CSS
410
+ * discards both).
411
+ */
412
+ function invertAccentScale(scale) {
413
+ const surface = scale.accentScale[8];
414
+ const ink = scale.accentContrast;
415
+ const surfaceColor = new Color(surface);
416
+ const inkColor = new Color(ink);
417
+ const [surfaceLightness, surfaceChroma, surfaceHue] = surfaceColor.to("oklch").coords;
418
+ const scrimColor = new Color("oklch", [
419
+ (inkColor.to("oklch").coords[0] ?? 0) > (surfaceLightness ?? 0) ? .12 : .98,
420
+ Math.max(.08 * (surfaceChroma ?? 0), .02),
421
+ surfaceHue ?? 0
422
+ ]);
423
+ const borderFloor = solveBorderFloor(surfaceColor, inkColor);
424
+ const ramp = [
425
+ ...INVERTED_FILL_RAMP.map((t) => ({
426
+ toward: scrimColor,
427
+ t
428
+ })),
429
+ {
430
+ toward: inkColor,
431
+ t: borderFloor
432
+ },
433
+ {
434
+ toward: inkColor,
435
+ t: (borderFloor + INVERTED_BORDER_TOP) / 2
436
+ },
437
+ {
438
+ toward: inkColor,
439
+ t: INVERTED_BORDER_TOP
440
+ },
441
+ ...INVERTED_INK_RAMP.map((t) => ({
442
+ toward: inkColor,
443
+ t
444
+ }))
445
+ ];
446
+ const blend = ({ toward, t }) => new Color(Color.mix(surfaceColor, toward, t, { space: "srgb" }));
447
+ const steps = ramp.map((s) => blend(s).to("srgb").toString({ format: "hex" }));
448
+ const stepsWideGamut = ramp.map((s) => oklchString(blend(s)));
449
+ const alpha = ramp.map(({ toward, t }) => {
450
+ const translucent = toward.clone();
451
+ translucent.alpha = t;
452
+ return translucent.to("srgb").toString({ format: "hex" });
453
+ });
454
+ const alphaWideGamut = ramp.map(({ toward, t }) => {
455
+ const translucent = toward.clone().to("p3");
456
+ translucent.alpha = t;
457
+ return translucent.toString({ precision: 4 }).replace("color(p3 ", "color(display-p3 ");
458
+ });
459
+ return {
460
+ ...scale,
461
+ accentScale: steps,
462
+ accentScaleWideGamut: stepsWideGamut,
463
+ accentScaleAlpha: alpha,
464
+ accentScaleAlphaWideGamut: alphaWideGamut,
465
+ accentContrast: surface,
466
+ accentOnSolid: surface,
467
+ accentSurface: steps[1],
468
+ accentSurfaceWideGamut: stepsWideGamut[1]
469
+ };
470
+ }
471
+ /**
472
+ * `neutralColor="auto"` resolution: prefer the canvas's own tint when it has one — a
473
+ * chromatic `background` seeds a visibly warmer/cooler ramp than bucketing it into one
474
+ * of the 6 named families would (see gap #12). Falls back to accent-matched bucketing
475
+ * when there's no tinted canvas to pull from.
476
+ */
477
+ function resolveAutoNeutral(accent, background) {
478
+ if (background) try {
479
+ const [, chroma] = new Color(background).to("oklch").coords;
480
+ if (chroma && chroma >= CHROMATIC_THRESHOLD) return background;
481
+ } catch {}
482
+ return getMatchingNeutral(accent);
483
+ }
484
+ /**
485
+ * Whether a string parses as a color at all — for validating a custom `neutralColor`
486
+ * before it reaches the generator. An unparseable seed (e.g. a typo'd family name like
487
+ * `"slat"`) would otherwise throw deep inside `generateRadixColors`, taking the whole
488
+ * accent scale down with it via the caller's shared try/catch.
489
+ */
490
+ function isParsableColor(value) {
491
+ try {
492
+ new Color(value).to("oklch");
493
+ return true;
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
168
498
  /** The 27 tokens every scale family declares, under an arbitrary prefix. */
169
499
  function scaleVars(prefix, steps, alpha, contrast, onSolid, surface) {
170
500
  return [
@@ -185,6 +515,23 @@ function sparkVars(steps, alpha, contrast, onSolid, surface, background) {
185
515
  ];
186
516
  }
187
517
  /**
518
+ * The same `--neutral-*` tokens `neutralSparkVars` emits, pointed back at a static Radix
519
+ * family instead of a generated scale — the counterpart for an appearance that has *no*
520
+ * custom canvas of its own.
521
+ *
522
+ * Ambient (no explicit `appearance`) mode emits both appearances' scales into one unlayered
523
+ * `<style>`, but a custom canvas only regenerates neutrals for the appearance it belongs to.
524
+ * Left undeclared, the other block doesn't fall back — nothing resets a custom property, so
525
+ * its sibling's canvas-tinted values simply stay in effect there, and being unlayered they
526
+ * outrank `[data-neutral-color]`'s own `@layer base` mapping too. Declaring the same token
527
+ * set closes that. `var(--<family>-*)` is itself appearance-aware (Radix ships the dark
528
+ * scales under `.dark`), so one declaration resolves correctly wherever the block applies.
529
+ */
530
+ function neutralFamilyVars(family) {
531
+ const ref = (suffix) => `var(--${family}-${suffix})`;
532
+ return scaleVars("neutral", Array.from({ length: 12 }, (_, i) => ref(`${i + 1}`)), Array.from({ length: 12 }, (_, i) => ref(`a${i + 1}`)), "var(--neutral-12)", "#fff", "var(--neutral-2)");
533
+ }
534
+ /**
188
535
  * `--neutral-*` declarations from the gray scale the generator produces alongside the
189
536
  * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
190
537
  * static Radix family. Worth consuming only when the canvas is custom: the static
@@ -202,6 +549,6 @@ function neutralSparkVarsP3(scale) {
202
549
  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
550
  }
204
551
  //#endregion
205
- export { CURATED_ACCENTS, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, neutralSparkVars, neutralSparkVarsP3 };
552
+ export { CURATED_ACCENTS, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral };
206
553
 
207
554
  //# 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/** 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
+ {"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 {\n generateRadixColors,\n getAlphaColorP3,\n getAlphaColorSrgb,\n} from './generate-radix-colors'\nimport {\n isSparkNeutralFamily,\n type SparkNeutral,\n type SparkNeutralFamily,\n} 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<SparkNeutralFamily, 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 /**\n * Neutral family, or an arbitrary color to seed the neutral scale (regenerated against\n * `background`, same as a named family). Pass a resolved value, not `'auto'` — this\n * option isn't itself typed to exclude it (an open string union can't forbid one\n * literal), but nothing in `<Theme>` ever forwards `'auto'` this far.\n */\n neutral?: SparkNeutral\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 * OKLCH chroma below which a color reads as neutral rather than tinted — shared by every\n * \"is this canvas/accent actually chromatic\" check (getMatchingNeutral, resolveAutoNeutral,\n * accentScaleForCanvas).\n */\nconst CHROMATIC_THRESHOLD = 0.04\n\n/**\n * An alpha step past this needs to cover the canvas almost completely to reproduce its\n * target — at that point it isn't a tint anymore, and the opaque solid is both cheaper and\n * more predictable. Only applied to the accent ramp's canvas-tinted low steps (gap #12a,\n * ask 2 — specifically the steps `tintLowStepsFromCanvas` retargets; steps 6-12 are by-design\n * high-alpha ink/solid tokens even on plain white — capping *those* would be a regression, not\n * a fix). Same shape as the deltaE check below, widened from \"could not solve\" to \"solved, but\n * not usefully\".\n */\nconst ALPHA_CAP = 0.5\n\n/**\n * Replace alpha steps the solver could not actually solve — or, within `capAlphaSteps` leading\n * steps, solved only at a near-opaque alpha — 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. Separately, even a solve that\n * *succeeds* can need alpha past `ALPHA_CAP` to get there — technically correct only over\n * that exact background, and no longer a usable \"tint\" anywhere else (gap #12a, ask 2).\n *\n * `space` must match whatever space `alpha` was actually solved in — `getAlphaColorP3` solves\n * per-channel in P3, and verifying that against an sRGB composite rejects perfectly good\n * solves (they were never trying to reproduce the sRGB composite in the first place).\n *\n * So verify the solve and fall back to the opaque value where either check fails. Only ever\n * called for a custom background; on white/black every step solves exactly at low alpha,\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 capAlphaSteps = 0,\n space: 'srgb' | 'p3' = 'srgb',\n): string[] {\n let bg: number[]\n try {\n bg = new Color(background).to(space).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(space).coords\n const a = ink.alpha as number\n if (i < capAlphaSteps && a > ALPHA_CAP) return target\n const composited = new Color(\n space,\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/**\n * Weight (0–1) each of the accent scale's first 5 steps is pulled toward the canvas's own\n * hue/chroma; step 6 and up (borders, solid, text) stay the true accent untouched.\n */\nconst CANVAS_TINT_WEIGHTS = [1, 0.8, 0.6, 0.4, 0.2]\n\n/**\n * Pull the accent scale's low steps toward the canvas's own color (gap #12a, ask 1): Radix's\n * generator sets every step's hue/chroma from the accent itself and only re-curves lightness\n * against the background, so on a chromatic canvas the low steps (app/component background\n * territory) stay a flat wash of the accent instead of a tint of the page — right where the\n * now-canvas-tinted neutral scale (gap #12) visibly disagrees with it. Step 9 (the solid,\n * brand-defining step) and everything above step 5 is left exactly as Radix generated it.\n *\n * Mixed in OKLab (rectangular a/b), not OKLCH (polar hue + chroma): a circular hue\n * interpolation between two far-apart hues (a blue accent on a yellow-green canvas, say) has\n * to sweep through whatever unrelated hues sit on the \"shorter\" arc between them — a vivid\n * green/teal/cyan that belongs to neither endpoint. Linearly mixing a/b instead moves along\n * the straight line between the two points, which naturally desaturates toward neutral as the\n * two colors pull in more different directions, rather than swinging through a third, unrelated\n * hue. Lightness (shared between OKLab and OKLCH) is untouched either way — Radix's own\n * background-aware curve already owns it.\n *\n * Takes/returns `Color` objects rather than strings so callers can reuse the same tinted\n * coordinates for both the sRGB and wide-gamut representations without a lossy round trip.\n */\nfunction tintLowStepsFromCanvas(\n solidScale: readonly Color[],\n bg: Color,\n): Color[] {\n const [, bgA, bgB] = bg.to('oklab').coords\n return solidScale.map((step, i) => {\n const weight = CANVAS_TINT_WEIGHTS[i]\n if (!weight) return step\n const [lightness, a, b] = step.to('oklab').coords\n return new Color('oklab', [\n lightness ?? 0,\n (a ?? 0) * (1 - weight) + (bgA ?? 0) * weight,\n (b ?? 0) * (1 - weight) + (bgB ?? 0) * weight,\n ])\n })\n}\n\n/** Mirrors generate-radix-colors.tsx's private toOklchString (not exported by the vendored file). */\nfunction toOklchWideGamutString(color: Color): string {\n const L = +((color.coords[0] ?? 0) * 100).toFixed(1)\n return color\n .to('oklch')\n .toString({ precision: 4 })\n .replace(/(\\S+)(.+)/, `oklch(${L}%$2`)\n}\n\n/** Same fixed alpha the vendored generator uses for `accentSurface` — a stylistic choice, not a solve. */\nfunction surfaceAlphaFor(appearance: Appearance): number {\n return appearance === 'light' ? 0.8 : 0.5\n}\n\n/**\n * Canvas-tinted low steps for the accent scale, or the untouched scale (and `tinted: false`)\n * when the canvas isn't chromatic enough to bother (gap #12a, ask 1). Only indices\n * `0..CANVAS_TINT_WEIGHTS.length-1` are ever replaced — steps 6-12 come back byte-identical to\n * what Radix generated, in both representations (no lossy sRGB round trip for the untouched\n * wide-gamut steps).\n *\n * `tinted` tells the caller whether to scope `opaqueWhereUnsolvable`'s alpha cap in at all —\n * the cap only makes sense for steps this function actually retargeted; a caller that applied\n * it unconditionally on any custom `background` would wrongly force perfectly fine low-alpha\n * steps opaque on an *achromatic* custom background (an explicit `#000000`/`#f5f5f5`, say),\n * where nothing here ever ran.\n *\n * Re-solves alpha for the tinted indices too: the generator's own `accentScaleAlpha` was\n * solved against the *pre-tint* solid, so once that solid moves the old alpha value no longer\n * reproduces it — left alone, every tinted step would fail `opaqueWhereUnsolvable`'s deltaE\n * check and go opaque, defeating the point (the `soft`-card fill this exists for would never\n * actually be translucent on a tinted canvas). Reuses Radix's own alpha solver so the result\n * matches what the generator itself would have produced had it been given this target directly.\n *\n * `accentSurface` (the `surface` card variant's fill) gets the same treatment: the generator\n * computes it as a fixed-alpha composite of step 2 — index 1, always tinted — over the\n * background, so recomputing it from the *tinted* step 2 at that same fixed alpha (0.8 light,\n * 0.5 dark; not a solve) keeps it in sync with `soft`'s fill instead of staying the old,\n * untinted wash (review-findings.md #13).\n */\nfunction accentScaleForCanvas(\n scale: GeneratedColors,\n background: string,\n appearance: Appearance,\n): Pick<\n GeneratedColors,\n | 'accentScale'\n | 'accentScaleWideGamut'\n | 'accentScaleAlpha'\n | 'accentScaleAlphaWideGamut'\n | 'accentSurface'\n | 'accentSurfaceWideGamut'\n> & { tinted: boolean } {\n try {\n const bg = new Color(background).to('oklch')\n const [, bgChroma] = bg.coords\n if (!bgChroma || bgChroma < CHROMATIC_THRESHOLD)\n return { ...scale, tinted: false }\n const tinted = tintLowStepsFromCanvas(\n scale.accentScale.map((hex) => new Color(hex).to('oklch')),\n bg,\n )\n const accentScale = scale.accentScale.map((original, i) =>\n CANVAS_TINT_WEIGHTS[i]\n ? tinted[i]!.to('srgb').toString({ format: 'hex' })\n : original,\n ) as typeof scale.accentScale\n const accentScaleWideGamut = scale.accentScaleWideGamut.map((original, i) =>\n CANVAS_TINT_WEIGHTS[i] ? toOklchWideGamutString(tinted[i]!) : original,\n ) as typeof scale.accentScaleWideGamut\n const accentScaleAlpha = scale.accentScaleAlpha.map((original, i) =>\n CANVAS_TINT_WEIGHTS[i]\n ? getAlphaColorSrgb(accentScale[i]!, scale.background)\n : original,\n ) as typeof scale.accentScaleAlpha\n const accentScaleAlphaWideGamut = scale.accentScaleAlphaWideGamut.map(\n (original, i) =>\n CANVAS_TINT_WEIGHTS[i]\n ? getAlphaColorP3(accentScaleWideGamut[i]!, scale.background)\n : original,\n ) as typeof scale.accentScaleAlphaWideGamut\n const surfaceAlpha = surfaceAlphaFor(appearance)\n const accentSurface = getAlphaColorSrgb(\n accentScale[1]!,\n scale.background,\n surfaceAlpha,\n )\n const accentSurfaceWideGamut = getAlphaColorP3(\n accentScaleWideGamut[1]!,\n scale.background,\n surfaceAlpha,\n )\n return {\n accentScale,\n accentScaleWideGamut,\n accentScaleAlpha,\n accentScaleAlphaWideGamut,\n accentSurface,\n accentSurfaceWideGamut,\n tinted: true,\n }\n } catch {\n return { ...scale, tinted: false }\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 ? isSparkNeutralFamily(opts.neutral)\n ? NEUTRAL_BASE[opts.neutral]\n : opts.neutral\n : DEFAULTS[appearance].gray,\n background,\n })\n const {\n accentScale,\n accentScaleWideGamut,\n accentScaleAlpha: canvasAccentScaleAlpha,\n accentScaleAlphaWideGamut: canvasAccentScaleAlphaWideGamut,\n accentSurface,\n accentSurfaceWideGamut,\n tinted,\n } = opts?.background\n ? accentScaleForCanvas(scale, background, appearance)\n : { ...scale, tinted: false }\n const capAlphaSteps = tinted ? CANVAS_TINT_WEIGHTS.length : 0\n const solved = opts?.background\n ? {\n ...scale,\n accentScale,\n accentScaleWideGamut,\n accentSurface,\n accentSurfaceWideGamut,\n accentScaleAlpha: opaqueWhereUnsolvable(\n background,\n accentScale,\n canvasAccentScaleAlpha,\n capAlphaSteps,\n ) as typeof scale.accentScaleAlpha,\n accentScaleAlphaWideGamut: opaqueWhereUnsolvable(\n background,\n accentScaleWideGamut,\n canvasAccentScaleAlphaWideGamut,\n capAlphaSteps,\n 'p3',\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 0,\n 'p3',\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 ratioOf = (hex: string) => new Color(hex).contrast(step9, 'WCAG21')\n const candidates = [native, tinted(0.25), tinted(0.15)]\n const ratios = candidates.map(ratioOf)\n const passing = ratios.findIndex((r) => r >= min)\n if (passing !== -1) return candidates[passing]!\n\n // The ladder above is a *preference* (least extreme ink first), not a search — it\n // stops at 0.15 and so missed accents that only clear `min` a little darker still\n // (#936c89 landed at 4.47 when 0.05 gives 4.73). Contrast rises monotonically as\n // the ink's lightness moves away from the fill's, so search outward from step 9\n // toward whichever extreme is stronger: if a passing ink exists, this finds it.\n const lightness9 = step9.to('oklch').coords[0] ?? 0.5\n const toward = ratioOf(tinted(0)) >= ratioOf(tinted(1)) ? 0 : 1\n let fails = lightness9\n let passes = toward\n for (let i = 0; i < 16; i++) {\n const mid = (fails + passes) / 2\n if (ratioOf(tinted(mid)) >= min) passes = mid\n else fails = mid\n }\n const found = tinted(passes)\n if (ratioOf(found) >= min) return found\n\n // No ink of any lightness clears `min` on this fill — step 9 itself is too\n // mid-toned. Return the strongest available; the palette sweep flags the accent.\n const all = [...candidates, tinted(toward)]\n const allRatios = all.map(ratioOf)\n return all[allRatios.indexOf(Math.max(...allRatios))]!\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, SparkNeutralFamily> = {\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): SparkNeutralFamily {\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 < CHROMATIC_THRESHOLD) 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/**\n * Fill steps (Radix roles 1-5), measured from the surface toward the *scrim* — the pole\n * opposite the ink. Magnitudes are unchanged from the original ink-directed ramp; only\n * the direction flips, so a fill and the text on it move apart instead of together.\n */\nconst INVERTED_FILL_RAMP = [0.06, 0.12, 0.2, 0.3, 0.4]\n\n/** Strongest border step (8, the hover/active edge). 6 is solved per accent; 7 splits them. */\nconst INVERTED_BORDER_TOP = 0.82\n\n/**\n * Ink steps (9-12), measured from the surface toward the ink. Step 9 (the solid fill)\n * lands on the ink itself; 10 eases back for hover; 11-12 are full-strength ink.\n */\nconst INVERTED_INK_RAMP = [1, 0.88, 1, 1]\n\n/**\n * Lowest alpha at which the ink, laid over the surface, is 3:1 distinguishable from it\n * (WCAG 1.4.11) — the floor for a border that has to read as an edge at all.\n *\n * Solved rather than hardcoded because it moves with the accent (0.57-0.72 across the\n * curated set). Contrast against the surface rises monotonically with `t`, so bisection\n * finds the boundary.\n *\n * Targets 1% above the floor rather than landing on it: the step ships as `#rrggbbaa`, and\n * 8-bit rounding on any of the four channels can drop an exact solve back under 3:1.\n */\nfunction solveBorderFloor(surface: Color, ink: Color): number {\n const target = MIN_GRAPHIC_CONTRAST * 1.01\n const ratioAt = (t: number) =>\n new Color(Color.mix(surface, ink, t, { space: 'srgb' })).contrast(\n surface,\n 'WCAG21',\n )\n let fails = 0\n let passes = 1\n for (let i = 0; i < 16; i++) {\n const mid = (fails + passes) / 2\n if (ratioAt(mid) >= target) passes = mid\n else fails = mid\n }\n return passes\n}\n\n/** OKLCH string in the wide-gamut format the generator emits (`oklch(96.1% 0.0155 254.2)`). */\nfunction oklchString(color: Color): string {\n const oklch = color.to('oklch')\n const lightness = +((oklch.coords[0] ?? 0) * 100).toFixed(1)\n return oklch\n .toString({ precision: 4 })\n .replace(/(\\S+)(.+)/, `oklch(${lightness}%$2`)\n}\n\n/**\n * Re-cast a scale for use ON its own solid (step-9) fill — the `inverted` knob.\n *\n * A solid accent control on a solid accent surface is invisible: both paint `--spark-9`.\n * Inverting swaps that pair — step 9 and its contrast ink trade places — so the solid fill\n * and its text are contrast-safe **by construction, for any accent**: `accentContrast` is\n * already the WCAG-checked readable ink for step 9 (`readableContrast`).\n *\n * The non-solid styles need a second idea, because the ink is also the text colour. A fill\n * blended surface→ink moves the background *toward* the foreground, so ink-on-fill contrast\n * falls monotonically with `t` — and measured across the curated set, the highest `t` that\n * still clears 4.5:1 (0.174) sits below the lowest `t` that is 3:1 distinguishable from the\n * surface (0.543). On that line no fill can be both legible and visible, on any accent.\n *\n * So fills blend the other way, toward the **scrim** — the lightness pole opposite the ink,\n * carrying the surface's own hue. Contrast against the ink now *rises* with `t`, which puts\n * every non-solid style at or above the solid pair's ratio for free. Borders keep stepping\n * toward the ink (a scrim-side border caps at ~2:1 on light accents like orange, so it can\n * never clear 1.4.11), starting at the solved 3:1 floor.\n *\n * Appearance-stable on purpose. The generator pins step 9 to the accent itself, so the\n * surface is identical in light and dark — which makes the whole inverted scale identical\n * too, and `[data-inverted]` needs no `.dark` twin.\n *\n * Two things this is deliberately NOT:\n * - **A positional mirror** (`1↔12`). It leaves the solid fill only 2.04:1 from the card in\n * dark and 2.23:1 on a light accent like orange — under the 3:1 graphical floor, i.e. still\n * invisible, which is the bug this exists to fix.\n * - **A regenerated on-surface ramp.** Feeding the surface back in as `background` produces\n * off-hue overlays (a warm wash on a blue card) and ink at 3.35:1.\n *\n * Values are literals, not `var(--spark-N)` aliases: a scale can't re-point to itself\n * (`--spark-1: var(--spark-12)` next to `--spark-12: var(--spark-1)` is a cycle, and CSS\n * discards both).\n */\nexport function invertAccentScale(scale: SparkAccentScale): SparkAccentScale {\n const surface = scale.accentScale[8]! // the solid fill this scale will sit on\n const ink = scale.accentContrast // already AA-checked against that fill\n const surfaceColor = new Color(surface)\n const inkColor = new Color(ink)\n\n // The scrim: opposite the ink in lightness, tinted with the surface's own hue by the\n // same rule readableContrast uses for its ink ladder. Derived from the surface, so this\n // introduces no new palette input.\n const [surfaceLightness, surfaceChroma, surfaceHue] =\n surfaceColor.to('oklch').coords\n const inkLightness = inkColor.to('oklch').coords[0] ?? 0\n const scrimColor = new Color('oklch', [\n inkLightness > (surfaceLightness ?? 0) ? 0.12 : 0.98,\n Math.max(0.08 * (surfaceChroma ?? 0), 0.02),\n surfaceHue ?? 0,\n ])\n\n const borderFloor = solveBorderFloor(surfaceColor, inkColor)\n const ramp: { toward: Color; t: number }[] = [\n ...INVERTED_FILL_RAMP.map((t) => ({ toward: scrimColor, t })),\n { toward: inkColor, t: borderFloor },\n { toward: inkColor, t: (borderFloor + INVERTED_BORDER_TOP) / 2 },\n { toward: inkColor, t: INVERTED_BORDER_TOP },\n ...INVERTED_INK_RAMP.map((t) => ({ toward: inkColor, t })),\n ]\n\n // sRGB, not OKLab: the browser composites `--spark-aN` as source-over in gamma sRGB, so\n // mixing the opaque twin in any other space makes `--spark-N` a different colour from it\n // (measured up to dE2000 2.8 on the old OKLab mix, over ALPHA_SOLVE_TOLERANCE).\n const blend = ({ toward, t }: { toward: Color; t: number }) =>\n new Color(Color.mix(surfaceColor, toward, t, { space: 'srgb' }))\n const steps = ramp.map((s) =>\n blend(s).to('srgb').toString({ format: 'hex' }),\n ) as typeof scale.accentScale\n const stepsWideGamut = ramp.map((s) =>\n oklchString(blend(s)),\n ) as typeof scale.accentScaleWideGamut\n // Mixing surface→X at t in sRGB is the same colour as X at alpha t over the surface, so\n // each alpha step is just its own pole at rising opacity — no solve needed.\n const alpha = ramp.map(({ toward, t }) => {\n const translucent = toward.clone()\n translucent.alpha = t\n return translucent.to('srgb').toString({ format: 'hex' })\n }) as typeof scale.accentScaleAlpha\n const alphaWideGamut = ramp.map(({ toward, t }) => {\n const translucent = toward.clone().to('p3')\n translucent.alpha = t\n return translucent\n .toString({ precision: 4 })\n .replace('color(p3 ', 'color(display-p3 ')\n }) as typeof scale.accentScaleAlphaWideGamut\n return {\n ...scale,\n accentScale: steps,\n accentScaleWideGamut: stepsWideGamut,\n accentScaleAlpha: alpha,\n accentScaleAlphaWideGamut: alphaWideGamut,\n // Ink on the inverted solid fill is the accent itself — the other half of the swapped pair.\n accentContrast: surface,\n accentOnSolid: surface,\n accentSurface: steps[1]!,\n accentSurfaceWideGamut: stepsWideGamut[1]!,\n }\n}\n\n/**\n * `neutralColor=\"auto\"` resolution: prefer the canvas's own tint when it has one — a\n * chromatic `background` seeds a visibly warmer/cooler ramp than bucketing it into one\n * of the 6 named families would (see gap #12). Falls back to accent-matched bucketing\n * when there's no tinted canvas to pull from.\n */\nexport function resolveAutoNeutral(\n accent: string,\n background?: string,\n): string {\n if (background) {\n try {\n const [, chroma] = new Color(background).to('oklch').coords\n if (chroma && chroma >= CHROMATIC_THRESHOLD) return background\n } catch {\n // fall through to accent-matched neutral\n }\n }\n return getMatchingNeutral(accent)\n}\n\n/**\n * Whether a string parses as a color at all — for validating a custom `neutralColor`\n * before it reaches the generator. An unparseable seed (e.g. a typo'd family name like\n * `\"slat\"`) would otherwise throw deep inside `generateRadixColors`, taking the whole\n * accent scale down with it via the caller's shared try/catch.\n */\nexport function isParsableColor(value: string): boolean {\n try {\n new Color(value).to('oklch')\n return true\n } catch {\n return false\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 * The same `--neutral-*` tokens `neutralSparkVars` emits, pointed back at a static Radix\n * family instead of a generated scale — the counterpart for an appearance that has *no*\n * custom canvas of its own.\n *\n * Ambient (no explicit `appearance`) mode emits both appearances' scales into one unlayered\n * `<style>`, but a custom canvas only regenerates neutrals for the appearance it belongs to.\n * Left undeclared, the other block doesn't fall back — nothing resets a custom property, so\n * its sibling's canvas-tinted values simply stay in effect there, and being unlayered they\n * outrank `[data-neutral-color]`'s own `@layer base` mapping too. Declaring the same token\n * set closes that. `var(--<family>-*)` is itself appearance-aware (Radix ships the dark\n * scales under `.dark`), so one declaration resolves correctly wherever the block applies.\n */\nexport function neutralFamilyVars(\n family: Exclude<SparkNeutral, 'auto'>,\n): string[] {\n const ref = (suffix: string) => `var(--${family}-${suffix})`\n return scaleVars(\n 'neutral',\n Array.from({ length: 12 }, (_, i) => ref(`${i + 1}`)),\n Array.from({ length: 12 }, (_, i) => ref(`a${i + 1}`)),\n // The companion tokens' own static defaults, from spark-default.css's .light/.dark.\n 'var(--neutral-12)',\n '#fff',\n 'var(--neutral-2)',\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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"}
@@ -13,11 +13,11 @@ const radiusClasses = {
13
13
  };
14
14
  const containerChrome = {
15
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))]"
16
+ soft: "bg-(--spark-card-c-soft,var(--neutral-a3)) [--spark-card-bg:var(--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)) [--spark-card-bg:var(--spark-card-c-surface,var(--neutral-2))]",
18
+ outline: "border border-(--spark-card-c-border,var(--neutral-a6)) [--spark-card-bg:transparent]",
19
+ ghost: "[--spark-card-bg:transparent]",
20
+ solid: "bg-(--spark-card-c-solid,var(--neutral-1)) [--spark-card-bg:var(--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
21
  };
22
22
  const containerPaddingX = {
23
23
  ambient: "px-(--spark-card-px,1rem)",
@@ -35,16 +35,18 @@ const containerPaddingAll = {
35
35
  ghost: "",
36
36
  solid: "p-4"
37
37
  };
38
- function containerInteractiveClasses(resolved) {
38
+ function containerInteractiveClasses(resolved, elevated) {
39
39
  return [
40
40
  "cursor-pointer text-left no-underline transition-colors motion-reduce:transition-none",
41
41
  "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden",
42
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))"
43
+ (resolved === "ambient" || resolved === "soft") && (elevated ? "hover:[background-image:linear-gradient(var(--spark-card-c-soft-hover,var(--neutral-a4)),var(--spark-card-c-soft-hover,var(--neutral-a4)))]" : "hover:bg-(--spark-card-c-soft-hover,var(--neutral-a4))")
44
44
  ];
45
45
  }
46
+ const containerElevated = "bg-(--spark-elevated) [background-image:linear-gradient(var(--spark-card-bg,transparent),var(--spark-card-bg,transparent))] shadow-(--spark-card-shadow,var(--spark-shadow-1))";
46
47
  //#endregion
47
48
  exports.containerChrome = containerChrome;
49
+ exports.containerElevated = containerElevated;
48
50
  exports.containerInteractiveClasses = containerInteractiveClasses;
49
51
  exports.containerPaddingAll = containerPaddingAll;
50
52
  exports.containerPaddingX = containerPaddingX;