@tenphi/glaze 0.0.0-snapshot.c84faa6 → 0.0.0-snapshot.e76f494

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/README.md CHANGED
@@ -22,6 +22,7 @@ Glaze generates robust **light**, **dark**, and **high-contrast** color schemes
22
22
 
23
23
  - **OKHSL color space** — perceptually uniform hue and saturation
24
24
  - **WCAG 2 contrast solving** — automatic lightness adjustment to meet AA/AAA targets
25
+ - **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
25
26
  - **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
26
27
  - **Light + Dark + High-Contrast** — all schemes from one definition
27
28
  - **Per-color hue override** — absolute or relative hue shifts within a theme
@@ -413,6 +414,139 @@ const css = glaze.format(v, 'oklch');
413
414
  }
414
415
  ```
415
416
 
417
+ ## Mix Colors
418
+
419
+ Mix colors blend two existing colors together. Use them for hover overlays, tints, shades, and any derived color that sits between two reference colors.
420
+
421
+ ### Opaque Mix
422
+
423
+ Produces a solid color by interpolating between `base` and `target`:
424
+
425
+ ```ts
426
+ theme.colors({
427
+ surface: { lightness: 95 },
428
+ accent: { lightness: 30 },
429
+
430
+ // 30% of the way from surface toward accent
431
+ tint: { type: 'mix', base: 'surface', target: 'accent', value: 30 },
432
+ });
433
+ ```
434
+
435
+ - `value` — mix ratio 0–100 (0 = pure base, 100 = pure target)
436
+ - The result is a fully opaque color (alpha = 1)
437
+ - Adapts to light/dark/HC schemes automatically via the resolved base and target
438
+
439
+ ### Transparent Mix
440
+
441
+ Produces the target color with a controlled opacity — useful for hover overlays:
442
+
443
+ ```ts
444
+ theme.colors({
445
+ surface: { lightness: 95 },
446
+ black: { lightness: 0, saturation: 0 },
447
+
448
+ hover: {
449
+ type: 'mix',
450
+ base: 'surface',
451
+ target: 'black',
452
+ value: 8,
453
+ blend: 'transparent',
454
+ },
455
+ });
456
+ // hover → target color (black) with alpha = 0.08
457
+ ```
458
+
459
+ The output color has `h`, `s`, `l` from the target and `alpha = value / 100`.
460
+
461
+ ### Blend Space
462
+
463
+ By default, opaque mixing interpolates in OKHSL (perceptually uniform, consistent with Glaze's model). Use `space: 'srgb'` for linear sRGB interpolation, which matches browser compositing:
464
+
465
+ ```ts
466
+ theme.colors({
467
+ surface: { lightness: 95 },
468
+ accent: { lightness: 30 },
469
+
470
+ // sRGB blend — matches what the browser would render
471
+ hover: { type: 'mix', base: 'surface', target: 'accent', value: 20, space: 'srgb' },
472
+ });
473
+ ```
474
+
475
+ | Space | Behavior | Best for |
476
+ |---|---|---|
477
+ | `'okhsl'` (default) | Perceptually uniform OKHSL interpolation | Design token derivation |
478
+ | `'srgb'` | Linear sRGB channel interpolation | Matching browser compositing |
479
+
480
+ The `space` option only affects opaque blending. Transparent blending always composites in linear sRGB (matching browser alpha compositing).
481
+
482
+ ### Contrast Solving
483
+
484
+ Mix colors support the same `contrast` prop as regular colors. The solver adjusts the mix ratio (opaque) or opacity (transparent) to meet the WCAG target:
485
+
486
+ ```ts
487
+ theme.colors({
488
+ surface: { lightness: 95 },
489
+ accent: { lightness: 30 },
490
+
491
+ // Ensure the mixed color has at least AA contrast against surface
492
+ tint: {
493
+ type: 'mix',
494
+ base: 'surface',
495
+ target: 'accent',
496
+ value: 10,
497
+ contrast: 'AA',
498
+ },
499
+
500
+ // Ensure the transparent overlay has at least 3:1 contrast
501
+ overlay: {
502
+ type: 'mix',
503
+ base: 'surface',
504
+ target: 'accent',
505
+ value: 5,
506
+ blend: 'transparent',
507
+ contrast: 3,
508
+ },
509
+ });
510
+ ```
511
+
512
+ ### High-Contrast Pairs
513
+
514
+ Both `value` and `contrast` support `[normal, highContrast]` pairs:
515
+
516
+ ```ts
517
+ theme.colors({
518
+ surface: { lightness: 95 },
519
+ accent: { lightness: 30 },
520
+
521
+ tint: {
522
+ type: 'mix',
523
+ base: 'surface',
524
+ target: 'accent',
525
+ value: [20, 40], // stronger mix in high-contrast mode
526
+ contrast: [3, 'AAA'], // stricter contrast in high-contrast mode
527
+ },
528
+ });
529
+ ```
530
+
531
+ ### Achromatic Colors
532
+
533
+ When mixing with achromatic colors (saturation near zero, e.g., white or black) in `okhsl` space, the hue comes from whichever color has saturation. This prevents meaningless hue artifacts and matches CSS `color-mix()` "missing component" behavior. For purely achromatic mixes, prefer `space: 'srgb'` where hue is irrelevant.
534
+
535
+ ### Mix Chaining
536
+
537
+ Mix colors can reference other mix colors, enabling multi-step derivations:
538
+
539
+ ```ts
540
+ theme.colors({
541
+ white: { lightness: 100, saturation: 0 },
542
+ black: { lightness: 0, saturation: 0 },
543
+ gray: { type: 'mix', base: 'white', target: 'black', value: 50, space: 'srgb' },
544
+ lightGray: { type: 'mix', base: 'white', target: 'gray', value: 50, space: 'srgb' },
545
+ });
546
+ ```
547
+
548
+ Mix colors cannot reference shadow colors (same restriction as regular dependent colors).
549
+
416
550
  ## Output Formats
417
551
 
418
552
  Control the color format in exports with the `format` option:
@@ -758,10 +892,10 @@ glaze.configure({
758
892
 
759
893
  ## Color Definition Shape
760
894
 
761
- `ColorDef` is a discriminated union of regular colors and shadow colors:
895
+ `ColorDef` is a discriminated union of regular colors, shadow colors, and mix colors:
762
896
 
763
897
  ```ts
