@tenphi/glaze 0.4.0 → 0.5.1

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.d.mts CHANGED
@@ -62,7 +62,15 @@ interface GlazeOutputModes {
62
62
  /** Include high-contrast variants (both light-HC and dark-HC). Default: false. */
63
63
  highContrast?: boolean;
64
64
  }
65
- interface ColorDef {
65
+ /** Hex color string for DX hints. Runtime validation in `parseHex()`. */
66
+ type HexColor = `#${string}`;
67
+ /** Direct OKHSL color input. */
68
+ interface OkhslColor {
69
+ h: number;
70
+ s: number;
71
+ l: number;
72
+ }
73
+ interface RegularColorDef {
66
74
  /**
67
75
  * Lightness value (0–100).
68
76
  * - Number: absolute lightness.
@@ -83,7 +91,60 @@ interface ColorDef {
83
91
  contrast?: HCPair<MinContrast>;
84
92
  /** Adaptation mode. Default: 'auto'. */
85
93
  mode?: AdaptationMode;
94
+ /**
95
+ * Fixed opacity (0–1).
96
+ * Output includes alpha in the CSS value.
97
+ * Does not affect contrast resolution — a semi-transparent color
98
+ * has no fixed perceived lightness, so `contrast` and `opacity`
99
+ * should not be combined (a console.warn is emitted).
100
+ */
101
+ opacity?: number;
86
102
  }
103
+ /** Shadow tuning knobs. All values use the 0–1 scale (OKHSL). */
104
+ interface ShadowTuning {
105
+ /** Fraction of fg saturation kept in pigment (0-1). Default: 0.18. */
106
+ saturationFactor?: number;
107
+ /** Upper clamp on pigment saturation (0-1). Default: 0.25. */
108
+ maxSaturation?: number;
109
+ /** Multiplier for bg lightness → pigment lightness. Default: 0.25. */
110
+ lightnessFactor?: number;
111
+ /** [min, max] clamp for pigment lightness (0-1). Default: [0.05, 0.20]. */
112
+ lightnessBounds?: [number, number];
113
+ /**
114
+ * Target minimum gap between pigment lightness and bg lightness (0-1).
115
+ * Default: 0.05.
116
+ */
117
+ minGapTarget?: number;
118
+ /** Max alpha (0-1). Reached at intensity=100 with max contrast. Default: 1.0. */
119
+ alphaMax?: number;
120
+ /**
121
+ * Blend weight (0-1) pulling pigment hue toward bg hue.
122
+ * 0 = pure fg hue, 1 = pure bg hue. Default: 0.2.
123
+ */
124
+ bgHueBlend?: number;
125
+ }
126
+ interface ShadowColorDef {
127
+ type: 'shadow';
128
+ /**
129
+ * Background color name — the surface the shadow sits on.
130
+ * Must reference a non-shadow color in the same theme.
131
+ */
132
+ bg: string;
133
+ /**
134
+ * Foreground color name for tinting and intensity modulation.
135
+ * Must reference a non-shadow color in the same theme.
136
+ * Omit for achromatic shadow at full user-specified intensity.
137
+ */
138
+ fg?: string;
139
+ /**
140
+ * Shadow intensity, 0-100.
141
+ * Supports [normal, highContrast] pair.
142
+ */
143
+ intensity: HCPair<number>;
144
+ /** Override default tuning. Merged field-by-field with global `shadowTuning`. */
145
+ tuning?: ShadowTuning;
146
+ }
147
+ type ColorDef = RegularColorDef | ShadowColorDef;
87
148
  type ColorMap = Record<string, ColorDef>;
88
149
  /** Resolved color for a single scheme variant. */
89
150
  interface ResolvedColorVariant {
@@ -93,6 +154,8 @@ interface ResolvedColorVariant {
93
154
  s: number;
94
155
  /** OKHSL lightness (0–1). */
95
156
  l: number;
157
+ /** Opacity (0–1). Default: 1. */
158
+ alpha: number;
96
159
  }
97
160
  /** Fully resolved color across all scheme variants. */
98
161
  interface ResolvedColor {
@@ -101,7 +164,8 @@ interface ResolvedColor {
101
164
  dark: ResolvedColorVariant;
102
165
  lightContrast: ResolvedColorVariant;
103
166
  darkContrast: ResolvedColorVariant;
104
- mode: AdaptationMode;
167
+ /** Adaptation mode. Present only for regular colors, omitted for shadows. */
168
+ mode?: AdaptationMode;
105
169
  }
106
170
  interface GlazeConfig {
107
171
  /** Dark scheme lightness window [lo, hi]. Default: [10, 90]. */
@@ -115,6 +179,8 @@ interface GlazeConfig {
115
179
  };
116
180
  /** Which scheme variants to include in exports. Default: both true. */
117
181
  modes?: GlazeOutputModes;
182
+ /** Default tuning for all shadow colors. Per-color tuning merges field-by-field. */
183
+ shadowTuning?: ShadowTuning;
118
184
  }
119
185
  interface GlazeConfigResolved {
120
186
  darkLightness: [number, number];
@@ -124,6 +190,7 @@ interface GlazeConfigResolved {
124
190
  highContrast: string;
125
191
  };
126
192
  modes: Required<GlazeOutputModes>;
193
+ shadowTuning?: ShadowTuning;
127
194
  }
128
195
  /** Serialized theme configuration (no resolved values). */
129
196
  interface GlazeThemeExport {
@@ -131,6 +198,16 @@ interface GlazeThemeExport {
131
198
  saturation: number;
132
199
  colors: ColorMap;
133
200
  }
201
+ /** Input for `glaze.shadow()` standalone factory. */
202
+ interface GlazeShadowInput {
203
+ /** Background color — hex string or OKHSL { h, s (0-1), l (0-1) }. */
204
+ bg: HexColor | OkhslColor;
205
+ /** Foreground color for tinting + intensity modulation. */
206
+ fg?: HexColor | OkhslColor;
207
+ /** Intensity 0-100. */
208
+ intensity: number;
209
+ tuning?: ShadowTuning;
210
+ }
134
211
  /** Input for `glaze.color()` standalone factory. */
135
212
  interface GlazeColorInput {
136
213
  hue: number;
@@ -298,6 +375,8 @@ declare namespace glaze {
298
375
  };
299
376
  var from: (data: GlazeThemeExport) => GlazeTheme;
300
377
  var color: (input: GlazeColorInput) => GlazeColorToken;
378
+ var shadow: (input: GlazeShadowInput) => ResolvedColorVariant;
379
+ var format: (variant: ResolvedColorVariant, colorFormat?: GlazeColorFormat) => string;
301
380
  var fromHex: (hex: string) => GlazeTheme;
302
381
  var fromRgb: (r: number, g: number, b: number) => GlazeTheme;
303
382
  var getConfig: () => GlazeConfigResolved;
@@ -350,20 +429,20 @@ declare function parseHex(hex: string): [number, number, number] | null;
350
429
  */
351
430
  declare function formatOkhsl(h: number, s: number, l: number): string;
352
431
  /**
353
- * Format OKHSL values as a CSS `rgb(R, G, B)` string with fractional 0–255 values.
432
+ * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
354
433
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
355
434
  */
356
435
  declare function formatRgb(h: number, s: number, l: number): string;
357
436
  /**
358
- * Format OKHSL values as a CSS `hsl(H, S%, L%)` string.
437
+ * Format OKHSL values as a CSS `hsl(H S% L%)` string.
359
438
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
360
439
  */
361
440
  declare function formatHsl(h: number, s: number, l: number): string;
362
441
  /**
363
- * Format OKHSL values as a CSS `oklch(L% C H)` string.
442
+ * Format OKHSL values as a CSS `oklch(L C H)` string.
364
443
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
365
444
  */
366
445
  declare function formatOklch(h: number, s: number, l: number): string;
367
446
  //#endregion
368
- export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type MinContrast, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
447
+ export { type AdaptationMode, type ColorDef, type ColorMap, type ContrastPreset, type FindLightnessForContrastOptions, type FindLightnessForContrastResult, type GlazeColorFormat, type GlazeColorInput, type GlazeColorToken, type GlazeConfig, type GlazeCssOptions, type GlazeCssResult, type GlazeExtendOptions, type GlazeJsonOptions, type GlazeOutputModes, type GlazePalette, type GlazeShadowInput, type GlazeTheme, type GlazeThemeExport, type GlazeTokenOptions, type HCPair, type HexColor, type MinContrast, type OkhslColor, type RegularColorDef, type RelativeValue, type ResolvedColor, type ResolvedColorVariant, type ShadowColorDef, type ShadowTuning, contrastRatioFromLuminance, findLightnessForContrast, formatHsl, formatOkhsl, formatOklch, formatRgb, glaze, okhslToLinearSrgb, okhslToOklab, okhslToSrgb, parseHex, relativeLuminanceFromLinearRgb, resolveMinContrast, srgbToOkhsl };
369
448
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -439,23 +439,26 @@ function parseHex(hex) {
439
439
  }
440
440
  return null;
441
441
  }
442
+ function fmt$1(value, decimals) {
443
+ return parseFloat(value.toFixed(decimals)).toString();
444
+ }
442
445
  /**
443
446
  * Format OKHSL values as a CSS `okhsl(H S% L%)` string.
444
447
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
445
448
  */
446
449
  function formatOkhsl(h, s, l) {
447
- return `okhsl(${h.toFixed(1)} ${s.toFixed(1)}% ${l.toFixed(1)}%)`;
450
+ return `okhsl(${fmt$1(h, 1)} ${fmt$1(s, 1)}% ${fmt$1(l, 1)}%)`;
448
451
  }
449
452
  /**
450
- * Format OKHSL values as a CSS `rgb(R, G, B)` string with fractional 0–255 values.
453
+ * Format OKHSL values as a CSS `rgb(R G B)` string with rounded integer values.
451
454
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
452
455
  */
453
456
  function formatRgb(h, s, l) {
454
457
  const [r, g, b] = okhslToSrgb(h, s / 100, l / 100);
455
- return `rgb(${(r * 255).toFixed(3)}, ${(g * 255).toFixed(3)}, ${(b * 255).toFixed(3)})`;
458
+ return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`;
456
459
  }
457
460
  /**
458
- * Format OKHSL values as a CSS `hsl(H, S%, L%)` string.
461
+ * Format OKHSL values as a CSS `hsl(H S% L%)` string.
459
462
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
460
463
  */
461
464
  function formatHsl(h, s, l) {
@@ -472,10 +475,10 @@ function formatHsl(h, s, l) {
472
475
  else if (max === g) hh = ((b - r) / delta + 2) * 60;
473
476
  else hh = ((r - g) / delta + 4) * 60;
474
477
  }
475
- return `hsl(${hh.toFixed(1)}, ${(ss * 100).toFixed(1)}%, ${(ll * 100).toFixed(1)}%)`;
478
+ return `hsl(${fmt$1(hh, 1)} ${fmt$1(ss * 100, 1)}% ${fmt$1(ll * 100, 1)}%)`;
476
479
  }
477
480
  /**
478
- * Format OKHSL values as a CSS `oklch(L% C H)` string.
481
+ * Format OKHSL values as a CSS `oklch(L C H)` string.
479
482
  * h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
480
483
  */
481
484
  function formatOklch(h, s, l) {
@@ -483,7 +486,7 @@ function formatOklch(h, s, l) {
483
486
  const C = Math.sqrt(a * a + b * b);
484
487
  let hh = Math.atan2(b, a) * (180 / Math.PI);
485
488
  hh = constrainAngle(hh);
486
- return `oklch(${(L * 100).toFixed(1)}% ${C.toFixed(4)} ${hh.toFixed(1)})`;
489
+ return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 1)})`;
487
490
  }
488
491
 
489
492
  //#endregion
@@ -709,14 +712,84 @@ function pairNormal(p) {
709
712
  function pairHC(p) {
710
713
  return Array.isArray(p) ? p[1] : p;
711
714
  }
715
+ function isShadowDef(def) {
716
+ return def.type === "shadow";
717
+ }
718
+ const DEFAULT_SHADOW_TUNING = {
719
+ saturationFactor: .18,
720
+ maxSaturation: .25,
721
+ lightnessFactor: .25,
722
+ lightnessBounds: [.05, .2],
723
+ minGapTarget: .05,
724
+ alphaMax: 1,
725
+ bgHueBlend: .2
726
+ };
727
+ function resolveShadowTuning(perColor) {
728
+ return {
729
+ ...DEFAULT_SHADOW_TUNING,
730
+ ...globalConfig.shadowTuning,
731
+ ...perColor,
732
+ lightnessBounds: perColor?.lightnessBounds ?? globalConfig.shadowTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
733
+ };
734
+ }
735
+ function circularLerp(a, b, t) {
736
+ let diff = b - a;
737
+ if (diff > 180) diff -= 360;
738
+ else if (diff < -180) diff += 360;
739
+ return ((a + diff * t) % 360 + 360) % 360;
740
+ }
741
+ /**
742
+ * Compute the canonical max-contrast reference t value for normalization.
743
+ * Uses bg.l=1, fg.l=0, intensity=100 — the theoretical maximum.
744
+ * This is a fixed constant per tuning configuration, ensuring uniform
745
+ * scaling across all bg/fg pairs at low intensities.
746
+ */
747
+ function computeRefT(tuning) {
748
+ const EPSILON = 1e-6;
749
+ let lShRef = clamp(tuning.lightnessFactor, tuning.lightnessBounds[0], tuning.lightnessBounds[1]);
750
+ lShRef = Math.max(Math.min(lShRef, 1 - tuning.minGapTarget), 0);
751
+ return 1 / Math.max(1 - lShRef, EPSILON);
752
+ }
753
+ function computeShadow(bg, fg, intensity, tuning) {
754
+ const EPSILON = 1e-6;
755
+ const clampedIntensity = clamp(intensity, 0, 100);
756
+ const contrastWeight = fg ? Math.abs(bg.l - fg.l) : 1;
757
+ const deltaL = clampedIntensity / 100 * contrastWeight;
758
+ const h = fg ? circularLerp(fg.h, bg.h, tuning.bgHueBlend) : bg.h;
759
+ const s = fg ? Math.min(fg.s * tuning.saturationFactor, tuning.maxSaturation) : 0;
760
+ let lSh = clamp(bg.l * tuning.lightnessFactor, tuning.lightnessBounds[0], tuning.lightnessBounds[1]);
761
+ lSh = Math.max(Math.min(lSh, bg.l - tuning.minGapTarget), 0);
762
+ const t = deltaL / Math.max(bg.l - lSh, EPSILON);
763
+ const tRef = computeRefT(tuning);
764
+ const norm = Math.tanh(tRef / tuning.alphaMax);
765
+ const alpha = Math.min(tuning.alphaMax * Math.tanh(t / tuning.alphaMax) / norm, tuning.alphaMax);
766
+ return {
767
+ h,
768
+ s,
769
+ l: lSh,
770
+ alpha
771
+ };
772
+ }
712
773
  function validateColorDefs(defs) {
713
774
  const names = new Set(Object.keys(defs));
714
775
  for (const [name, def] of Object.entries(defs)) {
715
- if (def.contrast !== void 0 && !def.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
716
- if (def.lightness !== void 0 && !isAbsoluteLightness(def.lightness) && !def.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
717
- if (isAbsoluteLightness(def.lightness) && def.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
718
- if (def.base && !names.has(def.base)) throw new Error(`glaze: color "${name}" references non-existent base "${def.base}".`);
719
- if (!isAbsoluteLightness(def.lightness) && def.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
776
+ if (isShadowDef(def)) {
777
+ if (!names.has(def.bg)) throw new Error(`glaze: shadow "${name}" references non-existent bg "${def.bg}".`);
778
+ if (isShadowDef(defs[def.bg])) throw new Error(`glaze: shadow "${name}" bg "${def.bg}" references another shadow color.`);
779
+ if (def.fg !== void 0) {
780
+ if (!names.has(def.fg)) throw new Error(`glaze: shadow "${name}" references non-existent fg "${def.fg}".`);
781
+ if (isShadowDef(defs[def.fg])) throw new Error(`glaze: shadow "${name}" fg "${def.fg}" references another shadow color.`);
782
+ }
783
+ continue;
784
+ }
785
+ const regDef = def;
786
+ if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
787
+ if (regDef.lightness !== void 0 && !isAbsoluteLightness(regDef.lightness) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
788
+ if (isAbsoluteLightness(regDef.lightness) && regDef.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
789
+ if (regDef.base && !names.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
790
+ if (regDef.base && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
791
+ if (!isAbsoluteLightness(regDef.lightness) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
792
+ if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived lightness unpredictable.`);
720
793
  }
721
794
  const visited = /* @__PURE__ */ new Set();
722
795
  const inStack = /* @__PURE__ */ new Set();
@@ -725,7 +798,13 @@ function validateColorDefs(defs) {
725
798
  if (visited.has(name)) return;
726
799
  inStack.add(name);
727
800
  const def = defs[name];
728
- if (def.base && !isAbsoluteLightness(def.lightness)) dfs(def.base);
801
+ if (isShadowDef(def)) {
802
+ dfs(def.bg);
803
+ if (def.fg) dfs(def.fg);
804
+ } else {
805
+ const regDef = def;
806
+ if (regDef.base && !isAbsoluteLightness(regDef.lightness)) dfs(regDef.base);
807
+ }
729
808
  inStack.delete(name);
730
809
  visited.add(name);
731
810
  }
@@ -738,7 +817,13 @@ function topoSort(defs) {
738
817
  if (visited.has(name)) return;
739
818
  visited.add(name);
740
819
  const def = defs[name];
741
- if (def.base && !isAbsoluteLightness(def.lightness)) visit(def.base);
820
+ if (isShadowDef(def)) {
821
+ visit(def.bg);
822
+ if (def.fg) visit(def.fg);
823
+ } else {
824
+ const regDef = def;
825
+ if (regDef.base && !isAbsoluteLightness(regDef.lightness)) visit(regDef.base);
826
+ }
742
827
  result.push(name);
743
828
  }
744
829
  for (const name of Object.keys(defs)) visit(name);
@@ -802,11 +887,8 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
802
887
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
803
888
  const mode = def.mode ?? "auto";
804
889
  const satFactor = clamp(def.saturation ?? 1, 0, 1);
805
- let baseL;
806
- if (isDark && isHighContrast) baseL = baseResolved.darkContrast.l * 100;
807
- else if (isDark) baseL = baseResolved.dark.l * 100;
808
- else if (isHighContrast) baseL = baseResolved.lightContrast.l * 100;
809
- else baseL = baseResolved.light.l * 100;
890
+ const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
891
+ const baseL = baseVariant.l * 100;
810
892
  let preferredL;
811
893
  const rawLightness = def.lightness;
812
894
  if (rawLightness === void 0) preferredL = baseL;
@@ -823,27 +905,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
823
905
  if (rawContrast !== void 0) {
824
906
  const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
825
907
  const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
826
- let baseH;
827
- let baseS;
828
- let baseLNorm;
829
- if (isDark && isHighContrast) {
830
- baseH = baseResolved.darkContrast.h;
831
- baseS = baseResolved.darkContrast.s;
832
- baseLNorm = baseResolved.darkContrast.l;
833
- } else if (isDark) {
834
- baseH = baseResolved.dark.h;
835
- baseS = baseResolved.dark.s;
836
- baseLNorm = baseResolved.dark.l;
837
- } else if (isHighContrast) {
838
- baseH = baseResolved.lightContrast.h;
839
- baseS = baseResolved.lightContrast.s;
840
- baseLNorm = baseResolved.lightContrast.l;
841
- } else {
842
- baseH = baseResolved.light.h;
843
- baseS = baseResolved.light.s;
844
- baseLNorm = baseResolved.light.l;
845
- }
846
- const baseLinearRgb = okhslToLinearSrgb(baseH, baseS, baseLNorm);
908
+ const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
847
909
  return {
848
910
  l: findLightnessForContrast({
849
911
  hue: effectiveHue,
@@ -860,18 +922,26 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
860
922
  satFactor
861
923
  };
862
924
  }
925
+ function getSchemeVariant(color, isDark, isHighContrast) {
926
+ if (isDark && isHighContrast) return color.darkContrast;
927
+ if (isDark) return color.dark;
928
+ if (isHighContrast) return color.lightContrast;
929
+ return color.light;
930
+ }
863
931
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
864
- const mode = def.mode ?? "auto";
865
- const isRoot = isAbsoluteLightness(def.lightness) && !def.base;
866
- const effectiveHue = resolveEffectiveHue(ctx.hue, def.hue);
932
+ if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
933
+ const regDef = def;
934
+ const mode = regDef.mode ?? "auto";
935
+ const isRoot = isAbsoluteLightness(regDef.lightness) && !regDef.base;
936
+ const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
867
937
  let lightL;
868
938
  let satFactor;
869
939
  if (isRoot) {
870
- const root = resolveRootColor(name, def, ctx, isHighContrast);
940
+ const root = resolveRootColor(name, regDef, ctx, isHighContrast);
871
941
  lightL = root.lightL;
872
942
  satFactor = root.satFactor;
873
943
  } else {
874
- const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue);
944
+ const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
875
945
  lightL = dep.l;
876
946
  satFactor = dep.satFactor;
877
947
  }
@@ -890,9 +960,18 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
890
960
  return {
891
961
  h: effectiveHue,
892
962
  s: clamp(finalSat, 0, 1),
893
- l: clamp(finalL / 100, 0, 1)
963
+ l: clamp(finalL / 100, 0, 1),
964
+ alpha: regDef.opacity ?? 1
894
965
  };
895
966
  }
967
+ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
968
+ const bgVariant = getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast);
969
+ let fgVariant;
970
+ if (def.fg) fgVariant = getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast);
971
+ const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
972
+ const tuning = resolveShadowTuning(def.tuning);
973
+ return computeShadow(bgVariant, fgVariant, intensity, tuning);
974
+ }
896
975
  function resolveAllColors(hue, saturation, defs) {
897
976
  validateColorDefs(defs);
898
977
  const order = topoSort(defs);
@@ -902,6 +981,9 @@ function resolveAllColors(hue, saturation, defs) {
902
981
  defs,
903
982
  resolved: /* @__PURE__ */ new Map()
904
983
  };
984
+ function defMode(def) {
985
+ return isShadowDef(def) ? void 0 : def.mode ?? "auto";
986
+ }
905
987
  const lightMap = /* @__PURE__ */ new Map();
906
988
  for (const name of order) {
907
989
  const variant = resolveColorForScheme(name, defs[name], ctx, false, false);
@@ -912,7 +994,7 @@ function resolveAllColors(hue, saturation, defs) {
912
994
  dark: variant,
913
995
  lightContrast: variant,
914
996
  darkContrast: variant,
915
- mode: defs[name].mode ?? "auto"
997
+ mode: defMode(defs[name])
916
998
  });
917
999
  }
918
1000
  const lightHCMap = /* @__PURE__ */ new Map();
@@ -935,7 +1017,7 @@ function resolveAllColors(hue, saturation, defs) {
935
1017
  dark: lightMap.get(name),
936
1018
  lightContrast: lightHCMap.get(name),
937
1019
  darkContrast: lightHCMap.get(name),
938
- mode: defs[name].mode ?? "auto"
1020
+ mode: defMode(defs[name])
939
1021
  });
940
1022
  for (const name of order) {
941
1023
  const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
@@ -965,7 +1047,7 @@ function resolveAllColors(hue, saturation, defs) {
965
1047
  dark: darkMap.get(name),
966
1048
  lightContrast: lightHCMap.get(name),
967
1049
  darkContrast: darkHCMap.get(name),
968
- mode: defs[name].mode ?? "auto"
1050
+ mode: defMode(defs[name])
969
1051
  });
970
1052
  return result;
971
1053
  }
@@ -975,8 +1057,14 @@ const formatters = {
975
1057
  hsl: formatHsl,
976
1058
  oklch: formatOklch
977
1059
  };
1060
+ function fmt(value, decimals) {
1061
+ return parseFloat(value.toFixed(decimals)).toString();
1062
+ }
978
1063
  function formatVariant(v, format = "okhsl") {
979
- return formatters[format](v.h, v.s * 100, v.l * 100);
1064
+ const base = formatters[format](v.h, v.s * 100, v.l * 100);
1065
+ if (v.alpha >= 1) return base;
1066
+ const closing = base.lastIndexOf(")");
1067
+ return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
980
1068
  }
981
1069
  function resolveModes(override) {
982
1070
  return {
@@ -1227,7 +1315,8 @@ glaze.configure = function configure(config) {
1227
1315
  modes: {
1228
1316
  dark: config.modes?.dark ?? globalConfig.modes.dark,
1229
1317
  highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
1230
- }
1318
+ },
1319
+ shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
1231
1320
  };
1232
1321
  };
1233
1322
  /**
@@ -1249,6 +1338,40 @@ glaze.color = function color(input) {
1249
1338
  return createColorToken(input);
1250
1339
  };
1251
1340
  /**
1341
+ * Compute a shadow color from a bg/fg pair and intensity.
1342
+ */
1343
+ glaze.shadow = function shadow(input) {
1344
+ const bg = parseOkhslInput(input.bg);
1345
+ const fg = input.fg ? parseOkhslInput(input.fg) : void 0;
1346
+ const tuning = resolveShadowTuning(input.tuning);
1347
+ return computeShadow({
1348
+ ...bg,
1349
+ alpha: 1
1350
+ }, fg ? {
1351
+ ...fg,
1352
+ alpha: 1
1353
+ } : void 0, input.intensity, tuning);
1354
+ };
1355
+ /**
1356
+ * Format a resolved color variant as a CSS string.
1357
+ */
1358
+ glaze.format = function format(variant, colorFormat) {
1359
+ return formatVariant(variant, colorFormat);
1360
+ };
1361
+ function parseOkhslInput(input) {
1362
+ if (typeof input === "string") {
1363
+ const rgb = parseHex(input);
1364
+ if (!rgb) throw new Error(`glaze: invalid hex color "${input}".`);
1365
+ const [h, s, l] = srgbToOkhsl(rgb);
1366
+ return {
1367
+ h,
1368
+ s,
1369
+ l
1370
+ };
1371
+ }
1372
+ return input;
1373
+ }
1374
+ /**
1252
1375
  * Create a theme from a hex color string.
1253
1376
  * Extracts hue and saturation from the color.
1254
1377
  */