@tenphi/glaze 0.0.0-snapshot.4c063ef → 0.0.0-snapshot.709df23

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.
package/dist/index.cjs CHANGED
@@ -81,8 +81,8 @@ const OKLab_to_linear_sRGB_coefficients = [
81
81
  .73956515,
82
82
  -.45954404,
83
83
  .08285427,
84
- .12541073,
85
- -.14503204
84
+ .1254107,
85
+ .14503204
86
86
  ]],
87
87
  [[.13110757611180954, 1.813339709266608], [
88
88
  1.35733652,
@@ -254,10 +254,9 @@ const getCs = (L, a, b, cusp) => {
254
254
  ];
255
255
  };
256
256
  /**
257
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
258
- * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
257
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
259
258
  */
260
- function okhslToLinearSrgb(h, s, l) {
259
+ function okhslToOklab(h, s, l) {
261
260
  const L = toeInv(l);
262
261
  let a = 0;
263
262
  let b = 0;
@@ -284,11 +283,18 @@ function okhslToLinearSrgb(h, s, l) {
284
283
  a = c * a_;
285
284
  b = c * b_;
286
285
  }
287
- return OKLabToLinearSRGB([
286
+ return [
288
287
  L,
289
288
  a,
290
289
  b
291
- ]);
290
+ ];
291
+ }
292
+ /**
293
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
294
+ * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
295
+ */
296
+ function okhslToLinearSrgb(h, s, l) {
297
+ return OKLabToLinearSRGB(okhslToOklab(h, s, l));
292
298
  }
293
299
  /**
294
300
  * Compute relative luminance Y from linear sRGB channels.
@@ -327,40 +333,15 @@ function okhslToSrgb(h, s, l) {
327
333
  ];
328
334
  }
329
335
  /**
330
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
336
+ * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
337
+ * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
338
+ * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
331
339
  */
332
- function okhslToOklab(h, s, l) {
333
- const L = toeInv(l);
334
- let a = 0;
335
- let b = 0;
336
- const hNorm = constrainAngle(h) / 360;
337
- if (L !== 0 && L !== 1 && s !== 0) {
338
- const a_ = Math.cos(TAU * hNorm);
339
- const b_ = Math.sin(TAU * hNorm);
340
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
341
- const mid = .8;
342
- const midInv = 1.25;
343
- let t, k0, k1, k2;
344
- if (s < mid) {
345
- t = midInv * s;
346
- k0 = 0;
347
- k1 = mid * c0;
348
- k2 = 1 - k1 / cMid;
349
- } else {
350
- t = 5 * (s - .8);
351
- k0 = cMid;
352
- k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
353
- k2 = 1 - k1 / (cMax - cMid);
354
- }
355
- const c = k0 + t * k1 / (1 - k2 * t);
356
- a = c * a_;
357
- b = c * b_;
358
- }
359
- return [
360
- L,
361
- a,
362
- b
363
- ];
340
+ function gamutClampedLuminance(linearRgb) {
341
+ const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
342
+ const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
343
+ const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
344
+ return .2126 * r + .7152 * g + .0722 * b;
364
345
  }
365
346
  const linearSrgbToOklab = (rgb) => {
366
347
  return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
@@ -449,15 +430,16 @@ function fmt$1(value, decimals) {
449
430
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
450
431
  */
451
432
  function formatOkhsl(h, s, l) {
452
- return `okhsl(${fmt$1(h, 1)} ${fmt$1(s, 1)}% ${fmt$1(l, 1)}%)`;
433
+ return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
453
434
  }
454
435
  /**
455
- * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
436
+ * Format OKHSL values as a CSS `rgb(R G B)` string.
437
+ * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
456
438
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
457
439
  */
458
440
  function formatRgb(h, s, l) {
459
441
  const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
460
- return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
442
+ return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
461
443
  }
462
444
  /**
463
445
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
@@ -477,7 +459,7 @@ function formatHsl(h, s, l) {
477
459
  else if (max === g) hh = ((b - r) / delta + 2) * 60;
478
460
  else hh = ((r - g) / delta + 4) * 60;
479
461
  }
480
- return `hsl(${fmt$1(hh, 1)} ${fmt$1(ss * 100, 1)}% ${fmt$1(ll * 100, 1)}%)`;
462
+ return `hsl(${fmt$1(hh, 2)} ${fmt$1(ss * 100, 2)}% ${fmt$1(ll * 100, 2)}%)`;
481
463
  }
482
464
  /**
483
465
  * Format OKHSL values as a CSS `oklch(L C H)` string.
@@ -488,7 +470,7 @@ function formatOklch(h, s, l) {
488
470
  const C = Math.sqrt(a * a + b * b);
489
471
  let hh = Math.atan2(b, a) * (180 / Math.PI);
490
472
  hh = constrainAngle(hh);
491
- return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
473
+ return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
492
474
  }
493
475
 
494
476
  //#endregion
@@ -518,7 +500,7 @@ function cachedLuminance(h, s, l) {
518
500
  const key = `${h}|${s}|${lRounded}`;
519
501
  const cached = luminanceCache.get(key);
520
502
  if (cached !== void 0) return cached;
521
- const y = relativeLuminanceFromLinearRgb(okhslToLinearSrgb(h, s, lRounded));
503
+ const y = gamutClampedLuminance(okhslToLinearSrgb(h, s, lRounded));
522
504
  if (luminanceCache.size >= CACHE_SIZE) {
523
505
  const evict = cacheOrder.shift();
524
506
  luminanceCache.delete(evict);
@@ -638,17 +620,20 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
638
620
  function findLightnessForContrast(options) {
639
621
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
640
622
  const target = resolveMinContrast(contrastInput);
641
- const yBase = relativeLuminanceFromLinearRgb(baseLinearRgb);
623
+ const searchTarget = target * 1.007;
624
+ const yBase = gamutClampedLuminance(baseLinearRgb);
642
625
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
643
- if (crPref >= target) return {
626
+ if (crPref >= searchTarget) return {
644
627
  lightness: preferredLightness,
645
628
  contrast: crPref,
646
629
  met: true,
647
630
  branch: "preferred"
648
631
  };
649
632
  const [minL, maxL] = lightnessRange;
650
- const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, target, epsilon, maxIterations, preferredLightness) : null;
651
- const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, target, epsilon, maxIterations, preferredLightness) : null;
633
+ const darkerResult = preferredLightness > minL ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
634
+ const lighterResult = preferredLightness < maxL ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : null;
635
+ if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
636
+ if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
652
637
  const darkerPasses = darkerResult?.met ?? false;
653
638
  const lighterPasses = lighterResult?.met ?? false;
654
639
  if (darkerPasses && lighterPasses) {
@@ -687,6 +672,135 @@ function findLightnessForContrast(options) {
687
672
  candidates.sort((a, b) => b.contrast - a.contrast);
688
673
  return candidates[0];
689
674
  }
675
+ /**
676
+ * Binary-search one branch [lo, hi] for the nearest passing mix value
677
+ * to `preferred`.
678
+ */
679
+ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
680
+ const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
681
+ const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
682
+ if (crLo < target && crHi < target) {
683
+ if (crLo >= crHi) return {
684
+ lightness: lo,
685
+ contrast: crLo,
686
+ met: false
687
+ };
688
+ return {
689
+ lightness: hi,
690
+ contrast: crHi,
691
+ met: false
692
+ };
693
+ }
694
+ let low = lo;
695
+ let high = hi;
696
+ for (let i = 0; i < maxIter; i++) {
697
+ if (high - low < epsilon) break;
698
+ const mid = (low + high) / 2;
699
+ if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
700
+ else high = mid;
701
+ else if (mid < preferred) high = mid;
702
+ else low = mid;
703
+ }
704
+ const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
705
+ const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
706
+ const lowPasses = crLow >= target;
707
+ const highPasses = crHigh >= target;
708
+ if (lowPasses && highPasses) {
709
+ if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
710
+ lightness: low,
711
+ contrast: crLow,
712
+ met: true
713
+ };
714
+ return {
715
+ lightness: high,
716
+ contrast: crHigh,
717
+ met: true
718
+ };
719
+ }
720
+ if (lowPasses) return {
721
+ lightness: low,
722
+ contrast: crLow,
723
+ met: true
724
+ };
725
+ if (highPasses) return {
726
+ lightness: high,
727
+ contrast: crHigh,
728
+ met: true
729
+ };
730
+ return crLow >= crHigh ? {
731
+ lightness: low,
732
+ contrast: crLow,
733
+ met: false
734
+ } : {
735
+ lightness: high,
736
+ contrast: crHigh,
737
+ met: false
738
+ };
739
+ }
740
+ /**
741
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
742
+ * target against a base color, staying as close to `preferredValue` as possible.
743
+ */
744
+ function findValueForMixContrast(options) {
745
+ const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
746
+ const target = resolveMinContrast(contrastInput);
747
+ const searchTarget = target * 1.01;
748
+ const yBase = gamutClampedLuminance(baseLinearRgb);
749
+ const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
750
+ if (crPref >= searchTarget) return {
751
+ value: preferredValue,
752
+ contrast: crPref,
753
+ met: true
754
+ };
755
+ const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
756
+ const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
757
+ if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
758
+ if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
759
+ const darkerPasses = darkerResult?.met ?? false;
760
+ const lighterPasses = lighterResult?.met ?? false;
761
+ if (darkerPasses && lighterPasses) {
762
+ if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
763
+ value: darkerResult.lightness,
764
+ contrast: darkerResult.contrast,
765
+ met: true
766
+ };
767
+ return {
768
+ value: lighterResult.lightness,
769
+ contrast: lighterResult.contrast,
770
+ met: true
771
+ };
772
+ }
773
+ if (darkerPasses) return {
774
+ value: darkerResult.lightness,
775
+ contrast: darkerResult.contrast,
776
+ met: true
777
+ };
778
+ if (lighterPasses) return {
779
+ value: lighterResult.lightness,
780
+ contrast: lighterResult.contrast,
781
+ met: true
782
+ };
783
+ const candidates = [];
784
+ if (darkerResult) candidates.push({
785
+ ...darkerResult,
786
+ branch: "lower"
787
+ });
788
+ if (lighterResult) candidates.push({
789
+ ...lighterResult,
790
+ branch: "upper"
791
+ });
792
+ if (candidates.length === 0) return {
793
+ value: preferredValue,
794
+ contrast: crPref,
795
+ met: false
796
+ };
797
+ candidates.sort((a, b) => b.contrast - a.contrast);
798
+ return {
799
+ value: candidates[0].lightness,
800
+ contrast: candidates[0].contrast,
801
+ met: candidates[0].met
802
+ };
803
+ }
690
804
 
691
805
  //#endregion
692
806
  //#region src/glaze.ts
@@ -718,6 +832,9 @@ function pairHC(p) {
718
832
  function isShadowDef(def) {
719
833
  return def.type === "shadow";
720
834
  }
835
+ function isMixDef(def) {
836
+ return def.type === "mix";
837
+ }
721
838
  const DEFAULT_SHADOW_TUNING = {
722
839
  saturationFactor: .18,
723
840
  maxSaturation: .25,
@@ -785,10 +902,16 @@ function validateColorDefs(defs) {
785
902
  }
786
903
  continue;
787
904
  }
905
+ if (isMixDef(def)) {
906
+ if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
907
+ if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
908
+ if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
909
+ if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
910
+ continue;
911
+ }
788
912
  const regDef = def;
789
913
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
790
914
  if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
791
- if (isAbsoluteLightness(regDef.lightness) && regDef.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
792
915
  if (regDef.base && !names.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
793
916
  if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
794
917
  if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
@@ -804,6 +927,9 @@ function validateColorDefs(defs) {
804
927
  if (isShadowDef(def)) {
805
928
  dfs(def.bg);
806
929
  if (def.fg) dfs(def.fg);
930
+ } else if (isMixDef(def)) {
931
+ dfs(def.base);
932
+ dfs(def.target);
807
933
  } else {
808
934
  const regDef = def;
809
935
  if (regDef.base) dfs(regDef.base);
@@ -823,6 +949,9 @@ function topoSort(defs) {
823
949
  if (isShadowDef(def)) {
824
950
  visit(def.bg);
825
951
  if (def.fg) visit(def.fg);
952
+ } else if (isMixDef(def)) {
953
+ visit(def.base);
954
+ visit(def.target);
826
955
  } else {
827
956
  const regDef = def;
828
957
  if (regDef.base) visit(regDef.base);
@@ -847,6 +976,11 @@ function mapSaturationDark(s, mode) {
847
976
  if (mode === "static") return s;
848
977
  return s * (1 - globalConfig.darkDesaturation);
849
978
  }
979
+ function schemeLightnessRange(isDark, mode) {
980
+ if (mode === "static") return [0, 1];
981
+ const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
982
+ return [lo / 100, hi / 100];
983
+ }
850
984
  function clamp(v, min, max) {
851
985
  return Math.max(min, Math.min(max, v));
852
986
  }
@@ -907,20 +1041,22 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
907
1041
  if (isDark && mode === "auto") delta = -delta;
908
1042
  preferredL = clamp(baseL + delta, 0, 100);
909
1043
  } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
910
- else preferredL = clamp(parsed.value, 0, 100);
1044
+ else preferredL = mapLightnessLight(parsed.value, mode);
911
1045
  }
912
1046
  const rawContrast = def.contrast;
913
1047
  if (rawContrast !== void 0) {
914
1048
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
915
1049
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
916
1050
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1051
+ const lightnessRange = schemeLightnessRange(isDark, mode);
917
1052
  return {
918
1053
  l: findLightnessForContrast({
919
1054
  hue: effectiveHue,
920
1055
  saturation: effectiveSat,
921
- preferredLightness: preferredL / 100,
1056
+ preferredLightness: clamp(preferredL / 100, lightnessRange[0], lightnessRange[1]),
922
1057
  baseLinearRgb,
923
- contrast: minCr
1058
+ contrast: minCr,
1059
+ lightnessRange
924
1060
  }).lightness * 100,
925
1061
  satFactor
926
1062
  };
@@ -938,6 +1074,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
938
1074
  }
939
1075
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
940
1076
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1077
+ if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
941
1078
  const regDef = def;
942
1079
  const mode = regDef.mode ?? "auto";
943
1080
  const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
@@ -983,6 +1120,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
983
1120
  const tuning = resolveShadowTuning(def.tuning);
984
1121
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
985
1122
  }
1123
+ function variantToLinearRgb(v) {
1124
+ return okhslToLinearSrgb(v.h, v.s, v.l);
1125
+ }
1126
+ /**
1127
+ * Resolve hue for OKHSL mixing, handling achromatic colors.
1128
+ * When one color has no saturation, its hue is meaningless —
1129
+ * use the hue from the color that has saturation (matches CSS
1130
+ * color-mix "missing component" behavior).
1131
+ */
1132
+ function mixHue(base, target, t) {
1133
+ const SAT_EPSILON = 1e-6;
1134
+ const baseHasSat = base.s > SAT_EPSILON;
1135
+ const targetHasSat = target.s > SAT_EPSILON;
1136
+ if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
1137
+ if (targetHasSat) return target.h;
1138
+ return base.h;
1139
+ }
1140
+ function linearSrgbLerp(base, target, t) {
1141
+ return [
1142
+ base[0] + (target[0] - base[0]) * t,
1143
+ base[1] + (target[1] - base[1]) * t,
1144
+ base[2] + (target[2] - base[2]) * t
1145
+ ];
1146
+ }
1147
+ function linearRgbToVariant(rgb) {
1148
+ const [h, s, l] = srgbToOkhsl([
1149
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1150
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1151
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1152
+ ]);
1153
+ return {
1154
+ h,
1155
+ s,
1156
+ l,
1157
+ alpha: 1
1158
+ };
1159
+ }
1160
+ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1161
+ const baseResolved = ctx.resolved.get(def.base);
1162
+ const targetResolved = ctx.resolved.get(def.target);
1163
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1164
+ const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
1165
+ let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1166
+ const blend = def.blend ?? "opaque";
1167
+ const space = def.space ?? "okhsl";
1168
+ const baseLinear = variantToLinearRgb(baseVariant);
1169
+ const targetLinear = variantToLinearRgb(targetVariant);
1170
+ if (def.contrast !== void 0) {
1171
+ const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
1172
+ let luminanceAt;
1173
+ if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1174
+ else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1175
+ else luminanceAt = (v) => {
1176
+ return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1177
+ };
1178
+ t = findValueForMixContrast({
1179
+ preferredValue: t,
1180
+ baseLinearRgb: baseLinear,
1181
+ targetLinearRgb: targetLinear,
1182
+ contrast: minCr,
1183
+ luminanceAtValue: luminanceAt
1184
+ }).value;
1185
+ }
1186
+ if (blend === "transparent") return {
1187
+ h: targetVariant.h,
1188
+ s: targetVariant.s,
1189
+ l: targetVariant.l,
1190
+ alpha: clamp(t, 0, 1)
1191
+ };
1192
+ if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1193
+ return {
1194
+ h: mixHue(baseVariant, targetVariant, t),
1195
+ s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
1196
+ l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
1197
+ alpha: 1
1198
+ };
1199
+ }
986
1200
  function resolveAllColors(hue, saturation, defs) {
987
1201
  validateColorDefs(defs);
988
1202
  const order = topoSort(defs);
@@ -993,7 +1207,8 @@ function resolveAllColors(hue, saturation, defs) {
993
1207
  resolved: /* @__PURE__ */ new Map()
994
1208
  };
995
1209
  function defMode(def) {
996
- return isShadowDef(def) ? void 0 : def.mode ?? "auto";
1210
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1211
+ return def.mode ?? "auto";
997
1212
  }
998
1213
  const lightMap = /* @__PURE__ */ new Map();
999
1214
  for (const name of order) {
@@ -1206,26 +1421,40 @@ function createTheme(hue, saturation, initialColors) {
1206
1421
  }
1207
1422
  };
1208
1423
  }
1209
- function resolvePrefix(options, themeName) {
1210
- if (options?.prefix === true) return `${themeName}-`;
1211
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1424
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1425
+ const prefix = options?.prefix ?? defaultPrefix;
1426
+ if (prefix === true) return `${themeName}-`;
1427
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1212
1428
  return "";
1213
1429
  }
1430
+ function validatePrimaryTheme(primary, themes) {
1431
+ if (primary !== void 0 && !(primary in themes)) {
1432
+ const available = Object.keys(themes).join(", ");
1433
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1434
+ }
1435
+ }
1214
1436
  function createPalette(themes) {
1215
1437
  return {
1216
1438
  tokens(options) {
1439
+ validatePrimaryTheme(options?.primary, themes);
1217
1440
  const modes = resolveModes(options?.modes);
1218
1441
  const allTokens = {};
1219
1442
  for (const [themeName, theme] of Object.entries(themes)) {
1220
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1443
+ const resolved = theme.resolve();
1444
+ const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1221
1445
  for (const variant of Object.keys(tokens)) {
1222
1446
  if (!allTokens[variant]) allTokens[variant] = {};
1223
1447
  Object.assign(allTokens[variant], tokens[variant]);
1224
1448
  }
1449
+ if (themeName === options?.primary) {
1450
+ const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1451
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1452
+ }
1225
1453
  }
1226
1454
  return allTokens;
1227
1455
  },
1228
1456
  tasty(options) {
1457
+ validatePrimaryTheme(options?.primary, themes);
1229
1458
  const states = {
1230
1459
  dark: options?.states?.dark ?? globalConfig.states.dark,
1231
1460
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1233,8 +1462,13 @@ function createPalette(themes) {
1233
1462
  const modes = resolveModes(options?.modes);
1234
1463
  const allTokens = {};
1235
1464
  for (const [themeName, theme] of Object.entries(themes)) {
1236
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1465
+ const resolved = theme.resolve();
1466
+ const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1237
1467
  Object.assign(allTokens, tokens);
1468
+ if (themeName === options?.primary) {
1469
+ const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1470
+ Object.assign(allTokens, unprefixed);
1471
+ }
1238
1472
  }
1239
1473
  return allTokens;
1240
1474
  },
@@ -1245,6 +1479,7 @@ function createPalette(themes) {
1245
1479
  return result;
1246
1480
  },
1247
1481
  css(options) {
1482
+ validatePrimaryTheme(options?.primary, themes);
1248
1483
  const suffix = options?.suffix ?? "-color";
1249
1484
  const format = options?.format ?? "rgb";
1250
1485
  const allLines = {
@@ -1254,13 +1489,23 @@ function createPalette(themes) {
1254
1489
  darkContrast: []
1255
1490
  };
1256
1491
  for (const [themeName, theme] of Object.entries(themes)) {
1257
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1492
+ const resolved = theme.resolve();
1493
+ const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1258
1494
  for (const key of [
1259
1495
  "light",
1260
1496
  "dark",
1261
1497
  "lightContrast",
1262
1498
  "darkContrast"
1263
1499
  ]) if (css[key]) allLines[key].push(css[key]);
1500
+ if (themeName === options?.primary) {
1501
+ const unprefixed = buildCssMap(resolved, "", suffix, format);
1502
+ for (const key of [
1503
+ "light",
1504
+ "dark",
1505
+ "lightContrast",
1506
+ "darkContrast"
1507
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1508
+ }
1264
1509
  }
1265
1510
  return {
1266
1511
  light: allLines.light.join("\n"),
@@ -1433,10 +1678,12 @@ glaze.resetConfig = function resetConfig() {
1433
1678
  //#endregion
1434
1679
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
1435
1680
  exports.findLightnessForContrast = findLightnessForContrast;
1681
+ exports.findValueForMixContrast = findValueForMixContrast;
1436
1682
  exports.formatHsl = formatHsl;
1437
1683
  exports.formatOkhsl = formatOkhsl;
1438
1684
  exports.formatOklch = formatOklch;
1439
1685
  exports.formatRgb = formatRgb;
1686
+ exports.gamutClampedLuminance = gamutClampedLuminance;
1440
1687
  exports.glaze = glaze;
1441
1688
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
1442
1689
  exports.okhslToOklab = okhslToOklab;