764
- type ColorDef = RegularColorDef | ShadowColorDef;
898
+ type ColorDef = RegularColorDef | ShadowColorDef | MixColorDef;
765
899
 
766
900
  interface RegularColorDef {
767
901
  lightness?: HCPair<number | RelativeValue>;
@@ -780,9 +914,19 @@ interface ShadowColorDef {
780
914
  intensity: HCPair<number>; // 0–100
781
915
  tuning?: ShadowTuning;
782
916
  }
917
+
918
+ interface MixColorDef {
919
+ type: 'mix';
920
+ base: string; // "from" color name
921
+ target: string; // "to" color name
922
+ value: HCPair<number>; // 0–100 (mix ratio or opacity)
923
+ blend?: 'opaque' | 'transparent'; // default: 'opaque'
924
+ space?: 'okhsl' | 'srgb'; // default: 'okhsl'
925
+ contrast?: HCPair<MinContrast>;
926
+ }
783
927
  ```
784
928
 
785
- A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color.
929
+ A root color must have absolute `lightness` (a number). A dependent color must have `base`. Relative `lightness` (a string) requires `base`. Shadow colors use `type: 'shadow'` and must reference a non-shadow `bg` color. Mix colors use `type: 'mix'` and must reference two non-shadow colors.
786
930
 
787
931
  ## Validation
788
932
 
@@ -801,6 +945,12 @@ A root color must have absolute `lightness` (a number). A dependent color must h
801
945
  | Regular color `base` references a shadow color | Validation error |
802
946
  | Shadow `intensity` outside 0–100 | Clamp silently |
803
947
  | `contrast` + `opacity` combined | Warning |
948
+ | Mix `base` references non-existent color | Validation error |
949
+ | Mix `target` references non-existent color | Validation error |
950
+ | Mix `base` references a shadow color | Validation error |
951
+ | Mix `target` references a shadow color | Validation error |
952
+ | Mix `value` outside 0–100 | Clamp silently |
953
+ | Circular references involving mix colors | Validation error |
804
954
 
805
955
  ## Advanced: Color Math Utilities
806
956
 
@@ -846,6 +996,10 @@ primary.colors({
846
996
  'shadow-md': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 10 },
847
997
  'shadow-lg': { type: 'shadow', bg: 'surface', fg: 'text', intensity: 20 },
848
998
 
999
+ // Mix colors — hover overlays and tints
1000
+ 'hover': { type: 'mix', base: 'surface', target: 'accent-fill', value: 8, blend: 'transparent' },
1001
+ 'tint': { type: 'mix', base: 'surface', target: 'accent-fill', value: 20 },
1002
+
849
1003
  // Fixed-alpha overlay
850
1004
  overlay: { lightness: 0, opacity: 0.5 },
851
1005
  });
package/dist/index.cjs CHANGED
@@ -518,7 +518,7 @@ const cacheOrder = [];
518
518
  * rendering pipeline: gamma-encode, clamp to sRGB gamut [0,1], then linearize.
519
519
  * This avoids over/under-estimating luminance for out-of-gamut OKHSL colors.
520
520
  */
521
- function gamutClampedLuminance(linearRgb) {
521
+ function gamutClampedLuminance$1(linearRgb) {
522
522
  const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
523
523
  const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
524
524
  const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
@@ -529,7 +529,7 @@ function cachedLuminance(h, s, l) {
529
529
  const key = `${h}|${s}|${lRounded}`;
530
530
  const cached = luminanceCache.get(key);
531
531
  if (cached !== void 0) return cached;
532
- const y = gamutClampedLuminance(okhslToLinearSrgb(h, s, lRounded));
532
+ const y = gamutClampedLuminance$1(okhslToLinearSrgb(h, s, lRounded));
533
533
  if (luminanceCache.size >= CACHE_SIZE) {
534
534
  const evict = cacheOrder.shift();
535
535
  luminanceCache.delete(evict);
@@ -649,10 +649,10 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
649
649
  function findLightnessForContrast(options) {
650
650
  const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
651
651
  const target = resolveMinContrast(contrastInput);
652
- const searchTarget = target + .1;
653
- const yBase = gamutClampedLuminance(baseLinearRgb);
652
+ const searchTarget = target + .01;
653
+ const yBase = gamutClampedLuminance$1(baseLinearRgb);
654
654
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
655
- if (crPref >= target) return {
655
+ if (crPref >= searchTarget) return {
656
656
  lightness: preferredLightness,
657
657
  contrast: crPref,
658
658
  met: true,
@@ -701,6 +701,135 @@ function findLightnessForContrast(options) {
701
701
  candidates.sort((a, b) => b.contrast - a.contrast);
702
702
  return candidates[0];
703
703
  }
704
+ /**
705
+ * Binary-search one branch [lo, hi] for the nearest passing mix value
706
+ * to `preferred`.
707
+ */
708
+ function searchMixBranch(lo, hi, yBase, target, epsilon, maxIter, preferred, luminanceAt) {
709
+ const crLo = contrastRatioFromLuminance(luminanceAt(lo), yBase);
710
+ const crHi = contrastRatioFromLuminance(luminanceAt(hi), yBase);
711
+ if (crLo < target && crHi < target) {
712
+ if (crLo >= crHi) return {
713
+ lightness: lo,
714
+ contrast: crLo,
715
+ met: false
716
+ };
717
+ return {
718
+ lightness: hi,
719
+ contrast: crHi,
720
+ met: false
721
+ };
722
+ }
723
+ let low = lo;
724
+ let high = hi;
725
+ for (let i = 0; i < maxIter; i++) {
726
+ if (high - low < epsilon) break;
727
+ const mid = (low + high) / 2;
728
+ if (contrastRatioFromLuminance(luminanceAt(mid), yBase) >= target) if (mid < preferred) low = mid;
729
+ else high = mid;
730
+ else if (mid < preferred) high = mid;
731
+ else low = mid;
732
+ }
733
+ const crLow = contrastRatioFromLuminance(luminanceAt(low), yBase);
734
+ const crHigh = contrastRatioFromLuminance(luminanceAt(high), yBase);
735
+ const lowPasses = crLow >= target;
736
+ const highPasses = crHigh >= target;
737
+ if (lowPasses && highPasses) {
738
+ if (Math.abs(low - preferred) <= Math.abs(high - preferred)) return {
739
+ lightness: low,
740
+ contrast: crLow,
741
+ met: true
742
+ };
743
+ return {
744
+ lightness: high,
745
+ contrast: crHigh,
746
+ met: true
747
+ };
748
+ }
749
+ if (lowPasses) return {
750
+ lightness: low,
751
+ contrast: crLow,
752
+ met: true
753
+ };
754
+ if (highPasses) return {
755
+ lightness: high,
756
+ contrast: crHigh,
757
+ met: true
758
+ };
759
+ return crLow >= crHigh ? {
760
+ lightness: low,
761
+ contrast: crLow,
762
+ met: false
763
+ } : {
764
+ lightness: high,
765
+ contrast: crHigh,
766
+ met: false
767
+ };
768
+ }
769
+ /**
770
+ * Find the mix parameter (ratio or opacity) that satisfies a WCAG 2 contrast
771
+ * target against a base color, staying as close to `preferredValue` as possible.
772
+ */
773
+ function findValueForMixContrast(options) {
774
+ const { preferredValue, baseLinearRgb, contrast: contrastInput, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
775
+ const target = resolveMinContrast(contrastInput);
776
+ const searchTarget = target + .01;
777
+ const yBase = gamutClampedLuminance$1(baseLinearRgb);
778
+ const crPref = contrastRatioFromLuminance(luminanceAtValue(preferredValue), yBase);
779
+ if (crPref >= searchTarget) return {
780
+ value: preferredValue,
781
+ contrast: crPref,
782
+ met: true
783
+ };
784
+ const darkerResult = preferredValue > 0 ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
785
+ const lighterResult = preferredValue < 1 ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : null;
786
+ if (darkerResult) darkerResult.met = darkerResult.contrast >= target;
787
+ if (lighterResult) lighterResult.met = lighterResult.contrast >= target;
788
+ const darkerPasses = darkerResult?.met ?? false;
789
+ const lighterPasses = lighterResult?.met ?? false;
790
+ if (darkerPasses && lighterPasses) {
791
+ if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
792
+ value: darkerResult.lightness,
793
+ contrast: darkerResult.contrast,
794
+ met: true
795
+ };
796
+ return {
797
+ value: lighterResult.lightness,
798
+ contrast: lighterResult.contrast,
799
+ met: true
800
+ };
801
+ }
802
+ if (darkerPasses) return {
803
+ value: darkerResult.lightness,
804
+ contrast: darkerResult.contrast,
805
+ met: true
806
+ };
807
+ if (lighterPasses) return {
808
+ value: lighterResult.lightness,
809
+ contrast: lighterResult.contrast,
810
+ met: true
811
+ };
812
+ const candidates = [];
813
+ if (darkerResult) candidates.push({
814
+ ...darkerResult,
815
+ branch: "lower"
816
+ });
817
+ if (lighterResult) candidates.push({
818
+ ...lighterResult,
819
+ branch: "upper"
820
+ });
821
+ if (candidates.length === 0) return {
822
+ value: preferredValue,
823
+ contrast: crPref,
824
+ met: false
825
+ };
826
+ candidates.sort((a, b) => b.contrast - a.contrast);
827
+ return {
828
+ value: candidates[0].lightness,
829
+ contrast: candidates[0].contrast,
830
+ met: candidates[0].met
831
+ };
832
+ }
704
833
 
705
834
  //#endregion
706
835
  //#region src/glaze.ts
@@ -732,6 +861,9 @@ function pairHC(p) {
732
861
  function isShadowDef(def) {
733
862
  return def.type === "shadow";
734
863
  }
864
+ function isMixDef(def) {
865
+ return def.type === "mix";
866
+ }
735
867
  const DEFAULT_SHADOW_TUNING = {
736
868
  saturationFactor: .18,
737
869
  maxSaturation: .25,
@@ -799,6 +931,13 @@ function validateColorDefs(defs) {
799
931
  }
800
932
  continue;
801
933
  }
934
+ if (isMixDef(def)) {
935
+ if (!names.has(def.base)) throw new Error(`glaze: mix "${name}" references non-existent base "${def.base}".`);
936
+ if (!names.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
937
+ if (isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
938
+ if (isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
939
+ continue;
940
+ }
802
941
  const regDef = def;
803
942
  if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
804
943
  if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
@@ -817,6 +956,9 @@ function validateColorDefs(defs) {
817
956
  if (isShadowDef(def)) {
818
957
  dfs(def.bg);
819
958
  if (def.fg) dfs(def.fg);
959
+ } else if (isMixDef(def)) {
960
+ dfs(def.base);
961
+ dfs(def.target);
820
962
  } else {
821
963
  const regDef = def;
822
964
  if (regDef.base) dfs(regDef.base);
@@ -836,6 +978,9 @@ function topoSort(defs) {
836
978
  if (isShadowDef(def)) {
837
979
  visit(def.bg);
838
980
  if (def.fg) visit(def.fg);
981
+ } else if (isMixDef(def)) {
982
+ visit(def.base);
983
+ visit(def.target);
839
984
  } else {
840
985
  const regDef = def;
841
986
  if (regDef.base) visit(regDef.base);
@@ -951,6 +1096,7 @@ function getSchemeVariant(color, isDark, isHighContrast) {
951
1096
  }
952
1097
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
953
1098
  if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
1099
+ if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
954
1100
  const regDef = def;
955
1101
  const mode = regDef.mode ?? "auto";
956
1102
  const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
@@ -996,6 +1142,89 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
996
1142
  const tuning = resolveShadowTuning(def.tuning);
997
1143
  return computeShadow(bgVariant, fgVariant, intensity, tuning);
998
1144
  }
1145
+ function variantToLinearRgb(v) {
1146
+ return okhslToLinearSrgb(v.h, v.s, v.l);
1147
+ }
1148
+ function gamutClampedLuminance(linearRgb) {
1149
+ const r = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[0]))));
1150
+ const g = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[1]))));
1151
+ const b = sRGBGammaToLinear(Math.max(0, Math.min(1, sRGBLinearToGamma(linearRgb[2]))));
1152
+ return .2126 * r + .7152 * g + .0722 * b;
1153
+ }
1154
+ /**
1155
+ * Resolve hue for OKHSL mixing, handling achromatic colors.
1156
+ * When one color has no saturation, its hue is meaningless —
1157
+ * use the hue from the color that has saturation (matches CSS
1158
+ * color-mix "missing component" behavior).
1159
+ */
1160
+ function mixHue(base, target, t) {
1161
+ const SAT_EPSILON = 1e-6;
1162
+ const baseHasSat = base.s > SAT_EPSILON;
1163
+ const targetHasSat = target.s > SAT_EPSILON;
1164
+ if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
1165
+ if (targetHasSat) return target.h;
1166
+ return base.h;
1167
+ }
1168
+ function linearSrgbLerp(base, target, t) {
1169
+ return [
1170
+ base[0] + (target[0] - base[0]) * t,
1171
+ base[1] + (target[1] - base[1]) * t,
1172
+ base[2] + (target[2] - base[2]) * t
1173
+ ];
1174
+ }
1175
+ function linearRgbToVariant(rgb) {
1176
+ const [h, s, l] = srgbToOkhsl([
1177
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[0]))),
1178
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[1]))),
1179
+ Math.max(0, Math.min(1, sRGBLinearToGamma(rgb[2])))
1180
+ ]);
1181
+ return {
1182
+ h,
1183
+ s,
1184
+ l,
1185
+ alpha: 1
1186
+ };
1187
+ }
1188
+ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
1189
+ const baseResolved = ctx.resolved.get(def.base);
1190
+ const targetResolved = ctx.resolved.get(def.target);
1191
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
1192
+ const targetVariant = getSchemeVariant(targetResolved, isDark, isHighContrast);
1193
+ let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
1194
+ const blend = def.blend ?? "opaque";
1195
+ const space = def.space ?? "okhsl";
1196
+ const baseLinear = variantToLinearRgb(baseVariant);
1197
+ const targetLinear = variantToLinearRgb(targetVariant);
1198
+ if (def.contrast !== void 0) {
1199
+ const minCr = isHighContrast ? pairHC(def.contrast) : pairNormal(def.contrast);
1200
+ let luminanceAt;
1201
+ if (blend === "transparent") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1202
+ else if (space === "srgb") luminanceAt = (v) => gamutClampedLuminance(linearSrgbLerp(baseLinear, targetLinear, v));
1203
+ else luminanceAt = (v) => {
1204
+ return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
1205
+ };
1206
+ t = findValueForMixContrast({
1207
+ preferredValue: t,
1208
+ baseLinearRgb: baseLinear,
1209
+ targetLinearRgb: targetLinear,
1210
+ contrast: minCr,
1211
+ luminanceAtValue: luminanceAt
1212
+ }).value;
1213
+ }
1214
+ if (blend === "transparent") return {
1215
+ h: targetVariant.h,
1216
+ s: targetVariant.s,
1217
+ l: targetVariant.l,
1218
+ alpha: clamp(t, 0, 1)
1219
+ };
1220
+ if (space === "srgb") return linearRgbToVariant(linearSrgbLerp(baseLinear, targetLinear, t));
1221
+ return {
1222
+ h: mixHue(baseVariant, targetVariant, t),
1223
+ s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
1224
+ l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
1225
+ alpha: 1
1226
+ };
1227
+ }
999
1228
  function resolveAllColors(hue, saturation, defs) {
1000
1229
  validateColorDefs(defs);
1001
1230
  const order = topoSort(defs);
@@ -1006,7 +1235,8 @@ function resolveAllColors(hue, saturation, defs) {
1006
1235
  resolved: /* @__PURE__ */ new Map()
1007
1236
  };
1008
1237
  function defMode(def) {
1009
- return isShadowDef(def) ? void 0 : def.mode ?? "auto";
1238
+ if (isShadowDef(def) || isMixDef(def)) return void 0;
1239
+ return def.mode ?? "auto";
1010
1240
  }
1011
1241
  const lightMap = /* @__PURE__ */ new Map();
1012
1242
  for (const name of order) {
@@ -1446,6 +1676,7 @@ glaze.resetConfig = function resetConfig() {
1446
1676
  //#endregion
1447
1677
  exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
1448
1678
  exports.findLightnessForContrast = findLightnessForContrast;
1679
+ exports.findValueForMixContrast = findValueForMixContrast;
1449
1680
  exports.formatHsl = formatHsl;
1450
1681
  exports.formatOkhsl = formatOkhsl;
1451
1682
  exports.formatOklch = formatOklch;