@epilot/spark-ui 0.18.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/components/attachment.d.cts +1 -1
  2. package/dist/components/attachment.d.ts +1 -1
  3. package/dist/components/bar-chart.cjs +1 -1
  4. package/dist/components/bar-chart.js +2 -2
  5. package/dist/components/breadcrumbs.cjs +1 -1
  6. package/dist/components/breadcrumbs.cjs.map +1 -1
  7. package/dist/components/breadcrumbs.js +1 -1
  8. package/dist/components/breadcrumbs.js.map +1 -1
  9. package/dist/components/bubble.d.cts +1 -1
  10. package/dist/components/bubble.d.ts +1 -1
  11. package/dist/components/button-group.cjs +1 -1
  12. package/dist/components/button-group.cjs.map +1 -1
  13. package/dist/components/button-group.js +1 -1
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/button.d.cts +2 -2
  16. package/dist/components/button.d.ts +2 -2
  17. package/dist/components/calendar.cjs +4 -4
  18. package/dist/components/calendar.cjs.map +1 -1
  19. package/dist/components/calendar.js +4 -4
  20. package/dist/components/calendar.js.map +1 -1
  21. package/dist/components/checkbox.cjs +1 -1
  22. package/dist/components/checkbox.cjs.map +1 -1
  23. package/dist/components/checkbox.js +1 -1
  24. package/dist/components/checkbox.js.map +1 -1
  25. package/dist/components/day-of-week-picker.cjs +2 -2
  26. package/dist/components/day-of-week-picker.cjs.map +1 -1
  27. package/dist/components/day-of-week-picker.js +2 -2
  28. package/dist/components/day-of-week-picker.js.map +1 -1
  29. package/dist/components/dropdown-menu.cjs +8 -8
  30. package/dist/components/dropdown-menu.cjs.map +1 -1
  31. package/dist/components/dropdown-menu.js +8 -8
  32. package/dist/components/dropdown-menu.js.map +1 -1
  33. package/dist/components/field.cjs +1 -1
  34. package/dist/components/field.cjs.map +1 -1
  35. package/dist/components/field.js +1 -1
  36. package/dist/components/field.js.map +1 -1
  37. package/dist/components/input-date.cjs +6 -6
  38. package/dist/components/input-date.cjs.map +1 -1
  39. package/dist/components/input-date.d.cts +2 -1
  40. package/dist/components/input-date.d.ts +2 -1
  41. package/dist/components/input-date.js +6 -6
  42. package/dist/components/input-date.js.map +1 -1
  43. package/dist/components/input-otp.cjs +2 -2
  44. package/dist/components/input-otp.cjs.map +1 -1
  45. package/dist/components/input-otp.js +2 -2
  46. package/dist/components/input-otp.js.map +1 -1
  47. package/dist/components/input.cjs +2 -2
  48. package/dist/components/input.cjs.map +1 -1
  49. package/dist/components/input.js +2 -2
  50. package/dist/components/input.js.map +1 -1
  51. package/dist/components/label.cjs +1 -1
  52. package/dist/components/label.cjs.map +1 -1
  53. package/dist/components/label.js +1 -1
  54. package/dist/components/label.js.map +1 -1
  55. package/dist/components/line-chart.cjs +3 -3
  56. package/dist/components/line-chart.js +4 -4
  57. package/dist/components/list.cjs +2 -2
  58. package/dist/components/list.cjs.map +1 -1
  59. package/dist/components/list.js +2 -2
  60. package/dist/components/list.js.map +1 -1
  61. package/dist/components/marker.cjs +2 -2
  62. package/dist/components/marker.cjs.map +1 -1
  63. package/dist/components/marker.d.cts +1 -1
  64. package/dist/components/marker.d.ts +1 -1
  65. package/dist/components/marker.js +2 -2
  66. package/dist/components/marker.js.map +1 -1
  67. package/dist/components/message-scroller.cjs +1 -1
  68. package/dist/components/message-scroller.js +2 -2
  69. package/dist/components/on-off-button.cjs +1 -1
  70. package/dist/components/on-off-button.cjs.map +1 -1
  71. package/dist/components/on-off-button.js +1 -1
  72. package/dist/components/on-off-button.js.map +1 -1
  73. package/dist/components/page-header.d.cts +1 -1
  74. package/dist/components/page-header.d.ts +1 -1
  75. package/dist/components/power-history-chart.cjs +3 -3
  76. package/dist/components/power-history-chart.js +4 -4
  77. package/dist/components/power-history.cjs +1 -1
  78. package/dist/components/power-history.js +1 -1
  79. package/dist/components/power-network.js +1 -1
  80. package/dist/components/radio.cjs +1 -1
  81. package/dist/components/radio.cjs.map +1 -1
  82. package/dist/components/radio.js +1 -1
  83. package/dist/components/radio.js.map +1 -1
  84. package/dist/components/schedule-list.cjs +1 -1
  85. package/dist/components/schedule-list.cjs.map +1 -1
  86. package/dist/components/schedule-list.js +1 -1
  87. package/dist/components/schedule-list.js.map +1 -1
  88. package/dist/components/scroll-area.cjs +2 -2
  89. package/dist/components/scroll-area.cjs.map +1 -1
  90. package/dist/components/scroll-area.js +2 -2
  91. package/dist/components/scroll-area.js.map +1 -1
  92. package/dist/components/segmented-control.cjs +1 -1
  93. package/dist/components/segmented-control.cjs.map +1 -1
  94. package/dist/components/segmented-control.js +1 -1
  95. package/dist/components/segmented-control.js.map +1 -1
  96. package/dist/components/select.cjs +6 -6
  97. package/dist/components/select.cjs.map +1 -1
  98. package/dist/components/select.js +6 -6
  99. package/dist/components/select.js.map +1 -1
  100. package/dist/components/series-legend.cjs +2 -2
  101. package/dist/components/series-legend.cjs.map +1 -1
  102. package/dist/components/series-legend.js +2 -2
  103. package/dist/components/series-legend.js.map +1 -1
  104. package/dist/components/slider.cjs +1 -1
  105. package/dist/components/slider.cjs.map +1 -1
  106. package/dist/components/slider.js +1 -1
  107. package/dist/components/slider.js.map +1 -1
  108. package/dist/components/smart-toggle-button.cjs +1 -1
  109. package/dist/components/smart-toggle-button.cjs.map +1 -1
  110. package/dist/components/smart-toggle-button.js +1 -1
  111. package/dist/components/smart-toggle-button.js.map +1 -1
  112. package/dist/components/solar-forecast-chart.js +1 -1
  113. package/dist/components/stepper.cjs +2 -2
  114. package/dist/components/stepper.cjs.map +1 -1
  115. package/dist/components/stepper.js +2 -2
  116. package/dist/components/stepper.js.map +1 -1
  117. package/dist/components/switch.cjs +1 -1
  118. package/dist/components/switch.cjs.map +1 -1
  119. package/dist/components/switch.js +1 -1
  120. package/dist/components/switch.js.map +1 -1
  121. package/dist/components/table.cjs +147 -0
  122. package/dist/components/table.cjs.map +1 -0
  123. package/dist/components/table.d.cts +75 -0
  124. package/dist/components/table.d.ts +75 -0
  125. package/dist/components/table.js +138 -0
  126. package/dist/components/table.js.map +1 -0
  127. package/dist/components/tabs.cjs +2 -2
  128. package/dist/components/tabs.cjs.map +1 -1
  129. package/dist/components/tabs.js +2 -2
  130. package/dist/components/tabs.js.map +1 -1
  131. package/dist/components/textarea.cjs +2 -2
  132. package/dist/components/textarea.cjs.map +1 -1
  133. package/dist/components/textarea.js +2 -2
  134. package/dist/components/textarea.js.map +1 -1
  135. package/dist/components/theme.cjs.map +1 -1
  136. package/dist/components/theme.d.cts +2 -2
  137. package/dist/components/theme.d.ts +2 -2
  138. package/dist/components/theme.js.map +1 -1
  139. package/dist/components/time-picker.cjs +1 -1
  140. package/dist/components/time-picker.cjs.map +1 -1
  141. package/dist/components/time-picker.js +1 -1
  142. package/dist/components/time-picker.js.map +1 -1
  143. package/dist/components/tooltip.cjs +1 -1
  144. package/dist/components/tooltip.cjs.map +1 -1
  145. package/dist/components/tooltip.js +1 -1
  146. package/dist/components/tooltip.js.map +1 -1
  147. package/dist/lib/accent-theme.cjs +199 -12
  148. package/dist/lib/accent-theme.cjs.map +1 -1
  149. package/dist/lib/accent-theme.d.cts +45 -2
  150. package/dist/lib/accent-theme.d.ts +45 -2
  151. package/dist/lib/accent-theme.js +195 -13
  152. package/dist/lib/accent-theme.js.map +1 -1
  153. package/dist/lib/container-chrome.cjs +1 -1
  154. package/dist/lib/container-chrome.cjs.map +1 -1
  155. package/dist/lib/container-chrome.js +1 -1
  156. package/dist/lib/container-chrome.js.map +1 -1
  157. package/dist/styles/index.css +9 -27
  158. package/dist/styles/spark-default.css +373 -19
  159. package/dist/styles/theme.css +2 -2
  160. package/package.json +17 -1
