@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/README.md +165 -73
- package/dist/index.cjs +129 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -13
- package/dist/index.d.mts +45 -13
- package/dist/index.mjs +129 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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 `
|
|
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,
|
|
637
|
-
const target = resolveMinContrast(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
768
|
-
|
|
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.
|
|
795
|
+
const rawL = def.lightness;
|
|
772
796
|
return {
|
|
773
|
-
lightL: clamp(isHighContrast ? pairHC(rawL) : pairNormal(rawL), 0, 100),
|
|
774
|
-
|
|
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
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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:
|
|
851
|
+
hue: effectiveHue,
|
|
820
852
|
saturation: effectiveSat,
|
|
821
853
|
preferredLightness: preferredL / 100,
|
|
822
854
|
baseLinearRgb,
|
|
823
|
-
|
|
855
|
+
contrast: minCr
|
|
824
856
|
}).lightness * 100,
|
|
825
|
-
|
|
857
|
+
satFactor
|
|
826
858
|
};
|
|
827
859
|
}
|
|
828
860
|
return {
|
|
829
861
|
l: clamp(preferredL, 0, 100),
|
|
830
|
-
|
|
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.
|
|
867
|
+
const isRoot = isAbsoluteLightness(def.lightness) && !def.base;
|
|
868
|
+
const effectiveHue = resolveEffectiveHue(ctx.hue, def.hue);
|
|
836
869
|
let lightL;
|
|
837
|
-
let
|
|
870
|
+
let satFactor;
|
|
838
871
|
if (isRoot) {
|
|
839
872
|
const root = resolveRootColor(name, def, ctx, isHighContrast);
|
|
840
873
|
lightL = root.lightL;
|
|
841
|
-
|
|
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
|
-
|
|
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(
|
|
884
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
852
885
|
} else if (isDark && !isRoot) {
|
|
853
886
|
finalL = lightL;
|
|
854
|
-
finalSat = mapSaturationDark(
|
|
887
|
+
finalSat = mapSaturationDark(satFactor * ctx.saturation / 100, mode);
|
|
855
888
|
} else {
|
|
856
889
|
finalL = lightL;
|
|
857
|
-
finalSat =
|
|
890
|
+
finalSat = satFactor * ctx.saturation / 100;
|
|
858
891
|
}
|
|
859
892
|
return {
|
|
860
|
-
h:
|
|
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
|
-
|
|
1068
|
-
|
|
1153
|
+
lightness: input.lightness,
|
|
1154
|
+
saturation: input.saturationFactor,
|
|
1069
1155
|
mode: input.mode
|
|
1070
1156
|
} };
|
|
1071
1157
|
return {
|