@tenphi/glaze 0.1.1 → 0.3.0

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
@@ -495,7 +495,7 @@ function formatOklch(h, s, l) {
495
495
  *
496
496
  * Finds the closest OKHSL lightness that satisfies a WCAG 2 contrast target
497
497
  * against a base color. Used by glaze when resolving dependent colors
498
- * with `ensureContrast`.
498
+ * with `contrast`.
499
499
  */
500
500
  const CONTRAST_PRESETS = {
501
501
  AA: 4.5,
@@ -633,8 +633,8 @@ function coarseScan(h, s, lo, hi, yBase, target, epsilon, maxIter) {
633
633
  * against a base color, staying as close to `preferredLightness` as possible.
634
634
  */
635
635
  function findLightnessForContrast(options) {
636
- const { hue, saturation, preferredLightness, baseLinearRgb, ensureContrast: ensureContrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
637
- const target = resolveMinContrast(ensureContrastInput);
636
+ const { hue, saturation, preferredLightness, baseLinearRgb, contrast: contrastInput, lightnessRange = [0, 1], epsilon = 1e-4, maxIterations = 14 } = options;
637
+ const target = resolveMinContrast(contrastInput);
638
638
  const yBase = relativeLuminanceFromLinearRgb(baseLinearRgb);
639
639
  const crPref = contrastRatioFromLuminance(cachedLuminance(hue, saturation, preferredLightness), yBase);
640
640
  if (crPref >= target) return {
@@ -715,9 +715,10 @@ function validateColorDefs(defs) {
715
715
  const names = new Set(Object.keys(defs));
716
716
  for (const [name, def] of Object.entries(defs)) {
717
717
  if (def.contrast !== void 0 && !def.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
718
- if (def.l !== void 0 && def.base !== void 0) console.warn(`glaze: color "${name}" has both "l" and "base". "l" takes precedence.`);
718
+ if (def.lightness !== void 0 && !isAbsoluteLightness(def.lightness) && !def.base) throw new Error(`glaze: color "${name}" has relative "lightness" without "base".`);
719
+ if (isAbsoluteLightness(def.lightness) && def.base !== void 0) console.warn(`glaze: color "${name}" has absolute "lightness" and "base". Absolute lightness takes precedence.`);
719
720
  if (def.base && !names.has(def.base)) throw new Error(`glaze: color "${name}" references non-existent base "${def.base}".`);
720
- if (def.l === void 0 && def.base === void 0) throw new Error(`glaze: color "${name}" must have either "l" (root) or "base" + "contrast" (dependent).`);
721
+ if (!isAbsoluteLightness(def.lightness) && def.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "lightness" (root) or "base" (dependent).`);
721
722
  }
722
723
  const visited = /* @__PURE__ */ new Set();
723
724
  const inStack = /* @__PURE__ */ new Set();
@@ -726,7 +727,7 @@ function validateColorDefs(defs) {
726
727
  if (visited.has(name)) return;
727
728
  inStack.add(name);
728
729
  const def = defs[name];
729
- if (def.base && def.l === void 0) dfs(def.base);
730
+ if (def.base && !isAbsoluteLightness(def.lightness)) dfs(def.base);
730
731
  inStack.delete(name);
731
732
  visited.add(name);
732
733
  }
@@ -739,7 +740,7 @@ function topoSort(defs) {
739
740
  if (visited.has(name)) return;
740
741
  visited.add(name);
741
742
  const def = defs[name];
742
- if (def.base && def.l === void 0) visit(def.base);
743
+ if (def.base && !isAbsoluteLightness(def.lightness)) visit(def.base);
743
744
  result.push(name);
744
745
  }
745
746
  for (const name of Object.keys(defs)) visit(name);
@@ -755,44 +756,75 @@ function mapSaturationDark(s, mode) {
755
756
  if (mode === "static") return s;
756
757
  return s * (1 - globalConfig.darkDesaturation);
757
758
  }
759
+ function clamp(v, min, max) {
760
+ return Math.max(min, Math.min(max, v));
761
+ }
758
762
  /**
759
- * Resolve the effective lightness from a contrast delta.
763
+ * Parse a value that can be absolute (number) or relative (signed string).
764
+ * Returns the numeric value and whether it's relative.
760
765
  */
761
- function resolveContrastLightness(baseLightness, contrast) {
762
- if (contrast < 0) return clamp(baseLightness + contrast, 0, 100);
763
- const candidate = baseLightness + contrast;
764
- if (candidate > 100) return clamp(baseLightness - contrast, 0, 100);
765
- return clamp(candidate, 0, 100);
766
+ function parseRelativeOrAbsolute(value) {
767
+ if (typeof value === "number") return {
768
+ value,
769
+ relative: false
770
+ };
771
+ return {
772
+ value: parseFloat(value),
773
+ relative: true
774
+ };
766
775
  }
767
- function clamp(v, min, max) {
768
- return Math.max(min, Math.min(max, v));
776
+ /**
777
+ * Compute the effective hue for a color, given the theme seed hue
778
+ * and an optional per-color hue override.
779
+ */
780
+ function resolveEffectiveHue(seedHue, defHue) {
781
+ if (defHue === void 0) return seedHue;
782
+ const parsed = parseRelativeOrAbsolute(defHue);
783
+ if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
784
+ return (parsed.value % 360 + 360) % 360;
785
+ }
786
+ /**
787
+ * Check whether a lightness value represents an absolute root definition
788
+ * (i.e. a number, not a relative string).
789
+ */
790
+ function isAbsoluteLightness(lightness) {
791
+ if (lightness === void 0) return false;
792
+ return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
769
793
  }
770
794
  function resolveRootColor(_name, def, _ctx, isHighContrast) {
771
- const rawL = def.l;
795
+ const rawL = def.lightness;
772
796
  return {
773
- lightL: clamp(isHighContrast ? pairHC(rawL) : pairNormal(rawL), 0, 100),
774
- sat: clamp(def.sat ?? 1, 0, 1)
797
+ lightL: clamp(parseRelativeOrAbsolute(isHighContrast ? pairHC(rawL) : pairNormal(rawL)).value, 0, 100),
798
+ satFactor: clamp(def.saturation ?? 1, 0, 1)
775
799
  };
776
800
  }
777
- function resolveDependentColor(name, def, ctx, isHighContrast, isDark) {
801
+ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue) {
778
802
  const baseName = def.base;
779
803
  const baseResolved = ctx.resolved.get(baseName);
780
804
  if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
781
805
  const mode = def.mode ?? "auto";
782
- const sat = clamp(def.sat ?? 1, 0, 1);
806
+ const satFactor = clamp(def.saturation ?? 1, 0, 1);
783
807
  let baseL;
784
808
  if (isDark && isHighContrast) baseL = baseResolved.darkContrast.l * 100;
785
809
  else if (isDark) baseL = baseResolved.dark.l * 100;
786
810
  else if (isHighContrast) baseL = baseResolved.lightContrast.l * 100;
787
811
  else baseL = baseResolved.light.l * 100;
788
- const rawContrast = def.contrast ?? 0;
789
- let contrast = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
790
- if (isDark && mode === "auto") contrast = -contrast;
791
- const preferredL = resolveContrastLightness(baseL, contrast);
792
- const rawEnsureContrast = def.ensureContrast;
793
- if (rawEnsureContrast !== void 0) {
794
- const minCr = isHighContrast ? pairHC(rawEnsureContrast) : pairNormal(rawEnsureContrast);
795
- const effectiveSat = isDark ? mapSaturationDark(sat * ctx.saturation / 100, mode) : sat * ctx.saturation / 100;
812
+ let preferredL;
813
+ const rawLightness = def.lightness;
814
+ if (rawLightness === void 0) preferredL = baseL;
815
+ else {
816
+ const parsed = parseRelativeOrAbsolute(isHighContrast ? pairHC(rawLightness) : pairNormal(rawLightness));
817
+ if (parsed.relative) {
818
+ let delta = parsed.value;
819
+ if (isDark && mode === "auto") delta = -delta;
820
+ preferredL = clamp(baseL + delta, 0, 100);
821
+ } else if (isDark) preferredL = mapLightnessDark(parsed.value, mode);
822
+ else preferredL = clamp(parsed.value, 0, 100);
823
+ }
824
+ const rawContrast = def.contrast;
825
+ if (rawContrast !== void 0) {
826
+ const minCr = isHighContrast ? pairHC(rawContrast) : pairNormal(rawContrast);
827
+ const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
796
828
  let baseH;
797
829
  let baseS;
798
830
  let baseLNorm;
@@ -816,48 +848,49 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark) {
816
848
  const baseLinearRgb = okhslToLinearSrgb(baseH, baseS, baseLNorm);
817
849
  return {
818
850
  l: findLightnessForContrast({
819
- hue: ctx.hue,
851
+ hue: effectiveHue,
820
852
  saturation: effectiveSat,
821
853
  preferredLightness: preferredL / 100,
822
854
  baseLinearRgb,
823
- ensureContrast: minCr
855
+ contrast: minCr
824
856
  }).lightness * 100,
825
- sat
857
+ satFactor
826
858
  };
827
859
  }
828
860
  return {
829
861
  l: clamp(preferredL, 0, 100),
830
- sat
862
+ satFactor
831
863
  };
832
864
  }
833
865
  function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
834
866
  const mode = def.mode ?? "auto";
835
- const isRoot = def.l !== void 0;
867
+ const isRoot = isAbsoluteLightness(def.lightness) && !def.base;
868
+ const effectiveHue = resolveEffectiveHue(ctx.hue, def.hue);
836
869
  let lightL;
837
- let sat;
870
+ let satFactor;
838
871
  if (isRoot) {
839
872
  const root = resolveRootColor(name, def, ctx, isHighContrast);
840
873
  lightL = root.lightL;
841
- sat = root.sat;
874
+ satFactor = root.satFactor;
842
875
  } else {
843
- const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark);
876
+ const dep = resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue);
844
877
  lightL = dep.l;
845
- sat = dep.sat;
878
+ satFactor = dep.satFactor;
846
879
  }
847
880
  let finalL;
848
881
  let finalSat;
849
882
  if (isDark && isRoot) {
850
883
  finalL = mapLightnessDark(lightL, mode);
851
- finalSat = mapSaturationDark(sat * ctx.saturation / 100, mode);
884
+ finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
852
885
  } else if (isDark && !isRoot) {
853
886
  finalL = lightL;
854
- finalSat = mapSaturationDark(sat * ctx.saturation / 100, mode);
887
+ finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
855
888
  } else {
856
889
  finalL = lightL;
857
- finalSat = sat * ctx.saturation / 100;
890
+ finalSat = satFactor * ctx.saturation / 100;
858
891
  }
859
892
  return {
860
- h: ctx.hue,
893
+ h: effectiveHue,
861
894
  s: clamp(finalSat, 0, 1),
862
895
  l: clamp(finalL / 100, 0, 1)
863
896
  };
@@ -976,6 +1009,27 @@ function buildJsonMap(resolved, modes, format = "okhsl") {
976
1009
  }
977
1010
  return result;
978
1011
  }
1012
+ function buildCssMap(resolved, prefix, suffix, format) {
1013
+ const lines = {
1014
+ light: [],
1015
+ dark: [],
1016
+ lightContrast: [],
1017
+ darkContrast: []
1018
+ };
1019
+ for (const [name, color] of resolved) {
1020
+ const prop = `--${prefix}${name}${suffix}`;
1021
+ lines.light.push(`${prop}: ${formatVariant(color.light, format)};`);
1022
+ lines.dark.push(`${prop}: ${formatVariant(color.dark, format)};`);
1023
+ lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format)};`);
1024
+ lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format)};`);
1025
+ }
1026
+ return {
1027
+ light: lines.light.join("\n"),
1028
+ dark: lines.dark.join("\n"),
1029
+ lightContrast: lines.lightContrast.join("\n"),
1030
+ darkContrast: lines.darkContrast.join("\n")
1031
+ };
1032
+ }
979
1033
  function createTheme(hue, saturation, initialColors) {
980
1034
  let colorDefs = initialColors ? { ...initialColors } : {};
981
1035
  return {
@@ -1032,6 +1086,9 @@ function createTheme(hue, saturation, initialColors) {
1032
1086
  },
1033
1087
  json(options) {
1034
1088
  return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
1089
+ },
1090
+ css(options) {
1091
+ return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
1035
1092
  }
1036
1093
  };
1037
1094
  }
@@ -1059,13 +1116,42 @@ function createPalette(themes) {
1059
1116
  const result = {};
1060
1117
  for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
1061
1118
  return result;
1119
+ },
1120
+ css(options) {
1121
+ const suffix = options?.suffix ?? "-color";
1122
+ const format = options?.format ?? "rgb";
1123
+ const allLines = {
1124
+ light: [],
1125
+ dark: [],
1126
+ lightContrast: [],
1127
+ darkContrast: []
1128
+ };
1129
+ for (const [themeName, theme] of Object.entries(themes)) {
1130
+ const resolved = theme.resolve();
1131
+ let prefix = "";
1132
+ if (options?.prefix === true) prefix = `${themeName}-`;
1133
+ else if (typeof options?.prefix === "object" && options.prefix !== null) prefix = options.prefix[themeName] ?? `${themeName}-`;
1134
+ const css = buildCssMap(resolved, prefix, suffix, format);
1135
+ for (const key of [
1136
+ "light",
1137
+ "dark",
1138
+ "lightContrast",
1139
+ "darkContrast"
1140
+ ]) if (css[key]) allLines[key].push(css[key]);
1141
+ }
1142
+ return {
1143
+ light: allLines.light.join("\n"),
1144
+ dark: allLines.dark.join("\n"),
1145
+ lightContrast: allLines.lightContrast.join("\n"),
1146
+ darkContrast: allLines.darkContrast.join("\n")
1147
+ };
1062
1148
  }
1063
1149
  };
1064
1150
  }
1065
1151
  function createColorToken(input) {
1066
1152
  const defs = { __color__: {
1067
- l: input.l,
1068
- sat: input.sat,
1153
+ lightness: input.lightness,
1154
+ saturation: input.saturationFactor,
1069
1155
  mode: input.mode
1070
1156
  } };
1071
1157
  return {