@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.mjs CHANGED
@@ -79,8 +79,8 @@ const OKLab_to_linear_sRGB_coefficients = [
79
79
  .73956515,
80
80
  -.45954404,
81
81
  .08285427,
82
- .12541073,
83
- -.14503204
82
+ .1254107,
83
+ .14503204
84
84
  ]],
85
85
  [[.13110757611180954, 1.813339709266608], [
86
86
  1.35733652,
@@ -252,10 +252,9 @@ const getCs = (L, a, b, cusp) => {
252
252
  ];
253
253
  };
254
254
  /**
255
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
256
- * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
255
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
257
256
  */
258
- function okhslToLinearSrgb(h, s, l) {
257
+ function okhslToOklab(h, s, l) {
259
258
  const L = toeInv(l);
260
259
  let a = 0;
261
260
  let b = 0;
@@ -282,11 +281,18 @@ function okhslToLinearSrgb(h, s, l) {
282
281
  a = c * a_;
283
282
  b = c * b_;
284
283
  }
285
- return OKLabToLinearSRGB([
284
+ return [
286
285
  L,
287
286
  a,
288
287
  b
289
- ]);
288
+ ];
289
+ }
290
+ /**
291
+ * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to linear sRGB.
292
+ * Channels may exceed [0, 1] near gamut boundaries — caller must clamp if needed.
293
+ */
294
+ function okhslToLinearSrgb(h, s, l) {
295
+ return OKLabToLinearSRGB(okhslToOklab(h, s, l));
290
296
  }
291
297
  /**
292
298
  * Compute relative luminance Y from linear sRGB channels.
@@ -325,40 +331,15 @@ function okhslToSrgb(h, s, l) {
325
331
  ];
326
332
  }
327
333
  /**
328
- * Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLab [L, a, b].
334
+ * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
335
+ * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
336
+ * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
329
337
  */
330
- function okhslToOklab(h, s, l) {
331
- const L = toeInv(l);
332
- let a = 0;
333
- let b = 0;
334
- const hNorm = constrainAngle(h) / 360;
335
- if (L !== 0 && L !== 1 && s !== 0) {
336
- const a_ = Math.cos(TAU * hNorm);
337
- const b_ = Math.sin(TAU * hNorm);
338
- const [c0, cMid, cMax] = getCs(L, a_, b_, findCuspOKLCH(a_, b_));
339
- const mid = .8;
340
- const midInv = 1.25;
341
- let t, k0, k1, k2;
342
- if (s < mid) {
343
- t = midInv * s;
344
- k0 = 0;
345
- k1 = mid * c0;
346
- k2 = 1 - k1 / cMid;
347
- } else {
348
- t = 5 * (s - .8);
349
- k0 = cMid;
350
- k1 = .2 * cMid ** 2 * 1.25 ** 2 / c0;
351
- k2 = 1 - k1 / (cMax - cMid);
352
- }
353
- const c = k0 + t * k1 / (1 - k2 * t);
354
- a = c * a_;
355
- b = c * b_;
356
- }
357
- return [
358
- L,
359
- a,
360
- b
361
- ];
338
+ function gamutClampedLuminance(linearRgb) {
339
+ const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
340
+ const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
341
+ const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
342
+ return .2126 * r + .7152 * g + .0722 * b;
362
343
  }
363
344
  const linearSrgbToOklab = (rgb) => {
364
345
  return transform(cbrt3(transform(rgb, linear_sRGB_to_LMS_M)), LMS_to_OKLab_M);
@@ -450,12 +431,13 @@ function formatOkhsl(h, s, l) {
450
431
  return `okhsl(${fmt$1(h, 2)} ${fmt$1(s, 2)}% ${fmt$1(l, 2)}%)`;
451
432
  }
452
433
  /**
453
- * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
434
+ * Format OKHSL values as a CSS `rgb(R G B)` string.
435
+ * Uses 2 decimal places to avoid 8-bit quantization contrast loss.
454
436
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
455
437
  */
456
438
  function formatRgb(h, s, l) {
457
439
  const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
458
- return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
440
+ return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
459
441
  }
460
442
  /**
461
443
  * Format OKHSL values as a CSS `hsl(H S% L%)` string.
@@ -486,7 +468,7 @@ function formatOklch(h, s, l) {
486
468
  const C = Math.sqrt(a * a + b * b);
487
469
  let hh = Math.atan2(b, a) * (180 / Math.PI);
488
470
  hh = constrainAngle(hh);
489
- return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
471
+ return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
490
472
  }
491
473
 
492
474
  //#endregion
@@ -511,17 +493,6 @@ function resolveMinContrast(value) {
511
493
  const CACHE_SIZE = 512;
512
494
  const luminanceCache = /* @__PURE__ */ new Map();
513
495
  const cacheOrder = [];
514
- /**
515
- * Compute WCAG 2 relative luminance from linear sRGB, matching the browser
516
- * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
517
- * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
518
- */
519
- function gamutClampedLuminance(linearRgb) {
520
- const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
521
- const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
522
- const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
523
- return .2126 * r + .7152 * g + .0722 * b;
524
- }
525
496
  function cachedLuminance(h, s, l) {
526
497
  const lRounded = Math.round(l * 1e4) / 1e4;
527
498
  const key = `${h}|${s}|${lRounded}`;
@@ -647,7 +618,7 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
647
618
  function findLightnessForContrast(options) {
648
619
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
649
620
  const target = resolveMinContrast(contrastInput);
650
- const searchTarget = target + .01;
621
+ const searchTarget = target * 1.007;
651
622
  const yBase = gamutClampedLuminance(baseLinearRgb);
652
623
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
653
624
  if (crPref >= searchTarget) return {
@@ -699,6 +670,135 @@ function findLightnessForContrast(options) {
699
670
  candidates.sort((a, b) => b.contrast - a.contrast);
700
671
  return candidates[0];
701
672
  }
673
+ /**
674
+ * Binary-search one branch [lo, hi] for the nearest passing mix value
675
+ * to `preferred`.
676
+ */
677
+ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
678
+ const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
679
+ const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
680
+ if (crLo < target && crHi < target) {
681
+ if (crLo >= crHi) return {
682
+ lightness: lo,
683
+ contrast: crLo,
684
+ met: false
685
+ };
686
+ return {
687
+ lightness: hi,
688
+ contrast: crHi,
689
+ met: false
690
+ };
691
+ }
692
+ let low = lo;
693
+ let high = hi;
694
+ for (let i = 0; i < maxIter; i++) {
695
+ if (high - low < epsilon) break;
696
+ const mid = (low + high) / 2;
697
+ if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
698
+ else high = mid;
699
+ else if (mid < preferred) high = mid;
700
+ else low = mid;
701
+ }
702
+ const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
703
+ const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
704
+ const lowPasses = crLow >= target;
705
+ const highPasses = crHigh >= target;
706
+ if (lowPasses && highPasses) {
707
+ if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
708
+ lightness: low,
709
+ contrast: crLow,
710
+ met: true
711
+ };
712
+ return {
713
+ lightness: high,
714
+ contrast: crHigh,
715
+ met: true
716
+ };
717
+ }
718
+ if (lowPasses) return {
719
+ lightness: low,
720
+ contrast: crLow,
721
+ met: true
722
+ };
723
+ if (highPasses) return {
724
+ lightness: high,
725
+ contrast: crHigh,
726
+ met: true
727
+ };
728
+ return crLow >= crHigh ? {
729
+ lightness: low,
730
+ contrast: crLow,
731
+ met: false
732
+ } : {
733
+ lightness: high,
734
+ contrast: crHigh,
735
+ met: false
736
+ };
737
+ }
738
+ /**
739
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
740
+ * target against a base color, staying as close to `preferredValue` as possible.
741
+ */
742
+ function findValueForMixContrast(options) {
743
+ const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
744
+ const target = resolveMinContrast(contrastInput);
745
+ const searchTarget = target * 1.01;
746
+ const yBase = gamutClampedLuminance(baseLinearRgb);
747
+ const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
748
+ if (crPref >= searchTarget) return {
749
+ value: preferredValue,
750
+ contrast: crPref,
751
+ met: true
752
+ };
753
+ const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
754
+ const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
755
+ if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
756
+ if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
757
+ const darkerPasses = darkerResult?.met ?? false;
758
+ const lighterPasses = lighterResult?.met ?? false;
759
+ if (darkerPasses && lighterPasses) {
760
+ if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
761
+ value: darkerResult.lightness,
762
+ contrast: darkerResult.contrast,
763
+ met: true
764
+ };
765
+ return {
766
+ value: lighterResult.lightness,
767
+ contrast: lighterResult.contrast,
768
+ met: true
769
+ };
770
+ }
771
+ if (darkerPasses) return {
772
+ value: darkerResult.lightness,
773
+ contrast: darkerResult.contrast,
774
+ met: true
775
+ };
776
+ if (lighterPasses) return {
777
+ value: lighterResult.lightness,
778
+ contrast: lighterResult.contrast,
779
+ met: true
780
+ };
781
+ const candidates = [];
782
+ if (darkerResult) candidates.push({
783
+ ...darkerResult,
784
+ branch: "lower"
785
+ });
786
+ if (lighterResult) candidates.push({
787
+ ...lighterResult,
788
+ branch: "upper"
789
+ });
790
+ if (candidates.length === 0) return {
791
+ value: preferredValue,
792
+ contrast: crPref,
793
+ met: false
794
+ };
795
+ candidates.sort((a, b) => b.contrast - a.contrast);
796
+ return {
797
+ value: candidates[0].lightness,
798
+ contrast: candidates[0].contrast,
799
+ met: candidates[0].met
800
+ };
801
+ }
702
802
 
703
803
  //#endregion
704
804
  //#region src/glaze.ts
@@ -712,6 +812,7 @@ let globalConfig = {
712
812
  lightLightness: [10, 100],
713
813
  darkLightness: [15, 95],
714
814
  darkDesaturation: .1,
815
+ darkCurve: .5,
715
816
  states: {
716
817
  dark: "@dark",
717
818
  highContrast: "@high-contrast"
@@ -730,6 +831,9 @@ function pairHC(p) {
730
831
  function isShadowDef(def) {
731
832
  return def.type === "shadow";
732
833
  }
834
+ function isMixDef(def) {
835
+ return def.type === "mix";
836
+ }
733
837
  const DEFAULT_SHADOW_TUNING = {
734
838
  saturationFactor: .18,
735
839
  maxSaturation: .25,
@@ -797,6 +901,13 @@ function validateColorDefs(defs) {
797
901
  }
798
902
  continue;
799
903
  }
904
+ if (isMixDef(def)) {
905
+ if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
906
+ if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
907
+ if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
908
+ if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
909
+ continue;
910
+ }
800
911
  const regDef = def;
801
912
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
802
913
  if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
@@ -815,6 +926,9 @@ function validateColorDefs(defs) {
815
926
  if (isShadowDef(def)) {
816
927
  dfs(def.bg);
817
928
  if (def.fg) dfs(def.fg);
929
+ } else if (isMixDef(def)) {
930
+ dfs(def.base);
931
+ dfs(def.target);
818
932
  } else {
819
933
  const regDef = def;
820
934
  if (regDef.base) dfs(regDef.base);
@@ -834,6 +948,9 @@ function topoSort(defs) {
834
948
  if (isShadowDef(def)) {
835
949
  visit(def.bg);
836
950
  if (def.fg) visit(def.fg);
951
+ } else if (isMixDef(def)) {
952
+ visit(def.base);
953
+ visit(def.target);
837
954
  } else {
838
955
  const regDef = def;
839
956
  if (regDef.base) visit(regDef.base);
@@ -843,21 +960,33 @@ function topoSort(defs) {
843
960
  for (const name of Object.keys(defs)) visit(name);
844
961
  return result;
845
962
  }
846
- function mapLightnessLight(l, mode) {
847
- if (mode === "static") return l;
963
+ function mapLightnessLight(l, mode, isHighContrast) {
964
+ if (mode === "static" || isHighContrast) return l;
848
965
  const [lo, hi] = globalConfig.lightLightness;
849
966
  return l * (hi - lo) / 100 + lo;
850
967
  }
851
- function mapLightnessDark(l, mode) {
968
+ function mapLightnessDark(l, mode, isHighContrast) {
852
969
  if (mode === "static") return l;
853
- const [lo, hi] = globalConfig.darkLightness;
854
- if (mode === "fixed") return l * (hi - lo) / 100 + lo;
855
- return (100 - l) * (hi - lo) / 100 + lo;
970
+ if (isHighContrast) {
971
+ if (mode === "fixed") return l;
972
+ const t = (100 - l) / 100;
973
+ return 100 * Math.pow(t, globalConfig.darkCurve);
974
+ }
975
+ const [darkLo, darkHi] = globalConfig.darkLightness;
976
+ if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
977
+ const [lightLo, lightHi] = globalConfig.lightLightness;
978
+ const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
979
+ return darkLo + (darkHi - darkLo) * Math.pow(t, globalConfig.darkCurve);
856
980
  }
857
981
  function mapSaturationDark(s, mode) {
858
982
  if (mode === "static") return s;
859
983
  return s * (1 - globalConfig.darkDesaturation);
860
984
  }
985
+ function schemeLightnessRange(isDark, mode, isHighContrast) {
986
+ if (mode === "static" || isHighContrast) return [0, 1];
987
+ const [lo, hi] = isDark ? globalConfig.darkLightness : globalConfig.lightLightness;
988
+ return [lo / 100, hi / 100];
989
+ }
861
990
  function clamp(v, min, max) {
862
991
  return Math.max(min, Math.min(max, v));
863
992
  }
@@ -917,21 +1046,23 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
917
1046
  let delta = parsed.value;
918
1047
  if (isDark && mode === "auto") delta = -delta;
919
1048
  preferredL = clamp(baseL + delta, 0, 100);
920
- } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
921
- else preferredL = clamp(parsed.value, 0, 100);
1049
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode, isHighContrast);
1050
+ else preferredL = mapLightnessLight(parsed.value, mode, isHighContrast);
922
1051
  }
923
1052
  const rawContrast = def.contrast;
924
1053
  if (rawContrast !== void 0) {
925
1054
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
926
1055
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
927
1056
  const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
1057
+ const windowRange = schemeLightnessRange(isDark, mode, isHighContrast);
928
1058
  return {
929
1059
  l: findLightnessForContrast({
930
1060
  hue: effectiveHue,
931
1061
  saturation: effectiveSat,
932
- preferredLightness: preferredL / 100,
1062
+ preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
933
1063
  baseLinearRgb,
934
- contrast: minCr
1064
+ contrast: minCr,
1065
+ lightnessRange: [0, 1]
935
1066
  }).lightness * 100,
936
1067
  satFactor
937
1068
  };
@@ -949,6 +1080,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
949
1080
  }
950
1081
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
951
1082
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1083
+ if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
952
1084
  const regDef = def;
953
1085
  const mode = regDef.mode ?? "auto";
954
1086
  const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
@@ -967,13 +1099,13 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
967
1099
  let finalL;
968
1100
  let finalSat;
969
1101
  if (isDark && isRoot) {
970
- finalL = mapLightnessDark(lightL, mode);
1102
+ finalL = mapLightnessDark(lightL, mode, isHighContrast);
971
1103
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
972
1104
  } else if (isDark && !isRoot) {
973
1105
  finalL = lightL;
974
1106
  finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
975
1107
  } else if (isRoot) {
976
- finalL = mapLightnessLight(lightL, mode);
1108
+ finalL = mapLightnessLight(lightL, mode, isHighContrast);
977
1109
  finalSat = satFactor * ctx.saturation / 100;
978
1110
  } else {
979
1111
  finalL = lightL;
@@ -994,6 +1126,83 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
994
1126
  const tuning = resolveShadowTuning(def.tuning);
995
1127
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
996
1128
  }
1129
+ function variantToLinearRgb(v) {
1130
+ return okhslToLinearSrgb(v.h, v.s, v.l);
1131
+ }
1132
+ /**
1133
+ * Resolve hue for OKHSL mixing, handling achromatic colors.
1134
+ * When one color has no saturation, its hue is meaningless —
1135
+ * use the hue from the color that has saturation (matches CSS
1136
+ * color-mix "missing component" behavior).
1137
+ */
1138
+ function mixHue(base, target, t) {
1139
+ const SAT_EPSILON = 1e-6;
1140
+ const baseHasSat = base.s > SAT_EPSILON;
1141
+ const targetHasSat = target.s > SAT_EPSILON;
1142
+ if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
1143
+ if (targetHasSat) return target.h;
1144
+ return base.h;
1145
+ }
1146
+ function linearSrgbLerp(base, target, t) {
1147
+ return [
1148
+ base[0] + (target[0] - base[0]) * t,
1149
+ base[1] + (target[1] - base[1]) * t,
1150
+ base[2] + (target[2] - base[2]) * t
1151
+ ];
1152
+ }
1153
+ function linearRgbToVariant(rgb) {
1154
+ const [h, s, l] = srgbToOkhsl([
1155
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1156
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1157
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1158
+ ]);
1159
+ return {
1160
+ h,
1161
+ s,
1162
+ l,
1163
+ alpha: 1
1164
+ };
1165
+ }
1166
+ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1167
+ const baseResolved = ctx.resolved.get(def.base);
1168
+ const targetResolved = ctx.resolved.get(def.target);
1169
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1170
+ const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
1171
+ let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1172
+ const blend = def.blend ?? "opaque";
1173
+ const space = def.space ?? "okhsl";
1174
+ const baseLinear = variantToLinearRgb(baseVariant);
1175
+ const targetLinear = variantToLinearRgb(targetVariant);
1176
+ if (def.contrast !== void 0) {
1177
+ const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
1178
+ let luminanceAt;
1179
+ if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1180
+ else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1181
+ else luminanceAt = (v) => {
1182
+ return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1183
+ };
1184
+ t = findValueForMixContrast({
1185
+ preferredValue: t,
1186
+ baseLinearRgb: baseLinear,
1187
+ targetLinearRgb: targetLinear,
1188
+ contrast: minCr,
1189
+ luminanceAtValue: luminanceAt
1190
+ }).value;
1191
+ }
1192
+ if (blend === "transparent") return {
1193
+ h: targetVariant.h,
1194
+ s: targetVariant.s,
1195
+ l: targetVariant.l,
1196
+ alpha: clamp(t, 0, 1)
1197
+ };
1198
+ if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1199
+ return {
1200
+ h: mixHue(baseVariant, targetVariant, t),
1201
+ s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
1202
+ l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
1203
+ alpha: 1
1204
+ };
1205
+ }
997
1206
  function resolveAllColors(hue, saturation, defs) {
998
1207
  validateColorDefs(defs);
999
1208
  const order = topoSort(defs);
@@ -1004,7 +1213,8 @@ function resolveAllColors(hue, saturation, defs) {
1004
1213
  resolved: /* @__PURE__ */ new Map()
1005
1214
  };
1006
1215
  function defMode(def) {
1007
- return isShadowDef(def) ? void 0 : def.mode ?? "auto";
1216
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1217
+ return def.mode ?? "auto";
1008
1218
  }
1009
1219
  const lightMap = /* @__PURE__ */ new Map();
1010
1220
  for (const name of order) {
@@ -1217,26 +1427,40 @@ function createTheme(hue, saturation, initialColors) {
1217
1427
  }
1218
1428
  };
1219
1429
  }
1220
- function resolvePrefix(options, themeName) {
1221
- if (options?.prefix === true) return `${themeName}-`;
1222
- if (typeof options?.prefix === "object" && options.prefix !== null) return options.prefix[themeName] ?? `${themeName}-`;
1430
+ function resolvePrefix(options, themeName, defaultPrefix = false) {
1431
+ const prefix = options?.prefix ?? defaultPrefix;
1432
+ if (prefix === true) return `${themeName}-`;
1433
+ if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
1223
1434
  return "";
1224
1435
  }
1436
+ function validatePrimaryTheme(primary, themes) {
1437
+ if (primary !== void 0 && !(primary in themes)) {
1438
+ const available = Object.keys(themes).join(", ");
1439
+ throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
1440
+ }
1441
+ }
1225
1442
  function createPalette(themes) {
1226
1443
  return {
1227
1444
  tokens(options) {
1445
+ validatePrimaryTheme(options?.primary, themes);
1228
1446
  const modes = resolveModes(options?.modes);
1229
1447
  const allTokens = {};
1230
1448
  for (const [themeName, theme] of Object.entries(themes)) {
1231
- const tokens = buildFlatTokenMap(theme.resolve(), resolvePrefix(options, themeName), modes, options?.format);
1449
+ const resolved = theme.resolve();
1450
+ const tokens = buildFlatTokenMap(resolved, resolvePrefix(options, themeName, true), modes, options?.format);
1232
1451
  for (const variant of Object.keys(tokens)) {
1233
1452
  if (!allTokens[variant]) allTokens[variant] = {};
1234
1453
  Object.assign(allTokens[variant], tokens[variant]);
1235
1454
  }
1455
+ if (themeName === options?.primary) {
1456
+ const unprefixed = buildFlatTokenMap(resolved, "", modes, options?.format);
1457
+ for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
1458
+ }
1236
1459
  }
1237
1460
  return allTokens;
1238
1461
  },
1239
1462
  tasty(options) {
1463
+ validatePrimaryTheme(options?.primary, themes);
1240
1464
  const states = {
1241
1465
  dark: options?.states?.dark ?? globalConfig.states.dark,
1242
1466
  highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
@@ -1244,8 +1468,13 @@ function createPalette(themes) {
1244
1468
  const modes = resolveModes(options?.modes);
1245
1469
  const allTokens = {};
1246
1470
  for (const [themeName, theme] of Object.entries(themes)) {
1247
- const tokens = buildTokenMap(theme.resolve(), resolvePrefix(options, themeName), states, modes, options?.format);
1471
+ const resolved = theme.resolve();
1472
+ const tokens = buildTokenMap(resolved, resolvePrefix(options, themeName, true), states, modes, options?.format);
1248
1473
  Object.assign(allTokens, tokens);
1474
+ if (themeName === options?.primary) {
1475
+ const unprefixed = buildTokenMap(resolved, "", states, modes, options?.format);
1476
+ Object.assign(allTokens, unprefixed);
1477
+ }
1249
1478
  }
1250
1479
  return allTokens;
1251
1480
  },
@@ -1256,6 +1485,7 @@ function createPalette(themes) {
1256
1485
  return result;
1257
1486
  },
1258
1487
  css(options) {
1488
+ validatePrimaryTheme(options?.primary, themes);
1259
1489
  const suffix = options?.suffix ?? "-color";
1260
1490
  const format = options?.format ?? "rgb";
1261
1491
  const allLines = {
@@ -1265,13 +1495,23 @@ function createPalette(themes) {
1265
1495
  darkContrast: []
1266
1496
  };
1267
1497
  for (const [themeName, theme] of Object.entries(themes)) {
1268
- const css = buildCssMap(theme.resolve(), resolvePrefix(options, themeName), suffix, format);
1498
+ const resolved = theme.resolve();
1499
+ const css = buildCssMap(resolved, resolvePrefix(options, themeName, true), suffix, format);
1269
1500
  for (const key of [
1270
1501
  "light",
1271
1502
  "dark",
1272
1503
  "lightContrast",
1273
1504
  "darkContrast"
1274
1505
  ]) if (css[key]) allLines[key].push(css[key]);
1506
+ if (themeName === options?.primary) {
1507
+ const unprefixed = buildCssMap(resolved, "", suffix, format);
1508
+ for (const key of [
1509
+ "light",
1510
+ "dark",
1511
+ "lightContrast",
1512
+ "darkContrast"
1513
+ ]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
1514
+ }
1275
1515
  }
1276
1516
  return {
1277
1517
  light: allLines.light.join("\n"),
@@ -1331,6 +1571,7 @@ glaze.configure = function configure(config) {
1331
1571
  lightLightness: config.lightLightness ?? globalConfig.lightLightness,
1332
1572
  darkLightness: config.darkLightness ?? globalConfig.darkLightness,
1333
1573
  darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
1574
+ darkCurve: config.darkCurve ?? globalConfig.darkCurve,
1334
1575
  states: {
1335
1576
  dark: config.states?.dark ?? globalConfig.states.dark,
1336
1577
  highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
@@ -1430,6 +1671,7 @@ glaze.resetConfig = function resetConfig() {
1430
1671
  lightLightness: [10, 100],
1431
1672
  darkLightness: [15, 95],
1432
1673
  darkDesaturation: .1,
1674
+ darkCurve: .5,
1433
1675
  states: {
1434
1676
  dark: "@dark",
1435
1677
  highContrast: "@high-contrast"
@@ -1442,5 +1684,5 @@ glaze.resetConfig = function resetConfig() {
1442
1684
  };
1443
1685
 
1444
1686
  //#endregion
1445
- export { contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1687
+ export { contrastRatioFromLuminance, findLightnessForContrast, findValueForMixContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, gamutClampedLuminance, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
1446
1688
  //# sourceMappingURL=index.mjs.map