@@ -20,6 +20,8 @@ type SparkAccentScale = GeneratedColors & {
20
20
  accentOnSolid: string;
21
21
  };
22
22
  type Appearance = 'light' | 'dark';
23
+ /** The epilot brand: the no-attribute default `--spark-*` in spark-default.css. */
24
+ declare const DEFAULT_ACCENT = "#005EB4";
23
25
  /** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */
24
26
  declare const CURATED_ACCENTS: Record<string, string>;
25
27
  interface GenerateScaleOptions {
@@ -39,8 +41,49 @@ interface GenerateScaleOptions {
39
41
  }
40
42
  /** Which appearance a canvas belongs to, by OKLCH lightness. */
41
43
  declare function appearanceForBackground(background: string): Appearance;
42
- /** Generate the Radix-style accent scale for one appearance from a single accent color. */
44
+ /**
45
+ * Generate the Radix-style accent scale for one appearance from a single accent color.
46
+ *
47
+ * Solid-fill inks come from `readableContrast`, which clears WCAG but can only reach an
48
+ * unreadable APCA on a light, saturated step 9 (a near-black tint at Lc 33-44). Where it
49
+ * does, the fill is darkened until plain white clears 4.5:1 and the scale is re-seeded
50
+ * from that fill — see `darkenForWhiteInk`.
51
+ */
43
52
  declare function generateAccentScale(accent: string, appearance: Appearance, opts?: GenerateScaleOptions): SparkAccentScale;
53
+ /**
54
+ * Step 11 is the ink of every non-solid style, read on the a3 fill and the a4 hover. Both
55
+ * are translucent, so on a soft card (neutral-a3 ≈ neutral-3 over the canvas) they
56
+ * composite darker than on the canvas and Radix's step 11 drops under 4.5:1 (red 3.72:1).
57
+ * Move step 11 away from the fill, holding chroma and hue, until a11 on a4 clears 4.5:1
58
+ * over every neutral knob's step 3, and re-solve a11 from it. Unchanged when it clears.
59
+ */
60
+ declare function withReadableTintInk<T extends GeneratedColors>(scale: T): T;
61
+ declare const RED_A4: Record<Appearance, string>;
62
+ /**
63
+ * The hover tints a static `--neutral-placeholder` must clear (field.tsx): gray
64
+ * (`neutralA4`), invalid (`--red-a4`) and brand (`--spark-a4`) for the default accent and
65
+ * every curated one. A runtime brand outside these is not covered.
66
+ */
67
+ declare function staticFieldHoverTints(appearance: Appearance, neutralA4: string): string[];
68
+ /**
69
+ * The lightest color between neutral a10 and a11 that reaches `PLACEHOLDER_CONTRAST` on a
70
+ * `card` over `page`, bare and under each of `tints`, clamped to a11, as an sRGB alpha step
71
+ * over `page` (opaque where the alpha solve cannot reproduce it). The card is the deepest
72
+ * surface a field sits on, so clearing it clears the page too.
73
+ */
74
+ declare function solvePlaceholder({
75
+ a10,
76
+ a11,
77
+ page,
78
+ card,
79
+ tints
80
+ }: {
81
+ a10: string;
82
+ a11: string;
83
+ page: string;
84
+ card: string;
85
+ tints: string[];
86
+ }): string;
44
87
  /** sRGB `--spark-*` declarations for a generated accent scale. */
45
88
  declare function accentSparkVars(scale: SparkAccentScale): string[];
46
89
  /** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */
@@ -125,5 +168,5 @@ declare function neutralSparkVars(scale: SparkAccentScale): string[];
125
168
  /** Wide-gamut (P3) twin of `neutralSparkVars`. */
126
169
  declare function neutralSparkVarsP3(scale: SparkAccentScale): string[];
127
170
  //#endregion
128
- export { Appearance, CURATED_ACCENTS, GenerateScaleOptions, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral };
171
+ export { Appearance, CURATED_ACCENTS, DEFAULT_ACCENT, GenerateScaleOptions, GeneratedColors, RED_A4, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral, solvePlaceholder, staticFieldHoverTints, withReadableTintInk };
129
172
  //# sourceMappingURL=accent-theme.d.cts.map
@@ -20,6 +20,8 @@ type SparkAccentScale = GeneratedColors & {
20
20
  accentOnSolid: string;
21
21
  };
22
22
  type Appearance = 'light' | 'dark';
23
+ /** The epilot brand: the no-attribute default `--spark-*` in spark-default.css. */
24
+ declare const DEFAULT_ACCENT = "#005EB4";
23
25
  /** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */
24
26
  declare const CURATED_ACCENTS: Record<string, string>;
25
27
  interface GenerateScaleOptions {
@@ -39,8 +41,49 @@ interface GenerateScaleOptions {
39
41
  }
40
42
  /** Which appearance a canvas belongs to, by OKLCH lightness. */
41
43
  declare function appearanceForBackground(background: string): Appearance;
42
- /** Generate the Radix-style accent scale for one appearance from a single accent color. */
44
+ /**
45
+ * Generate the Radix-style accent scale for one appearance from a single accent color.
46
+ *
47
+ * Solid-fill inks come from `readableContrast`, which clears WCAG but can only reach an
48
+ * unreadable APCA on a light, saturated step 9 (a near-black tint at Lc 33-44). Where it
49
+ * does, the fill is darkened until plain white clears 4.5:1 and the scale is re-seeded
50
+ * from that fill — see `darkenForWhiteInk`.
51
+ */
43
52
  declare function generateAccentScale(accent: string, appearance: Appearance, opts?: GenerateScaleOptions): SparkAccentScale;
53
+ /**
54
+ * Step 11 is the ink of every non-solid style, read on the a3 fill and the a4 hover. Both
55
+ * are translucent, so on a soft card (neutral-a3 ≈ neutral-3 over the canvas) they
56
+ * composite darker than on the canvas and Radix's step 11 drops under 4.5:1 (red 3.72:1).
57
+ * Move step 11 away from the fill, holding chroma and hue, until a11 on a4 clears 4.5:1
58
+ * over every neutral knob's step 3, and re-solve a11 from it. Unchanged when it clears.
59
+ */
60
+ declare function withReadableTintInk<T extends GeneratedColors>(scale: T): T;
61
+ declare const RED_A4: Record<Appearance, string>;
62
+ /**
63
+ * The hover tints a static `--neutral-placeholder` must clear (field.tsx): gray
64
+ * (`neutralA4`), invalid (`--red-a4`) and brand (`--spark-a4`) for the default accent and
65
+ * every curated one. A runtime brand outside these is not covered.
66
+ */
67
+ declare function staticFieldHoverTints(appearance: Appearance, neutralA4: string): string[];
68
+ /**
69
+ * The lightest color between neutral a10 and a11 that reaches `PLACEHOLDER_CONTRAST` on a
70
+ * `card` over `page`, bare and under each of `tints`, clamped to a11, as an sRGB alpha step
71
+ * over `page` (opaque where the alpha solve cannot reproduce it). The card is the deepest
72
+ * surface a field sits on, so clearing it clears the page too.
73
+ */
74
+ declare function solvePlaceholder({
75
+ a10,
76
+ a11,
77
+ page,
78
+ card,
79
+ tints
80
+ }: {
81
+ a10: string;
82
+ a11: string;
83
+ page: string;
84
+ card: string;
85
+ tints: string[];
86
+ }): string;
44
87
  /** sRGB `--spark-*` declarations for a generated accent scale. */
45
88
  declare function accentSparkVars(scale: SparkAccentScale): string[];
46
89
  /** Wide-gamut (P3) `--spark-*` declarations — for a `color-gamut: p3` `@supports` block. */
@@ -125,5 +168,5 @@ declare function neutralSparkVars(scale: SparkAccentScale): string[];
125
168
  /** Wide-gamut (P3) twin of `neutralSparkVars`. */
126
169
  declare function neutralSparkVarsP3(scale: SparkAccentScale): string[];
127
170
  //#endregion
128
- export { Appearance, CURATED_ACCENTS, GenerateScaleOptions, GeneratedColors, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral };
171
+ export { Appearance, CURATED_ACCENTS, DEFAULT_ACCENT, GenerateScaleOptions, GeneratedColors, RED_A4, SparkAccentScale, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral, solvePlaceholder, staticFieldHoverTints, withReadableTintInk };
129
172
  //# sourceMappingURL=accent-theme.d.ts.map
@@ -4,6 +4,8 @@ import { isSparkNeutralFamily } from "./theme-types.js";
4
4
  import * as RadixColors from "@radix-ui/colors";
5
5
  import Color from "colorjs.io";
6
6
  //#region src/lib/accent-theme.ts
7
+ /** The epilot brand: the no-attribute default `--spark-*` in spark-default.css. */
8
+ const DEFAULT_ACCENT = "#005EB4";
7
9
  /** Curated name → source color. `blue` = Radix blue; the no-attribute default `--spark-*` stays the epilot brand. */
8
10
  const CURATED_ACCENTS = {
9
11
  blue: RadixColors.blue.blue9,
@@ -79,9 +81,9 @@ const ALPHA_CAP = .5;
79
81
  * per-channel in P3, and verifying that against an sRGB composite rejects perfectly good
80
82
  * solves (they were never trying to reproduce the sRGB composite in the first place).
81
83
  *
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,
84
- * so the default output — and portal parity — is untouched either way.
84
+ * So verify the solve and fall back to the opaque value where either check fails. On
85
+ * white/black every step solves exactly at low alpha, so the default output — and portal
86
+ * parity — is untouched either way.
85
87
  */
86
88
  function opaqueWhereUnsolvable(background, solid, alpha, capAlphaSteps = 0, space = "srgb") {
87
89
  let bg;
@@ -217,8 +219,8 @@ function accentScaleForCanvas(scale, background, appearance) {
217
219
  };
218
220
  }
219
221
  }
220
- /** Generate the Radix-style accent scale for one appearance from a single accent color. */
221
- function generateAccentScale(accent, appearance, opts) {
222
+ /** Build the Radix-style scale for one appearance from a single accent color. */
223
+ function buildScale(accent, appearance, opts) {
222
224
  const background = opts?.background ?? DEFAULTS[appearance].background;
223
225
  const scale = generateRadixColors({
224
226
  appearance,
@@ -231,7 +233,7 @@ function generateAccentScale(accent, appearance, opts) {
231
233
  tinted: false
232
234
  };
233
235
  const capAlphaSteps = tinted ? CANVAS_TINT_WEIGHTS.length : 0;
234
- const solved = opts?.background ? {
236
+ return opts?.background ? {
235
237
  ...scale,
236
238
  accentScale,
237
239
  accentScaleWideGamut,
@@ -242,14 +244,126 @@ function generateAccentScale(accent, appearance, opts) {
242
244
  grayScaleAlpha: opaqueWhereUnsolvable(background, scale.grayScale, scale.grayScaleAlpha),
243
245
  grayScaleAlphaWideGamut: opaqueWhereUnsolvable(background, scale.grayScaleWideGamut, scale.grayScaleAlphaWideGamut, 0, "p3")
244
246
  } : scale;
247
+ }
248
+ /**
249
+ * Generate the Radix-style accent scale for one appearance from a single accent color.
250
+ *
251
+ * Solid-fill inks come from `readableContrast`, which clears WCAG but can only reach an
252
+ * unreadable APCA on a light, saturated step 9 (a near-black tint at Lc 33-44). Where it
253
+ * does, the fill is darkened until plain white clears 4.5:1 and the scale is re-seeded
254
+ * from that fill — see `darkenForWhiteInk`.
255
+ */
256
+ function generateAccentScale(accent, appearance, opts) {
257
+ const solved = withNativeInks(buildScale(accent, appearance, opts));
258
+ const fill = solved.accentScale[8];
259
+ if (apcaOn(fill, solved.accentContrast) >= MIN_INK_APCA) return withReadableTintInk(solved);
260
+ const reseeded = buildScale(darkenForWhiteInk(fill), appearance, opts);
261
+ const reseededFill = reseeded.accentScale[8];
262
+ if (contrastOn(reseededFill, WHITE) < MIN_TEXT_CONTRAST) return withReadableTintInk(solved);
263
+ return withReadableTintInk({
264
+ ...reseeded,
265
+ accentContrast: WHITE,
266
+ accentOnSolid: WHITE
267
+ });
268
+ }
269
+ /**
270
+ * Step 11 is the ink of every non-solid style, read on the a3 fill and the a4 hover. Both
271
+ * are translucent, so on a soft card (neutral-a3 ≈ neutral-3 over the canvas) they
272
+ * composite darker than on the canvas and Radix's step 11 drops under 4.5:1 (red 3.72:1).
273
+ * Move step 11 away from the fill, holding chroma and hue, until a11 on a4 clears 4.5:1
274
+ * over every neutral knob's step 3, and re-solve a11 from it. Unchanged when it clears.
275
+ */
276
+ function withReadableTintInk(scale) {
277
+ const canvas = scale.background;
278
+ const dark = appearanceForBackground(canvas) === "dark";
279
+ const hoverFills = [scale.grayScale[2], ...Object.keys(NEUTRAL_BASE).map((family) => {
280
+ return RadixColors[dark ? `${family}Dark` : family][`${family}3`];
281
+ })].map((page) => composite(scale.accentScaleAlpha[3], page));
282
+ const inkOn = (step11) => {
283
+ const ink = getAlphaColorSrgb(step11, canvas);
284
+ return Math.min(...hoverFills.map((fill) => contrastOn(fill, composite(ink, fill))));
285
+ };
286
+ const original = scale.accentScale[10];
287
+ if (inkOn(original) >= MIN_TEXT_CONTRAST) return scale;
288
+ const [lightness, chroma, hue] = new Color(original).to("oklch").coords;
289
+ const [, wideChroma, wideHue] = new Color(scale.accentScaleWideGamut[10]).to("oklch").coords;
290
+ const toward = contrastOn(canvas, "#000") > contrastOn(canvas, "#fff") ? 0 : 1;
291
+ const srgbAt = (l) => new Color("oklch", [
292
+ l,
293
+ chroma ?? 0,
294
+ hue ?? 0
295
+ ]).toGamut({ space: "srgb" }).to("srgb").toString({ format: "hex" });
296
+ let fails = lightness ?? .5;
297
+ let passes = toward;
298
+ for (let i = 0; i < 16; i++) {
299
+ const mid = (fails + passes) / 2;
300
+ if (inkOn(srgbAt(mid)) >= MIN_TEXT_CONTRAST) passes = mid;
301
+ else fails = mid;
302
+ }
303
+ const step11 = srgbAt(passes);
304
+ const wideStep11 = toOklchWideGamutString(new Color("oklch", [
305
+ passes,
306
+ wideChroma ?? 0,
307
+ wideHue ?? 0
308
+ ]).toGamut({ space: "p3" }));
309
+ const withStep = (steps, value) => steps.map((v, i) => i === 10 ? value : v);
310
+ return {
311
+ ...scale,
312
+ accentScale: withStep(scale.accentScale, step11),
313
+ accentScaleWideGamut: withStep(scale.accentScaleWideGamut, wideStep11),
314
+ accentScaleAlpha: withStep(scale.accentScaleAlpha, getAlphaColorSrgb(step11, canvas)),
315
+ accentScaleAlphaWideGamut: withStep(scale.accentScaleAlphaWideGamut, getAlphaColorP3(wideStep11, canvas))
316
+ };
317
+ }
318
+ const composite = (top, bottom) => {
319
+ const fg = new Color(top).to("srgb");
320
+ const bg = new Color(bottom).to("srgb").coords;
321
+ const a = fg.alpha;
322
+ const channel = (k) => (Math.round(bg[k] * 255 * (1 - a)) + Math.round(fg.coords[k] * 255 * a)) / 255;
323
+ return new Color("srgb", [
324
+ channel(0),
325
+ channel(1),
326
+ channel(2)
327
+ ]).toString({ format: "hex" });
328
+ };
329
+ function withNativeInks(scale) {
330
+ const fill = scale.accentScale[8];
245
331
  return {
246
- ...solved,
247
- accentContrast: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_TEXT_CONTRAST),
248
- accentOnSolid: readableContrast(solved.accentScale[8], solved.accentContrast, MIN_GRAPHIC_CONTRAST)
332
+ ...scale,
333
+ accentContrast: readableContrast(fill, scale.accentContrast, MIN_TEXT_CONTRAST),
334
+ accentOnSolid: readableContrast(fill, scale.accentContrast, MIN_GRAPHIC_CONTRAST)
249
335
  };
250
336
  }
337
+ /**
338
+ * Darkest the fill needs to be for white text to clear 4.5:1, holding chroma and hue.
339
+ * Returns the fill unchanged when white already clears — the generator picks its ink
340
+ * from `Math.abs(white.contrastAPCA(fill))`, whose arguments are transposed, so it can
341
+ * reach for a dark tint on a fill white already handles.
342
+ */
343
+ function darkenForWhiteInk(fillHex) {
344
+ const fill = new Color(fillHex);
345
+ if (contrastOn(fillHex, WHITE) >= MIN_TEXT_CONTRAST) return fillHex;
346
+ const [lightness, chroma, hue] = fill.to("oklch").coords;
347
+ const darkened = (l) => new Color("oklch", [
348
+ l,
349
+ chroma ?? 0,
350
+ hue ?? 0
351
+ ]).toGamut({ space: "srgb" }).to("srgb").toString({ format: "hex" });
352
+ let fails = lightness ?? .5;
353
+ let passes = 0;
354
+ for (let i = 0; i < 16; i++) {
355
+ const mid = (fails + passes) / 2;
356
+ if (contrastOn(darkened(mid), WHITE) >= MIN_TEXT_CONTRAST) passes = mid;
357
+ else fails = mid;
358
+ }
359
+ return darkened(passes);
360
+ }
361
+ const WHITE = "#fff";
251
362
  const MIN_TEXT_CONTRAST = 4.5;
252
363
  const MIN_GRAPHIC_CONTRAST = 3;
364
+ const MIN_INK_APCA = 45;
365
+ const contrastOn = (bgHex, fgHex) => new Color(fgHex).contrast(new Color(bgHex), "WCAG21");
366
+ const apcaOn = (bgHex, fgHex) => Math.abs(new Color(bgHex).contrastAPCA(new Color(fgHex)));
253
367
  function readableContrast(step9Hex, native, min) {
254
368
  const step9 = new Color(step9Hex);
255
369
  const [, chroma, hue] = step9.to("oklch").coords;
@@ -281,6 +395,63 @@ function readableContrast(step9Hex, native, min) {
281
395
  const allRatios = all.map(ratioOf);
282
396
  return all[allRatios.indexOf(Math.max(...allRatios))];
283
397
  }
398
+ const RED_A4 = {
399
+ light: "#ff25002a",
400
+ dark: "#ff00005a"
401
+ };
402
+ /**
403
+ * The hover tints a static `--neutral-placeholder` must clear (field.tsx): gray
404
+ * (`neutralA4`), invalid (`--red-a4`) and brand (`--spark-a4`) for the default accent and
405
+ * every curated one. A runtime brand outside these is not covered.
406
+ */
407
+ function staticFieldHoverTints(appearance, neutralA4) {
408
+ return [
409
+ neutralA4,
410
+ RED_A4[appearance],
411
+ ...[DEFAULT_ACCENT, ...Object.values(CURATED_ACCENTS)].map((accent) => generateAccentScale(accent, appearance).accentScaleAlpha[3])
412
+ ];
413
+ }
414
+ const PLACEHOLDER_CONTRAST = 4.55;
415
+ function compositeOver(background, color) {
416
+ const ink = new Color(color).to("srgb");
417
+ const alpha = ink.alpha;
418
+ const bg = background.to("srgb").coords;
419
+ return new Color("srgb", [
420
+ 0,
421
+ 1,
422
+ 2
423
+ ].map((k) => bg[k] * (1 - alpha) + ink.coords[k] * alpha));
424
+ }
425
+ /**
426
+ * The lightest color between neutral a10 and a11 that reaches `PLACEHOLDER_CONTRAST` on a
427
+ * `card` over `page`, bare and under each of `tints`, clamped to a11, as an sRGB alpha step
428
+ * over `page` (opaque where the alpha solve cannot reproduce it). The card is the deepest
429
+ * surface a field sits on, so clearing it clears the page too.
430
+ */
431
+ function solvePlaceholder({ a10, a11, page, card, tints }) {
432
+ const bg = new Color(page);
433
+ const onCard = compositeOver(bg, card);
434
+ const fills = [onCard, ...tints.map((tint) => compositeOver(onCard, tint))];
435
+ const from = compositeOver(bg, a10);
436
+ const to = compositeOver(bg, a11);
437
+ const at = (t) => from.mix(to, t, { space: "oklch" }).to("srgb");
438
+ const inkAt = (t) => {
439
+ const solid = at(t).toString({ format: "hex" });
440
+ return opaqueWhereUnsolvable(page, [solid], [getAlphaColorSrgb(solid, page)])[0];
441
+ };
442
+ const passes = (t) => fills.every((fill) => compositeOver(fill, inkAt(t)).contrast(fill, "WCAG21") >= PLACEHOLDER_CONTRAST);
443
+ let found = 1;
444
+ if (passes(0)) found = 0;
445
+ else if (passes(1)) {
446
+ let fails = 0;
447
+ for (let i = 0; i < 16; i++) {
448
+ const mid = (fails + found) / 2;
449
+ if (passes(mid)) found = mid;
450
+ else fails = mid;
451
+ }
452
+ }
453
+ return inkAt(found);
454
+ }
284
455
  /** sRGB `--spark-*` declarations for a generated accent scale. */
285
456
  function accentSparkVars(scale) {
286
457
  return sparkVars(scale.accentScale, scale.accentScaleAlpha, scale.accentContrast, scale.accentOnSolid, scale.accentSurface, scale.background);
@@ -529,8 +700,19 @@ function sparkVars(steps, alpha, contrast, onSolid, surface, background) {
529
700
  */
530
701
  function neutralFamilyVars(family) {
531
702
  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)");
703
+ return [...scaleVars("neutral", Array.from({ length: 12 }, (_, i) => ref(`${i + 1}`)), Array.from({ length: 12 }, (_, i) => ref(`a${i + 1}`)), `var(--${family}-contrast)`, "#fff", "var(--neutral-2)"), `--neutral-placeholder: ${ref("placeholder")};`];
533
704
  }
705
+ const neutralPlaceholderVar = (scale) => `--neutral-placeholder: ${solvePlaceholder({
706
+ a10: scale.grayScaleAlpha[9],
707
+ a11: scale.grayScaleAlpha[10],
708
+ page: scale.background,
709
+ card: scale.grayScale[2],
710
+ tints: [
711
+ scale.grayScaleAlpha[3],
712
+ RED_A4[appearanceForBackground(scale.background)],
713
+ scale.accentScaleAlpha[3]
714
+ ]
715
+ })};`;
534
716
  /**
535
717
  * `--neutral-*` declarations from the gray scale the generator produces alongside the
536
718
  * accent — normally discarded, because `[data-neutral-color]` points `--neutral-*` at a
@@ -542,13 +724,13 @@ function neutralFamilyVars(family) {
542
724
  * than the generator's translucent `graySurface`.
543
725
  */
544
726
  function neutralSparkVars(scale) {
545
- 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]);
727
+ 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]), neutralPlaceholderVar(scale)];
546
728
  }
547
729
  /** Wide-gamut (P3) twin of `neutralSparkVars`. */
548
730
  function neutralSparkVarsP3(scale) {
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]);
731
+ 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]), neutralPlaceholderVar(scale)];
550
732
  }
551
733
  //#endregion
552
- export { CURATED_ACCENTS, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral };
734
+ export { CURATED_ACCENTS, DEFAULT_ACCENT, RED_A4, accentSparkVars, accentSparkVarsP3, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, neutralSparkVarsP3, resolveAutoNeutral, solvePlaceholder, staticFieldHoverTints, withReadableTintInk };
553
735
 
554
736
  //# sourceMappingURL=accent-theme.js.map