@tenphi/glaze 0.0.0-snapshot.7c1fc7d → 0.0.0-snapshot.7e2a1da

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);
@@ -452,12 +433,13 @@ function formatOkhsl(h, s, l) {
452
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.
@@ -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
@@ -513,17 +495,6 @@ function resolveMinContrast(value) {
513
495
  const CACHE_SIZE = 512;
514
496
  const luminanceCache = /* @__PURE__ */ new Map();
515
497
  const cacheOrder = [];
516
- /**
517
- * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
518
- * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
519
- * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
520
- */
521
- function gamutClampedLuminance(linearRgb) {
522
- const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
523
- const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
524
- const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
525
- return .2126 * r + .7152 * g + .0722 * b;
526
- }
527
498
  function cachedLuminance(h, s, l) {
528
499
  const lRounded = Math.round(l * 1e4) / 1e4;
529
500
  const key = `${h}|${s}|${lRounded}`;
@@ -649,7 +620,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
649
620
  function findLightnessForContrast(options) {
650
621
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
651
622
  const target = resolveMinContrast(contrastInput);
652
- const searchTarget = target + .01;
623
+ const searchTarget = target * 1.007;
653
624
  const yBase = gamutClampedLuminance(baseLinearRgb);
654
625
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
655
626
  if (crPref >= searchTarget) return {
@@ -701,6 +672,135 @@ function findLightnessForContrast(options) {
701
672
  candidates.sort((a, b) => b.contrast - a.contrast);
702
673
  return candidates[0];
703
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
+ }
704
804
 
705
805
  //#endregion
706
806
  //#region src/glaze.ts
@@ -714,6 +814,7 @@ let globalConfig = {
714
814
  lightLightness: [10, 100],
715
815
  darkLightness: [15, 95],
716
816
  darkDesaturation: .1,
817
+ darkCurve: .5,
717
818
  states: {
718
819
  dark: "@dark",
719
820
  highContrast: "@high-contrast"
@@ -732,6 +833,9 @@ function pairHC(p) {
732
833
  function isShadowDef(def) {
733
834
  return def.type === "shadow";
734
835
  }
836
+ function isMixDef(def) {
837
+ return def.type === "mix";
838
+ }
735
839
  const DEFAULT_SHADOW_TUNING = {
736
840
  saturationFactor: .18,
737
841
  maxSaturation: .25,
@@ -799,6 +903,13 @@ function validateColorDefs(defs) {
799
903
  }
800
904
  continue;
801
905
  }
906
+ if (isMixDef(def)) {
907
+ if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
908
+ if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
909
+ if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
910
+ if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
911
+ continue;
912
+ }
802
913
  const regDef = def;
803
914
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
804
915
  if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
@@ -817,6 +928,9 @@ function validateColorDefs(defs) {
817
928
  if (isShadowDef(def)) {
818
929
  dfs(def.bg);
819
930
  if (def.fg) dfs(def.fg);
931
+ } else if (isMixDef(def)) {
932
+ dfs(def.base);
933
+ dfs(def.target);
820
934
  } else {
821
935
  const regDef = def;
822
936
  if (regDef.base) dfs(regDef.base);
@@ -836,6 +950,9 @@ function topoSort(defs) {
836
950
  if (isShadowDef(def)) {
837
951
  visit(def.bg);
838
952
  if (def.fg) visit(def.fg);
953
+ } else if (isMixDef(def)) {
954
+ visit(def.base);
955
+ visit(def.target);
839
956
  } else {
840
957
  const regDef = def;
841
958
  if (regDef.base) visit(regDef.base);
@@ -845,21 +962,33 @@ function topoSort(defs) {
845
962
  for (const name of Object.keys(defs)) visit(name);
846
963
  return result;
847
964
  }
848
- function mapLightnessLight(l, mode) {
849
- if (mode === "static") return l;
965
+ function mapLightnessLight(l, mode, isHighContrast) {
966
+ if (mode === "static" || isHighContrast) return l;
850
967
  const [lo, hi] = globalConfig.lightLightness;
851
968
  return l * (hi - lo) / 100 + lo;
852
969
  }
853
- function mapLightnessDark(l, mode) {
970
+ function mapLightnessDark(l, mode, isHighContrast) {
854
971
  if (mode === "static") return l;
855
- const [lo, hi] = globalConfig.darkLightness;
856
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
857
- return (100 - l) * (hi - lo) / 100 + lo;
972
+ if (isHighContrast) {
973
+ if (mode === "fixed") return l;
974
+ const t = (100 - l) / 100;
975
+ return 100 * Math.pow(t, globalConfig.darkCurve);
976
+ }
977
+ const [darkLo, darkHi] = globalConfig.darkLightness;
978
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
979
+ const [lightLo, lightHi] = globalConfig.lightLightness;
980
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
981
+ return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
858
982
  }
859
983
  function mapSaturationDark(s, mode) {
860
984
  if (mode === "static") return s;
861
985
  return s * (1 - globalConfig.darkDesaturation);
862
986
  }
987
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
988
+ if (mode === "static" || isHighContrast) return [0, 1];
989
+ const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
990
+ return [lo / 100, hi / 100];
991
+ }
863
992
  function clamp(v, min, max) {
864
993
  return Math.max(min, Math.min(max, v));
865
994
  }
@@ -919,21 +1048,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
919
1048
  let delta = parsed.value;
920
1049
  if (isDark && mode === "auto") delta = -delta;
921
1050
  preferredL = clamp(baseL + delta, 0, 100);
922
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
923
- else preferredL = clamp(parsed.value, 0, 100);
1051
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1052
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
924
1053
  }
925
1054
  const rawContrast = def.contrast;
926
1055
  if (rawContrast !== void 0) {
927
1056
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
928
1057
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
929
1058
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1059
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
930
1060
  return {
931
1061
  l: findLightnessForContrast({
932
1062
  hue: effectiveHue,
933
1063
  saturation: effectiveSat,
934
- preferredLightness: preferredL / 100,
1064
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
935
1065
  baseLinearRgb,
936
- contrast: minCr
1066
+ contrast: minCr,
1067
+ lightnessRange: [0, 1]
937
1068
  }).lightness * 100,
938
1069
  satFactor
939
1070
  };
@@ -951,6 +1082,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
951
1082
  }
952
1083
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
953
1084
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1085
+ if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
954
1086
  const regDef = def;
955
1087
  const mode = regDef.mode ?? "auto";
956
1088
  const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
@@ -969,13 +1101,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
969
1101
  let finalL;
970
1102
  let finalSat;
971
1103
  if (isDark && isRoot) {
972
- finalL = mapLightnessDark(lightL, mode);
1104
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
973
1105
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
974
1106
  } else if (isDark && !isRoot) {
975
1107
  finalL = lightL;
976
1108
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
977
1109
  } else if (isRoot) {
978
- finalL = mapLightnessLight(lightL, mode);
1110
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
979
1111
  finalSat = satFactor * ctx.saturation / 100;
980
1112
  } else {
981
1113
  finalL = lightL;
@@ -996,6 +1128,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
996
1128
  const tuning = resolveShadowTuning(def.tuning);
997
1129
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
998
1130
  }
1131
+ function variantToLinearRgb(v) {
1132
+ return okhslToLinearSrgb(v.h, v.s, v.l);
1133
+ }
1134
+ /**
1135
+ * Resolve hue for OKHSL mixing, handling achromatic colors.
1136
+ * When one color has no saturation, its hue is meaningless —
1137
+ * use the hue from the color that has saturation (matches CSS
1138
+ * color-mix "missing component" behavior).
1139
+ */
1140
+ function mixHue(base, target, t) {
1141
+ const SAT_EPSILON = 1e-6;
1142
+ const baseHasSat = base.s > SAT_EPSILON;
1143
+ const targetHasSat = target.s > SAT_EPSILON;
1144
+ if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
1145
+ if (targetHasSat) return target.h;
1146
+ return base.h;
1147
+ }
1148
+ function linearSrgbLerp(base, target, t) {
1149
+ return [
1150
+ base[0] + (target[0] - base[0]) * t,
1151
+ base[1] + (target[1] - base[1]) * t,
1152
+ base[2] + (target[2] - base[2]) * t
1153
+ ];
1154
+ }
1155
+ function linearRgbToVariant(rgb) {
1156
+ const [h, s, l] = srgbToOkhsl([
1157
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1158
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1159
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1160
+ ]);
1161
+ return {
1162
+ h,
1163
+ s,
1164
+ l,
1165
+ alpha: 1
1166
+ };
1167
+ }
1168
+ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1169
+ const baseResolved = ctx.resolved.get(def.base);
1170
+ const targetResolved = ctx.resolved.get(def.target);
1171
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1172
+ const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
1173
+ let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1174
+ const blend = def.blend ?? "opaque";
1175
+ const space = def.space ?? "okhsl";
1176
+ const baseLinear = variantToLinearRgb(baseVariant);
1177
+ const targetLinear = variantToLinearRgb(targetVariant);
1178
+ if (def.contrast !== void 0) {
1179
+ const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
1180
+ let luminanceAt;
1181
+ if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1182
+ else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1183
+ else luminanceAt = (v) => {
1184
+ return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1185
+ };
1186
+ t = findValueForMixContrast({
1187
+ preferredValue: t,
1188
+ baseLinearRgb: baseLinear,
1189
+ targetLinearRgb: targetLinear,
1190
+ contrast: minCr,
1191
+ luminanceAtValue: luminanceAt
1192
+ }).value;
1193
+ }
1194
+ if (blend === "transparent") return {
1195
+ h: targetVariant.h,
1196
+ s: targetVariant.s,
1197
+ l: targetVariant.l,
1198
+ alpha: clamp(t, 0, 1)
1199
+ };
1200
+ if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1201
+ return {
1202
+ h: mixHue(baseVariant, targetVariant, t),
1203
+ s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
1204
+ l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
1205
+ alpha: 1
1206
+ };
1207
+ }
999
1208
  function resolveAllColors(hue, saturation, defs) {
1000
1209
  validateColorDefs(defs);
1001
1210
  const order = topoSort(defs);
@@ -1006,7 +1215,8 @@ function resolveAllColors(hue, saturation, defs) {
1006
1215
  resolved: /* @__PURE__ */ new Map()
1007
1216
  };
1008
1217
  function defMode(def) {
1009
- return isShadowDef(def) ? void 0 : def.mode ?? "auto";
1218
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1219
+ return def.mode ?? "auto";
1010
1220
  }
1011
1221
  const lightMap = /* @__PURE__ */ new Map();
1012
1222
  for (const name of order) {
@@ -1219,26 +1429,40 @@ function createTheme(hue, saturation, initialColors) {
1219
1429
  }
1220
1430
  };
1221
1431
  }
1222
- function resolvePrefix(options, themeName) {
1223
- if (options?.prefix === true) return `${themeName}-`;
1224
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1432
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1433
+ const prefix = options?.prefix ?? defaultPrefix;
1434
+ if (prefix === true) return `${themeName}-`;
1435
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1225
1436
  return "";
1226
1437
  }
1438
+ function validatePrimaryTheme(primary, themes) {
1439
+ if (primary !== void 0 && !(primary in themes)) {
1440
+ const available = Object.keys(themes).join(", ");
1441
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1442
+ }
1443
+ }
1227
1444
  function createPalette(themes) {
1228
1445
  return {
1229
1446
  tokens(options) {
1447
+ validatePrimaryTheme(options?.primary, themes);
1230
1448
  const modes = resolveModes(options?.modes);
1231
1449
  const allTokens = {};
1232
1450
  for (const [themeName, theme] of Object.entries(themes)) {
1233
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1451
+ const resolved = theme.resolve();
1452
+ const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1234
1453
  for (const variant of Object.keys(tokens)) {
1235
1454
  if (!allTokens[variant]) allTokens[variant] = {};
1236
1455
  Object.assign(allTokens[variant], tokens[variant]);
1237
1456
  }
1457
+ if (themeName === options?.primary) {
1458
+ const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1459
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1460
+ }
1238
1461
  }
1239
1462
  return allTokens;
1240
1463
  },
1241
1464
  tasty(options) {
1465
+ validatePrimaryTheme(options?.primary, themes);
1242
1466
  const states = {
1243
1467
  dark: options?.states?.dark ?? globalConfig.states.dark,
1244
1468
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1246,8 +1470,13 @@ function createPalette(themes) {
1246
1470
  const modes = resolveModes(options?.modes);
1247
1471
  const allTokens = {};
1248
1472
  for (const [themeName, theme] of Object.entries(themes)) {
1249
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1473
+ const resolved = theme.resolve();
1474
+ const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1250
1475
  Object.assign(allTokens, tokens);
1476
+ if (themeName === options?.primary) {
1477
+ const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1478
+ Object.assign(allTokens, unprefixed);
1479
+ }
1251
1480
  }
1252
1481
  return allTokens;
1253
1482
  },
@@ -1258,6 +1487,7 @@ function createPalette(themes) {
1258
1487
  return result;
1259
1488
  },
1260
1489
  css(options) {
1490
+ validatePrimaryTheme(options?.primary, themes);
1261
1491
  const suffix = options?.suffix ?? "-color";
1262
1492
  const format = options?.format ?? "rgb";
1263
1493
  const allLines = {
@@ -1267,13 +1497,23 @@ function createPalette(themes) {
1267
1497
  darkContrast: []
1268
1498
  };
1269
1499
  for (const [themeName, theme] of Object.entries(themes)) {
1270
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1500
+ const resolved = theme.resolve();
1501
+ const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1271
1502
  for (const key of [
1272
1503
  "light",
1273
1504
  "dark",
1274
1505
  "lightContrast",
1275
1506
  "darkContrast"
1276
1507
  ]) if (css[key]) allLines[key].push(css[key]);
1508
+ if (themeName === options?.primary) {
1509
+ const unprefixed = buildCssMap(resolved, "", suffix, format);
1510
+ for (const key of [
1511
+ "light",
1512
+ "dark",
1513
+ "lightContrast",
1514
+ "darkContrast"
1515
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1516
+ }
1277
1517
  }
1278
1518
  return {
1279
1519
  light: allLines.light.join("\n"),
@@ -1333,6 +1573,7 @@ glaze.configure = function configure(config) {
1333
1573
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1334
1574
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1335
1575
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1576
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1336
1577
  states: {
1337
1578
  dark: config.states?.dark ?? globalConfig.states.dark,
1338
1579
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1432,6 +1673,7 @@ glaze.resetConfig = function resetConfig() {
1432
1673
  lightLightness: [10, 100],
1433
1674
  darkLightness: [15, 95],
1434
1675
  darkDesaturation: .1,
1676
+ darkCurve: .5,
1435
1677
  states: {
1436
1678
  dark: "@dark",
1437
1679
  highContrast: "@high-contrast"
@@ -1446,10 +1688,12 @@ glaze.resetConfig = function resetConfig() {
1446
1688
  //#endregion
1447
1689
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
1448
1690
  exports.findLightnessForContrast = findLightnessForContrast;
1691
+ exports.findValueForMixContrast = findValueForMixContrast;
1449
1692
  exports.formatHsl = formatHsl;
1450
1693
  exports.formatOkhsl = formatOkhsl;
1451
1694
  exports.formatOklch = formatOklch;
1452
1695
  exports.formatRgb = formatRgb;
1696
+ exports.gamutClampedLuminance = gamutClampedLuminance;
1453
1697
  exports.glaze = glaze;
1454
1698
  exports.okhslToLinearSrgb = okhslToLinearSrgb;
1455
1699
  exports.okhslToOklab = okhslToOklab;