@tenphi/glaze 0.11.0 → 0.12.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 +19 -1390
- package/dist/index.cjs +861 -672
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -41
- package/dist/index.d.mts +115 -41
- package/dist/index.mjs +861 -672
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +1080 -0
- package/docs/methodology.md +336 -0
- package/docs/migration.md +237 -0
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -558,6 +558,118 @@ function formatOklch(h, s, l) {
|
|
|
558
558
|
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/config.ts
|
|
563
|
+
/**
|
|
564
|
+
* Build a fresh defaults object. Called from module init and from
|
|
565
|
+
* `resetConfig()` so the two paths can't drift.
|
|
566
|
+
*/
|
|
567
|
+
function defaultConfig() {
|
|
568
|
+
return {
|
|
569
|
+
lightLightness: [10, 100],
|
|
570
|
+
darkLightness: [15, 95],
|
|
571
|
+
darkDesaturation: .1,
|
|
572
|
+
darkCurve: .5,
|
|
573
|
+
states: {
|
|
574
|
+
dark: "@dark",
|
|
575
|
+
highContrast: "@high-contrast"
|
|
576
|
+
},
|
|
577
|
+
modes: {
|
|
578
|
+
dark: true,
|
|
579
|
+
highContrast: false
|
|
580
|
+
},
|
|
581
|
+
autoFlip: true
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
let globalConfig = defaultConfig();
|
|
585
|
+
/**
|
|
586
|
+
* Monotonic counter incremented on every `configure()` / `resetConfig()`
|
|
587
|
+
* call. Theme / palette caches read this to invalidate stale resolve
|
|
588
|
+
* results when the config changes between exports.
|
|
589
|
+
*/
|
|
590
|
+
let configVersion = 0;
|
|
591
|
+
/** Live reference to the current config. Mutated by `configure()` / `resetConfig()`. */
|
|
592
|
+
function getConfig() {
|
|
593
|
+
return globalConfig;
|
|
594
|
+
}
|
|
595
|
+
function getConfigVersion() {
|
|
596
|
+
return configVersion;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Public-facing snapshot used by `glaze.getConfig()`. Returns a shallow
|
|
600
|
+
* copy so callers can't mutate the live config.
|
|
601
|
+
*/
|
|
602
|
+
function snapshotConfig() {
|
|
603
|
+
return { ...globalConfig };
|
|
604
|
+
}
|
|
605
|
+
function configure(config) {
|
|
606
|
+
configVersion++;
|
|
607
|
+
globalConfig = {
|
|
608
|
+
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
609
|
+
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
610
|
+
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
611
|
+
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
612
|
+
states: {
|
|
613
|
+
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
614
|
+
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
615
|
+
},
|
|
616
|
+
modes: {
|
|
617
|
+
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
618
|
+
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
619
|
+
},
|
|
620
|
+
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
621
|
+
autoFlip: config.autoFlip ?? globalConfig.autoFlip
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function resetConfig() {
|
|
625
|
+
configVersion++;
|
|
626
|
+
globalConfig = defaultConfig();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/hc-pair.ts
|
|
631
|
+
function pairNormal(p) {
|
|
632
|
+
return Array.isArray(p) ? p[0] : p;
|
|
633
|
+
}
|
|
634
|
+
function pairHC(p) {
|
|
635
|
+
return Array.isArray(p) ? p[1] : p;
|
|
636
|
+
}
|
|
637
|
+
function clamp(v, min, max) {
|
|
638
|
+
return Math.max(min, Math.min(max, v));
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Parse a value that can be absolute (number) or relative (signed string).
|
|
642
|
+
* Returns the numeric value and whether it's relative.
|
|
643
|
+
*/
|
|
644
|
+
function parseRelativeOrAbsolute(value) {
|
|
645
|
+
if (typeof value === "number") return {
|
|
646
|
+
value,
|
|
647
|
+
relative: false
|
|
648
|
+
};
|
|
649
|
+
return {
|
|
650
|
+
value: parseFloat(value),
|
|
651
|
+
relative: true
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Compute the effective hue for a color, given the theme seed hue
|
|
656
|
+
* and an optional per-color hue override.
|
|
657
|
+
*/
|
|
658
|
+
function resolveEffectiveHue(seedHue, defHue) {
|
|
659
|
+
if (defHue === void 0) return seedHue;
|
|
660
|
+
const parsed = parseRelativeOrAbsolute(defHue);
|
|
661
|
+
if (parsed.relative) return ((seedHue + parsed.value) % 360 + 360) % 360;
|
|
662
|
+
return (parsed.value % 360 + 360) % 360;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Check whether a lightness value represents an absolute root definition
|
|
666
|
+
* (i.e. a number, not a relative string).
|
|
667
|
+
*/
|
|
668
|
+
function isAbsoluteLightness(lightness) {
|
|
669
|
+
if (lightness === void 0) return false;
|
|
670
|
+
return typeof (Array.isArray(lightness) ? lightness[0] : lightness) === "number";
|
|
671
|
+
}
|
|
672
|
+
|
|
561
673
|
//#endregion
|
|
562
674
|
//#region src/contrast-solver.ts
|
|
563
675
|
/**
|
|
@@ -715,47 +827,64 @@ function findLightnessForContrast(options) {
|
|
|
715
827
|
branch: "preferred"
|
|
716
828
|
};
|
|
717
829
|
const [minL, maxL] = lightnessRange;
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
725
|
-
if (Math.abs(darkerResult.lightness - preferredLightness) <= Math.abs(lighterResult.lightness - preferredLightness)) return {
|
|
726
|
-
...darkerResult,
|
|
727
|
-
branch: "darker"
|
|
728
|
-
};
|
|
729
|
-
return {
|
|
730
|
-
...lighterResult,
|
|
731
|
-
branch: "lighter"
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
if (darkerPasses) return {
|
|
735
|
-
...darkerResult,
|
|
736
|
-
branch: "darker"
|
|
737
|
-
};
|
|
738
|
-
if (lighterPasses) return {
|
|
739
|
-
...lighterResult,
|
|
740
|
-
branch: "lighter"
|
|
741
|
-
};
|
|
742
|
-
const candidates = [];
|
|
743
|
-
if (darkerResult) candidates.push({
|
|
744
|
-
...darkerResult,
|
|
745
|
-
branch: "darker"
|
|
746
|
-
});
|
|
747
|
-
if (lighterResult) candidates.push({
|
|
748
|
-
...lighterResult,
|
|
749
|
-
branch: "lighter"
|
|
750
|
-
});
|
|
751
|
-
if (candidates.length === 0) return {
|
|
830
|
+
const canDarker = preferredLightness > minL;
|
|
831
|
+
const canLighter = preferredLightness < maxL;
|
|
832
|
+
let initialIsDarker;
|
|
833
|
+
if (options.initialDirection !== void 0) initialIsDarker = options.initialDirection === "darker";
|
|
834
|
+
else if (canDarker && !canLighter) initialIsDarker = true;
|
|
835
|
+
else if (!canDarker && canLighter) initialIsDarker = false;
|
|
836
|
+
else if (!canDarker && !canLighter) return {
|
|
752
837
|
lightness: preferredLightness,
|
|
753
838
|
contrast: crPref,
|
|
754
839
|
met: false,
|
|
755
840
|
branch: "preferred"
|
|
756
841
|
};
|
|
757
|
-
|
|
758
|
-
|
|
842
|
+
else {
|
|
843
|
+
const yMinExt = cachedLuminance(hue, saturation, minL);
|
|
844
|
+
const yMaxExt = cachedLuminance(hue, saturation, maxL);
|
|
845
|
+
initialIsDarker = contrastRatioFromLuminance(yMinExt, yBase) >= contrastRatioFromLuminance(yMaxExt, yBase);
|
|
846
|
+
}
|
|
847
|
+
const searchInitial = () => initialIsDarker ? searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
848
|
+
const searchOpposite = () => initialIsDarker ? searchBranch(hue, saturation, preferredLightness, maxL, yBase, searchTarget, epsilon, maxIterations, preferredLightness) : searchBranch(hue, saturation, minL, preferredLightness, yBase, searchTarget, epsilon, maxIterations, preferredLightness);
|
|
849
|
+
const initialBranchName = initialIsDarker ? "darker" : "lighter";
|
|
850
|
+
const oppositeBranchName = initialIsDarker ? "lighter" : "darker";
|
|
851
|
+
const initialResult = searchInitial();
|
|
852
|
+
initialResult.met = initialResult.contrast >= target;
|
|
853
|
+
if (initialResult.met && !options.flip) return {
|
|
854
|
+
...initialResult,
|
|
855
|
+
branch: initialBranchName
|
|
856
|
+
};
|
|
857
|
+
if (options.flip) {
|
|
858
|
+
const oppositeResult = (initialIsDarker ? canLighter : canDarker) ? searchOpposite() : null;
|
|
859
|
+
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
860
|
+
if (initialResult.met && oppositeResult?.met) {
|
|
861
|
+
if (Math.abs(initialResult.lightness - preferredLightness) <= Math.abs(oppositeResult.lightness - preferredLightness)) return {
|
|
862
|
+
...initialResult,
|
|
863
|
+
branch: initialBranchName
|
|
864
|
+
};
|
|
865
|
+
return {
|
|
866
|
+
...oppositeResult,
|
|
867
|
+
branch: oppositeBranchName,
|
|
868
|
+
flipped: true
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
if (initialResult.met) return {
|
|
872
|
+
...initialResult,
|
|
873
|
+
branch: initialBranchName
|
|
874
|
+
};
|
|
875
|
+
if (oppositeResult?.met) return {
|
|
876
|
+
...oppositeResult,
|
|
877
|
+
branch: oppositeBranchName,
|
|
878
|
+
flipped: true
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const extreme = initialIsDarker ? minL : maxL;
|
|
882
|
+
return {
|
|
883
|
+
lightness: extreme,
|
|
884
|
+
contrast: contrastRatioFromLuminance(cachedLuminance(hue, saturation, extreme), yBase),
|
|
885
|
+
met: false,
|
|
886
|
+
branch: initialBranchName
|
|
887
|
+
};
|
|
759
888
|
}
|
|
760
889
|
/**
|
|
761
890
|
* Binary-search one branch [lo, hi] for the nearest passing mix value
|
|
@@ -837,167 +966,72 @@ function findValueForMixContrast(options) {
|
|
|
837
966
|
contrast: crPref,
|
|
838
967
|
met: true
|
|
839
968
|
};
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
if (
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (darkerPasses && lighterPasses) {
|
|
847
|
-
if (Math.abs(darkerResult.lightness - preferredValue) <= Math.abs(lighterResult.lightness - preferredValue)) return {
|
|
848
|
-
value: darkerResult.lightness,
|
|
849
|
-
contrast: darkerResult.contrast,
|
|
850
|
-
met: true
|
|
851
|
-
};
|
|
852
|
-
return {
|
|
853
|
-
value: lighterResult.lightness,
|
|
854
|
-
contrast: lighterResult.contrast,
|
|
855
|
-
met: true
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
if (darkerPasses) return {
|
|
859
|
-
value: darkerResult.lightness,
|
|
860
|
-
contrast: darkerResult.contrast,
|
|
861
|
-
met: true
|
|
862
|
-
};
|
|
863
|
-
if (lighterPasses) return {
|
|
864
|
-
value: lighterResult.lightness,
|
|
865
|
-
contrast: lighterResult.contrast,
|
|
866
|
-
met: true
|
|
867
|
-
};
|
|
868
|
-
const candidates = [];
|
|
869
|
-
if (darkerResult) candidates.push({
|
|
870
|
-
...darkerResult,
|
|
871
|
-
branch: "lower"
|
|
872
|
-
});
|
|
873
|
-
if (lighterResult) candidates.push({
|
|
874
|
-
...lighterResult,
|
|
875
|
-
branch: "upper"
|
|
876
|
-
});
|
|
877
|
-
if (candidates.length === 0) return {
|
|
969
|
+
const canLower = preferredValue > 0;
|
|
970
|
+
const canUpper = preferredValue < 1;
|
|
971
|
+
let initialIsLower;
|
|
972
|
+
if (canLower && !canUpper) initialIsLower = true;
|
|
973
|
+
else if (!canLower && canUpper) initialIsLower = false;
|
|
974
|
+
else if (!canLower && !canUpper) return {
|
|
878
975
|
value: preferredValue,
|
|
879
976
|
contrast: crPref,
|
|
880
977
|
met: false
|
|
881
978
|
};
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
979
|
+
else initialIsLower = contrastRatioFromLuminance(luminanceAtValue(0), yBase) >= contrastRatioFromLuminance(luminanceAtValue(1), yBase);
|
|
980
|
+
const searchInitial = () => initialIsLower ? searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
|
|
981
|
+
const searchOpposite = () => initialIsLower ? searchMixBranch(preferredValue, 1, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue) : searchMixBranch(0, preferredValue, yBase, searchTarget, epsilon, maxIterations, preferredValue, luminanceAtValue);
|
|
982
|
+
const initialResult = searchInitial();
|
|
983
|
+
initialResult.met = initialResult.contrast >= target;
|
|
984
|
+
if (initialResult.met && !options.flip) return {
|
|
985
|
+
value: initialResult.lightness,
|
|
986
|
+
contrast: initialResult.contrast,
|
|
987
|
+
met: true
|
|
887
988
|
};
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
* Möbius-invert to totally-white in dark mode. Object / tuple /
|
|
915
|
-
* structured inputs snapshot both windows from `globalConfig` verbatim
|
|
916
|
-
* so they behave like an ordinary theme color (auto-adapted on both
|
|
917
|
-
* sides).
|
|
918
|
-
*/
|
|
919
|
-
function defaultStandaloneScaling(isString) {
|
|
920
|
-
if (isString) {
|
|
921
|
-
const [darkLo] = globalConfig.darkLightness;
|
|
922
|
-
return {
|
|
923
|
-
lightLightness: false,
|
|
924
|
-
darkLightness: [darkLo, 100]
|
|
989
|
+
if (options.flip) {
|
|
990
|
+
const oppositeResult = (initialIsLower ? canUpper : canLower) ? searchOpposite() : null;
|
|
991
|
+
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
992
|
+
if (initialResult.met && oppositeResult?.met) {
|
|
993
|
+
if (Math.abs(initialResult.lightness - preferredValue) <= Math.abs(oppositeResult.lightness - preferredValue)) return {
|
|
994
|
+
value: initialResult.lightness,
|
|
995
|
+
contrast: initialResult.contrast,
|
|
996
|
+
met: true
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
value: oppositeResult.lightness,
|
|
1000
|
+
contrast: oppositeResult.contrast,
|
|
1001
|
+
met: true,
|
|
1002
|
+
flipped: true
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (initialResult.met) return {
|
|
1006
|
+
value: initialResult.lightness,
|
|
1007
|
+
contrast: initialResult.contrast,
|
|
1008
|
+
met: true
|
|
1009
|
+
};
|
|
1010
|
+
if (oppositeResult?.met) return {
|
|
1011
|
+
value: oppositeResult.lightness,
|
|
1012
|
+
contrast: oppositeResult.contrast,
|
|
1013
|
+
met: true,
|
|
1014
|
+
flipped: true
|
|
925
1015
|
};
|
|
926
1016
|
}
|
|
1017
|
+
const extreme = initialIsLower ? 0 : 1;
|
|
927
1018
|
return {
|
|
928
|
-
|
|
929
|
-
|
|
1019
|
+
value: extreme,
|
|
1020
|
+
contrast: contrastRatioFromLuminance(luminanceAtValue(extreme), yBase),
|
|
1021
|
+
met: false
|
|
930
1022
|
};
|
|
931
1023
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
STANDALONE_SEED,
|
|
936
|
-
STANDALONE_BASE
|
|
937
|
-
]);
|
|
938
|
-
/**
|
|
939
|
-
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
940
|
-
* Used to widen `base?` so it accepts either a token reference or a
|
|
941
|
-
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
942
|
-
*/
|
|
943
|
-
function isGlazeColorToken(candidate) {
|
|
944
|
-
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
945
|
-
}
|
|
946
|
-
let globalConfig = {
|
|
947
|
-
lightLightness: [10, 100],
|
|
948
|
-
darkLightness: [15, 95],
|
|
949
|
-
darkDesaturation: .1,
|
|
950
|
-
darkCurve: .5,
|
|
951
|
-
states: {
|
|
952
|
-
dark: "@dark",
|
|
953
|
-
highContrast: "@high-contrast"
|
|
954
|
-
},
|
|
955
|
-
modes: {
|
|
956
|
-
dark: true,
|
|
957
|
-
highContrast: false
|
|
958
|
-
}
|
|
959
|
-
};
|
|
960
|
-
function pairNormal(p) {
|
|
961
|
-
return Array.isArray(p) ? p[0] : p;
|
|
962
|
-
}
|
|
963
|
-
function pairHC(p) {
|
|
964
|
-
return Array.isArray(p) ? p[1] : p;
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Dedupe contrast warnings within a single process. The cache survives
|
|
968
|
-
* the lifetime of a token because tokens memoize their resolution; the
|
|
969
|
-
* limit is a soft cap to keep noise bounded across long-lived sessions
|
|
970
|
-
* (e.g. dev servers with HMR re-resolving themes repeatedly).
|
|
971
|
-
*/
|
|
972
|
-
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
973
|
-
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
974
|
-
function schemeLabel(isDark, isHighContrast) {
|
|
975
|
-
if (isDark && isHighContrast) return "darkContrast";
|
|
976
|
-
if (isDark) return "dark";
|
|
977
|
-
if (isHighContrast) return "lightContrast";
|
|
978
|
-
return "light";
|
|
979
|
-
}
|
|
980
|
-
function formatContrastTarget(input, ratio) {
|
|
981
|
-
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
982
|
-
}
|
|
1024
|
+
|
|
1025
|
+
//#endregion
|
|
1026
|
+
//#region src/shadow.ts
|
|
983
1027
|
/**
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
1028
|
+
* Shadow color computation.
|
|
1029
|
+
*
|
|
1030
|
+
* Owns the shadow / mix def predicates, default tuning constants, the
|
|
1031
|
+
* tuning merge, and the actual `computeShadow` math (hue blend,
|
|
1032
|
+
* saturation cap, lightness clamp, alpha curve). The resolver consumes
|
|
1033
|
+
* this module per scheme variant.
|
|
989
1034
|
*/
|
|
990
|
-
const CONTRAST_WARN_SLACK = .98;
|
|
991
|
-
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
992
|
-
const targetRatio = resolveMinContrast(target);
|
|
993
|
-
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
994
|
-
const scheme = schemeLabel(isDark, isHighContrast);
|
|
995
|
-
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
996
|
-
if (contrastWarnCache.has(key)) return;
|
|
997
|
-
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
998
|
-
contrastWarnCache.add(key);
|
|
999
|
-
console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
|
|
1000
|
-
}
|
|
1001
1035
|
function isShadowDef(def) {
|
|
1002
1036
|
return def.type === "shadow";
|
|
1003
1037
|
}
|
|
@@ -1014,11 +1048,12 @@ const DEFAULT_SHADOW_TUNING = {
|
|
|
1014
1048
|
bgHueBlend: .2
|
|
1015
1049
|
};
|
|
1016
1050
|
function resolveShadowTuning(perColor) {
|
|
1051
|
+
const globalTuning = getConfig().shadowTuning;
|
|
1017
1052
|
return {
|
|
1018
1053
|
...DEFAULT_SHADOW_TUNING,
|
|
1019
|
-
...
|
|
1054
|
+
...globalTuning,
|
|
1020
1055
|
...perColor,
|
|
1021
|
-
lightnessBounds: perColor?.lightnessBounds ??
|
|
1056
|
+
lightnessBounds: perColor?.lightnessBounds ?? globalTuning?.lightnessBounds ?? DEFAULT_SHADOW_TUNING.lightnessBounds
|
|
1022
1057
|
};
|
|
1023
1058
|
}
|
|
1024
1059
|
function circularLerp(a, b, t) {
|
|
@@ -1059,6 +1094,80 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1059
1094
|
alpha
|
|
1060
1095
|
};
|
|
1061
1096
|
}
|
|
1097
|
+
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/scheme-mapping.ts
|
|
1100
|
+
/**
|
|
1101
|
+
* Light / dark scheme lightness mappings.
|
|
1102
|
+
*
|
|
1103
|
+
* Owns the active lightness window selection (with per-call scaling
|
|
1104
|
+
* overrides and high-contrast handling), the Möbius curve used by the
|
|
1105
|
+
* `'auto'` dark adaptation, and the saturation-desaturation reducer
|
|
1106
|
+
* for dark mode.
|
|
1107
|
+
*/
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolve the active lightness window for a scheme.
|
|
1110
|
+
* - HC variants always return `[0, 100]` (existing behavior, predates per-call overrides).
|
|
1111
|
+
* - Otherwise, per-call `scaling` (e.g. from `glaze.color()`'s third arg) wins;
|
|
1112
|
+
* `false` is interpreted as `[0, 100]` (no remap). Falls back to `globalConfig.*Lightness`.
|
|
1113
|
+
*/
|
|
1114
|
+
function lightnessWindow(isHighContrast, kind, scaling) {
|
|
1115
|
+
if (isHighContrast) return [0, 100];
|
|
1116
|
+
if (scaling) {
|
|
1117
|
+
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1118
|
+
if (override === false) return [0, 100];
|
|
1119
|
+
if (override !== void 0) return override;
|
|
1120
|
+
}
|
|
1121
|
+
const cfg = getConfig();
|
|
1122
|
+
return kind === "dark" ? cfg.darkLightness : cfg.lightLightness;
|
|
1123
|
+
}
|
|
1124
|
+
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
1125
|
+
if (mode === "static") return l;
|
|
1126
|
+
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1127
|
+
return l * (hi - lo) / 100 + lo;
|
|
1128
|
+
}
|
|
1129
|
+
function mobiusCurve(t, beta) {
|
|
1130
|
+
if (beta >= 1) return t;
|
|
1131
|
+
return t / (t + beta * (1 - t));
|
|
1132
|
+
}
|
|
1133
|
+
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
1134
|
+
if (mode === "static") return l;
|
|
1135
|
+
const cfg = getConfig();
|
|
1136
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1137
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1138
|
+
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1139
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1140
|
+
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1141
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1142
|
+
}
|
|
1143
|
+
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1144
|
+
const cfg = getConfig();
|
|
1145
|
+
const beta = isHighContrast ? pairHC(cfg.darkCurve) : pairNormal(cfg.darkCurve);
|
|
1146
|
+
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1147
|
+
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1148
|
+
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1149
|
+
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1150
|
+
}
|
|
1151
|
+
function mapSaturationDark(s, mode) {
|
|
1152
|
+
if (mode === "static") return s;
|
|
1153
|
+
return s * (1 - getConfig().darkDesaturation);
|
|
1154
|
+
}
|
|
1155
|
+
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1156
|
+
if (mode === "static") return [0, 1];
|
|
1157
|
+
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1158
|
+
return [lo / 100, hi / 100];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/validation.ts
|
|
1163
|
+
/**
|
|
1164
|
+
* Color graph validation and topological sort.
|
|
1165
|
+
*
|
|
1166
|
+
* `validateColorDefs` rejects bad references (missing / shadow-referencing /
|
|
1167
|
+
* base/contrast/lightness mismatches) and detects cycles before the
|
|
1168
|
+
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
1169
|
+
* its base / bg / fg / target dependencies.
|
|
1170
|
+
*/
|
|
1062
1171
|
function validateColorDefs(defs, externalBases) {
|
|
1063
1172
|
const localNames = new Set(Object.keys(defs));
|
|
1064
1173
|
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
@@ -1133,89 +1242,62 @@ function topoSort(defs) {
|
|
|
1133
1242
|
for (const name of Object.keys(defs)) visit(name);
|
|
1134
1243
|
return result;
|
|
1135
1244
|
}
|
|
1245
|
+
|
|
1246
|
+
//#endregion
|
|
1247
|
+
//#region src/warnings.ts
|
|
1136
1248
|
/**
|
|
1137
|
-
*
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1249
|
+
* Contrast-warning dispatcher.
|
|
1250
|
+
*
|
|
1251
|
+
* Tokens memoize their resolution, but a long-lived process (e.g. a dev
|
|
1252
|
+
* server with HMR) can re-resolve the same theme many times. The cache
|
|
1253
|
+
* here dedupes warnings within a session with a soft cap to keep noise
|
|
1254
|
+
* bounded.
|
|
1141
1255
|
*/
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
if (scaling) {
|
|
1145
|
-
const override = kind === "dark" ? scaling.darkLightness : scaling.lightLightness;
|
|
1146
|
-
if (override === false) return [0, 100];
|
|
1147
|
-
if (override !== void 0) return override;
|
|
1148
|
-
}
|
|
1149
|
-
return kind === "dark" ? globalConfig.darkLightness : globalConfig.lightLightness;
|
|
1150
|
-
}
|
|
1151
|
-
function mapLightnessLight(l, mode, isHighContrast, scaling) {
|
|
1152
|
-
if (mode === "static") return l;
|
|
1153
|
-
const [lo, hi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1154
|
-
return l * (hi - lo) / 100 + lo;
|
|
1155
|
-
}
|
|
1156
|
-
function mobiusCurve(t, beta) {
|
|
1157
|
-
if (beta >= 1) return t;
|
|
1158
|
-
return t / (t + beta * (1 - t));
|
|
1159
|
-
}
|
|
1160
|
-
function mapLightnessDark(l, mode, isHighContrast, scaling) {
|
|
1161
|
-
if (mode === "static") return l;
|
|
1162
|
-
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1163
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1164
|
-
if (mode === "fixed") return l * (darkHi - darkLo) / 100 + darkLo;
|
|
1165
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1166
|
-
const t = (lightHi - (l * (lightHi - lightLo) / 100 + lightLo)) / (lightHi - lightLo);
|
|
1167
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1168
|
-
}
|
|
1169
|
-
function lightMappedToDark(lightL, isHighContrast, scaling) {
|
|
1170
|
-
const beta = isHighContrast ? pairHC(globalConfig.darkCurve) : pairNormal(globalConfig.darkCurve);
|
|
1171
|
-
const [lightLo, lightHi] = lightnessWindow(isHighContrast, "light", scaling);
|
|
1172
|
-
const [darkLo, darkHi] = lightnessWindow(isHighContrast, "dark", scaling);
|
|
1173
|
-
const t = (lightHi - clamp(lightL, lightLo, lightHi)) / (lightHi - lightLo);
|
|
1174
|
-
return darkLo + (darkHi - darkLo) * mobiusCurve(t, beta);
|
|
1175
|
-
}
|
|
1176
|
-
function mapSaturationDark(s, mode) {
|
|
1177
|
-
if (mode === "static") return s;
|
|
1178
|
-
return s * (1 - globalConfig.darkDesaturation);
|
|
1179
|
-
}
|
|
1180
|
-
function schemeLightnessRange(isDark, mode, isHighContrast, scaling) {
|
|
1181
|
-
if (mode === "static") return [0, 1];
|
|
1182
|
-
const [lo, hi] = lightnessWindow(isHighContrast, isDark ? "dark" : "light", scaling);
|
|
1183
|
-
return [lo / 100, hi / 100];
|
|
1184
|
-
}
|
|
1185
|
-
function clamp(v, min, max) {
|
|
1186
|
-
return Math.max(min, Math.min(max, v));
|
|
1187
|
-
}
|
|
1256
|
+
const CONTRAST_WARN_CACHE_LIMIT = 256;
|
|
1257
|
+
const contrastWarnCache = /* @__PURE__ */ new Set();
|
|
1188
1258
|
/**
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1259
|
+
* Slack factor below the requested target before we emit a warning.
|
|
1260
|
+
* The contrast solver already overshoots by `OVERSHOOT` (currently 1%)
|
|
1261
|
+
* to absorb rounding noise (`see findLightnessForContrast` in
|
|
1262
|
+
* `contrast-solver.ts`), so an `actual` ratio within ~2x that overshoot
|
|
1263
|
+
* is effectively a pass and not worth nagging the user about.
|
|
1191
1264
|
*/
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
return
|
|
1198
|
-
value: parseFloat(value),
|
|
1199
|
-
relative: true
|
|
1200
|
-
};
|
|
1265
|
+
const CONTRAST_WARN_SLACK = .98;
|
|
1266
|
+
function schemeLabel(isDark, isHighContrast) {
|
|
1267
|
+
if (isDark && isHighContrast) return "darkContrast";
|
|
1268
|
+
if (isDark) return "dark";
|
|
1269
|
+
if (isHighContrast) return "lightContrast";
|
|
1270
|
+
return "light";
|
|
1201
1271
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
if (
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1272
|
+
function formatContrastTarget(input, ratio) {
|
|
1273
|
+
return typeof input === "string" ? `"${input}" (${ratio.toFixed(2)})` : ratio.toFixed(2);
|
|
1274
|
+
}
|
|
1275
|
+
function warnContrastUnmet(name, isDark, isHighContrast, target, actual) {
|
|
1276
|
+
const targetRatio = resolveMinContrast(target);
|
|
1277
|
+
if (actual >= targetRatio * CONTRAST_WARN_SLACK) return;
|
|
1278
|
+
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1279
|
+
const key = `${name}|${scheme}|${targetRatio.toFixed(3)}|${actual.toFixed(2)}`;
|
|
1280
|
+
if (contrastWarnCache.has(key)) return;
|
|
1281
|
+
if (contrastWarnCache.size >= CONTRAST_WARN_CACHE_LIMIT) contrastWarnCache.clear();
|
|
1282
|
+
contrastWarnCache.add(key);
|
|
1283
|
+
console.warn(`glaze: color "${name}" cannot meet contrast ${formatContrastTarget(target, targetRatio)} in ${scheme} scheme (got ${actual.toFixed(2)}). Try widening the lightness window, lowering the contrast target, or picking a base color further from this color's lightness.`);
|
|
1211
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/resolver.ts
|
|
1212
1288
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1289
|
+
* Color resolution engine.
|
|
1290
|
+
*
|
|
1291
|
+
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
1292
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name.
|
|
1293
|
+
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1294
|
+
* color defs.
|
|
1215
1295
|
*/
|
|
1216
|
-
function
|
|
1217
|
-
if (
|
|
1218
|
-
|
|
1296
|
+
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1297
|
+
if (isDark && isHighContrast) return color.darkContrast;
|
|
1298
|
+
if (isDark) return color.dark;
|
|
1299
|
+
if (isHighContrast) return color.lightContrast;
|
|
1300
|
+
return color.light;
|
|
1219
1301
|
}
|
|
1220
1302
|
function resolveRootColor(_name, def, _ctx, isHighContrast) {
|
|
1221
1303
|
const rawL = def.lightness;
|
|
@@ -1250,13 +1332,19 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1250
1332
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode) : satFactor * ctx.saturation / 100;
|
|
1251
1333
|
const baseLinearRgb = okhslToLinearSrgb(baseVariant.h, baseVariant.s, baseVariant.l);
|
|
1252
1334
|
const windowRange = schemeLightnessRange(isDark, mode, isHighContrast, ctx.scaling);
|
|
1335
|
+
const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
|
|
1336
|
+
let initialDirection;
|
|
1337
|
+
if (preferredL < baseL) initialDirection = "darker";
|
|
1338
|
+
else if (preferredL > baseL) initialDirection = "lighter";
|
|
1253
1339
|
const result = findLightnessForContrast({
|
|
1254
1340
|
hue: effectiveHue,
|
|
1255
1341
|
saturation: effectiveSat,
|
|
1256
1342
|
preferredLightness: clamp(preferredL / 100, windowRange[0], windowRange[1]),
|
|
1257
1343
|
baseLinearRgb,
|
|
1258
1344
|
contrast: minCr,
|
|
1259
|
-
lightnessRange: [0, 1]
|
|
1345
|
+
lightnessRange: [0, 1],
|
|
1346
|
+
initialDirection,
|
|
1347
|
+
flip: autoFlip
|
|
1260
1348
|
});
|
|
1261
1349
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, minCr, result.contrast);
|
|
1262
1350
|
return {
|
|
@@ -1269,12 +1357,6 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1269
1357
|
satFactor
|
|
1270
1358
|
};
|
|
1271
1359
|
}
|
|
1272
|
-
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
1273
|
-
if (isDark && isHighContrast) return color.darkContrast;
|
|
1274
|
-
if (isDark) return color.dark;
|
|
1275
|
-
if (isHighContrast) return color.lightContrast;
|
|
1276
|
-
return color.light;
|
|
1277
|
-
}
|
|
1278
1360
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1279
1361
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1280
1362
|
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -1378,12 +1460,14 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1378
1460
|
else luminanceAt = (v) => {
|
|
1379
1461
|
return gamutClampedLuminance(okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v));
|
|
1380
1462
|
};
|
|
1463
|
+
const autoFlip = ctx.autoFlip ?? getConfig().autoFlip;
|
|
1381
1464
|
t = findValueForMixContrast({
|
|
1382
1465
|
preferredValue: t,
|
|
1383
1466
|
baseLinearRgb: baseLinear,
|
|
1384
1467
|
targetLinearRgb: targetLinear,
|
|
1385
1468
|
contrast: minCr,
|
|
1386
|
-
luminanceAtValue: luminanceAt
|
|
1469
|
+
luminanceAtValue: luminanceAt,
|
|
1470
|
+
flip: autoFlip
|
|
1387
1471
|
}).value;
|
|
1388
1472
|
}
|
|
1389
1473
|
if (blend === "transparent") return {
|
|
@@ -1400,26 +1484,27 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1400
1484
|
alpha: 1
|
|
1401
1485
|
};
|
|
1402
1486
|
}
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1416
|
-
return def.mode ?? "auto";
|
|
1417
|
-
}
|
|
1418
|
-
const lightMap = /* @__PURE__ */ new Map();
|
|
1487
|
+
function defMode(def) {
|
|
1488
|
+
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
1489
|
+
return def.mode ?? "auto";
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Run a single resolve pass over all local names. Pass 1 lazily creates
|
|
1493
|
+
* each `ResolvedColor` (all four slots seeded with the just-resolved
|
|
1494
|
+
* variant) the first time it sees a name; later passes update the
|
|
1495
|
+
* `target` slot on the existing record.
|
|
1496
|
+
*/
|
|
1497
|
+
function runPass(order, defs, ctx, isDark, isHighContrast, target) {
|
|
1498
|
+
const out = /* @__PURE__ */ new Map();
|
|
1419
1499
|
for (const name of order) {
|
|
1420
|
-
const variant = resolveColorForScheme(name, defs[name], ctx,
|
|
1421
|
-
|
|
1422
|
-
ctx.resolved.
|
|
1500
|
+
const variant = resolveColorForScheme(name, defs[name], ctx, isDark, isHighContrast);
|
|
1501
|
+
out.set(name, variant);
|
|
1502
|
+
const existing = ctx.resolved.get(name);
|
|
1503
|
+
if (existing) ctx.resolved.set(name, {
|
|
1504
|
+
...existing,
|
|
1505
|
+
[target]: variant
|
|
1506
|
+
});
|
|
1507
|
+
else ctx.resolved.set(name, {
|
|
1423
1508
|
name,
|
|
1424
1509
|
light: variant,
|
|
1425
1510
|
dark: variant,
|
|
@@ -1428,49 +1513,42 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1428
1513
|
mode: defMode(defs[name])
|
|
1429
1514
|
});
|
|
1430
1515
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
lightHCMap.set(name, variant);
|
|
1439
|
-
ctx.resolved.set(name, {
|
|
1440
|
-
...ctx.resolved.get(name),
|
|
1441
|
-
lightContrast: variant
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
const darkMap = /* @__PURE__ */ new Map();
|
|
1445
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1446
|
-
name,
|
|
1447
|
-
light: lightMap.get(name),
|
|
1448
|
-
dark: lightMap.get(name),
|
|
1449
|
-
lightContrast: lightHCMap.get(name),
|
|
1450
|
-
darkContrast: lightHCMap.get(name),
|
|
1451
|
-
mode: defMode(defs[name])
|
|
1452
|
-
});
|
|
1453
|
-
for (const name of order) {
|
|
1454
|
-
const variant = resolveColorForScheme(name, defs[name], ctx, true, false);
|
|
1455
|
-
darkMap.set(name, variant);
|
|
1456
|
-
ctx.resolved.set(name, {
|
|
1457
|
-
...ctx.resolved.get(name),
|
|
1458
|
-
dark: variant
|
|
1459
|
-
});
|
|
1460
|
-
}
|
|
1461
|
-
const darkHCMap = /* @__PURE__ */ new Map();
|
|
1462
|
-
for (const name of order) ctx.resolved.set(name, {
|
|
1463
|
-
...ctx.resolved.get(name),
|
|
1464
|
-
darkContrast: darkMap.get(name)
|
|
1465
|
-
});
|
|
1516
|
+
return out;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Re-seed a single variant slot with a previously-resolved map so the
|
|
1520
|
+
* upcoming pass reads sensible fallbacks via `getSchemeVariant`.
|
|
1521
|
+
*/
|
|
1522
|
+
function seedField(order, ctx, field, source) {
|
|
1466
1523
|
for (const name of order) {
|
|
1467
|
-
const
|
|
1468
|
-
darkHCMap.set(name, variant);
|
|
1524
|
+
const existing = ctx.resolved.get(name);
|
|
1469
1525
|
ctx.resolved.set(name, {
|
|
1470
|
-
...
|
|
1471
|
-
|
|
1526
|
+
...existing,
|
|
1527
|
+
[field]: source.get(name)
|
|
1472
1528
|
});
|
|
1473
1529
|
}
|
|
1530
|
+
}
|
|
1531
|
+
function resolveAllColors(hue, saturation, defs, scaling, externalBases, overrideAutoFlip) {
|
|
1532
|
+
validateColorDefs(defs, externalBases);
|
|
1533
|
+
const order = topoSort(defs);
|
|
1534
|
+
const cfg = getConfig();
|
|
1535
|
+
const ctx = {
|
|
1536
|
+
hue,
|
|
1537
|
+
saturation,
|
|
1538
|
+
defs,
|
|
1539
|
+
resolved: /* @__PURE__ */ new Map(),
|
|
1540
|
+
scaling,
|
|
1541
|
+
autoFlip: overrideAutoFlip ?? cfg.autoFlip
|
|
1542
|
+
};
|
|
1543
|
+
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1544
|
+
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
1545
|
+
seedField(order, ctx, "lightContrast", lightMap);
|
|
1546
|
+
const lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
1547
|
+
seedField(order, ctx, "dark", lightMap);
|
|
1548
|
+
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
1549
|
+
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
1550
|
+
seedField(order, ctx, "darkContrast", darkMap);
|
|
1551
|
+
const darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
1474
1552
|
const result = /* @__PURE__ */ new Map();
|
|
1475
1553
|
for (const name of order) result.set(name, {
|
|
1476
1554
|
name,
|
|
@@ -1482,6 +1560,19 @@ function resolveAllColors(hue, saturation, defs, scaling, externalBases) {
|
|
|
1482
1560
|
});
|
|
1483
1561
|
return result;
|
|
1484
1562
|
}
|
|
1563
|
+
|
|
1564
|
+
//#endregion
|
|
1565
|
+
//#region src/formatters.ts
|
|
1566
|
+
/**
|
|
1567
|
+
* Output formatting for resolved color maps.
|
|
1568
|
+
*
|
|
1569
|
+
* Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
|
|
1570
|
+
* `oklch`) and the four token-map shapes Glaze emits:
|
|
1571
|
+
* - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
|
|
1572
|
+
* - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
|
|
1573
|
+
* - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
|
|
1574
|
+
* - `buildCssMap` — CSS custom property declaration strings per variant.
|
|
1575
|
+
*/
|
|
1485
1576
|
const formatters = {
|
|
1486
1577
|
okhsl: formatOkhsl,
|
|
1487
1578
|
rgb: formatRgb,
|
|
@@ -1498,9 +1589,10 @@ function formatVariant(v, format = "okhsl") {
|
|
|
1498
1589
|
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
1499
1590
|
}
|
|
1500
1591
|
function resolveModes(override) {
|
|
1592
|
+
const cfg = getConfig();
|
|
1501
1593
|
return {
|
|
1502
|
-
dark: override?.dark ??
|
|
1503
|
-
highContrast: override?.highContrast ??
|
|
1594
|
+
dark: override?.dark ?? cfg.modes.dark,
|
|
1595
|
+
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
1504
1596
|
};
|
|
1505
1597
|
}
|
|
1506
1598
|
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl") {
|
|
@@ -1561,206 +1653,69 @@ function buildCssMap(resolved, prefix, suffix, format) {
|
|
|
1561
1653
|
darkContrast: lines.darkContrast.join("\n")
|
|
1562
1654
|
};
|
|
1563
1655
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1656
|
+
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/color-token.ts
|
|
1659
|
+
/**
|
|
1660
|
+
* Standalone single-color tokens (`glaze.color()` / `glaze.colorFrom()`).
|
|
1661
|
+
*
|
|
1662
|
+
* Owns the value-shorthand parser (hex, `rgb()` / `hsl()` / `okhsl()` /
|
|
1663
|
+
* `oklch()`, `{ r, g, b }`, `{ h, s, l }`, `{ l, c, h }`), the structured-input
|
|
1664
|
+
* validator, the two factory paths (value vs structured), and the
|
|
1665
|
+
* JSON-safe export / rehydration round-trip.
|
|
1666
|
+
*
|
|
1667
|
+
* Standalone tokens snapshot the relevant `globalConfig` fields at
|
|
1668
|
+
* create time so later `configure()` calls do not retroactively change
|
|
1669
|
+
* exported tokens — the snapshot is captured eagerly in
|
|
1670
|
+
* `defaultStandaloneScaling()`. The token's resolved variants are then
|
|
1671
|
+
* memoized on first `.resolve()` / `.token()` / ... call.
|
|
1672
|
+
*/
|
|
1673
|
+
/** Internal name of the user-facing standalone color in the synthesized def map. */
|
|
1674
|
+
const STANDALONE_VALUE = "value";
|
|
1675
|
+
/** Internal name of the hidden static-anchor seed used for relative lightness / contrast. */
|
|
1676
|
+
const STANDALONE_SEED = "seed";
|
|
1677
|
+
/** Internal name of an externally-resolved `GlazeColorToken` injected as a base reference. */
|
|
1678
|
+
const STANDALONE_BASE = "externalBase";
|
|
1679
|
+
/** Reserved internal names that user-supplied `name` must not collide with. */
|
|
1680
|
+
const RESERVED_STANDALONE_NAMES = new Set([
|
|
1681
|
+
STANDALONE_VALUE,
|
|
1682
|
+
STANDALONE_SEED,
|
|
1683
|
+
STANDALONE_BASE
|
|
1684
|
+
]);
|
|
1685
|
+
/**
|
|
1686
|
+
* Create-time scaling for all value-shorthand `glaze.color()` inputs.
|
|
1687
|
+
* Light lightness is preserved (`lightLightness: false`); dark uses the
|
|
1688
|
+
* theme window from `globalConfig.darkLightness`, snapshotted at create
|
|
1689
|
+
* time so later `configure()` does not retroactively change tokens.
|
|
1690
|
+
*/
|
|
1691
|
+
function defaultValueShorthandScaling() {
|
|
1566
1692
|
return {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
},
|
|
1570
|
-
get saturation() {
|
|
1571
|
-
return saturation;
|
|
1572
|
-
},
|
|
1573
|
-
colors(defs) {
|
|
1574
|
-
colorDefs = {
|
|
1575
|
-
...colorDefs,
|
|
1576
|
-
...defs
|
|
1577
|
-
};
|
|
1578
|
-
},
|
|
1579
|
-
color(name, def) {
|
|
1580
|
-
if (def === void 0) return colorDefs[name];
|
|
1581
|
-
colorDefs[name] = def;
|
|
1582
|
-
},
|
|
1583
|
-
remove(names) {
|
|
1584
|
-
const list = Array.isArray(names) ? names : [names];
|
|
1585
|
-
for (const name of list) delete colorDefs[name];
|
|
1586
|
-
},
|
|
1587
|
-
has(name) {
|
|
1588
|
-
return name in colorDefs;
|
|
1589
|
-
},
|
|
1590
|
-
list() {
|
|
1591
|
-
return Object.keys(colorDefs);
|
|
1592
|
-
},
|
|
1593
|
-
reset() {
|
|
1594
|
-
colorDefs = {};
|
|
1595
|
-
},
|
|
1596
|
-
export() {
|
|
1597
|
-
return {
|
|
1598
|
-
hue,
|
|
1599
|
-
saturation,
|
|
1600
|
-
colors: { ...colorDefs }
|
|
1601
|
-
};
|
|
1602
|
-
},
|
|
1603
|
-
extend(options) {
|
|
1604
|
-
const newHue = options.hue ?? hue;
|
|
1605
|
-
const newSat = options.saturation ?? saturation;
|
|
1606
|
-
const inheritedColors = {};
|
|
1607
|
-
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
1608
|
-
return createTheme(newHue, newSat, options.colors ? {
|
|
1609
|
-
...inheritedColors,
|
|
1610
|
-
...options.colors
|
|
1611
|
-
} : { ...inheritedColors });
|
|
1612
|
-
},
|
|
1613
|
-
resolve() {
|
|
1614
|
-
return resolveAllColors(hue, saturation, colorDefs);
|
|
1615
|
-
},
|
|
1616
|
-
tokens(options) {
|
|
1617
|
-
return buildFlatTokenMap(resolveAllColors(hue, saturation, colorDefs), "", resolveModes(options?.modes), options?.format);
|
|
1618
|
-
},
|
|
1619
|
-
tasty(options) {
|
|
1620
|
-
return buildTokenMap(resolveAllColors(hue, saturation, colorDefs), "", {
|
|
1621
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1622
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1623
|
-
}, resolveModes(options?.modes), options?.format);
|
|
1624
|
-
},
|
|
1625
|
-
json(options) {
|
|
1626
|
-
return buildJsonMap(resolveAllColors(hue, saturation, colorDefs), resolveModes(options?.modes), options?.format);
|
|
1627
|
-
},
|
|
1628
|
-
css(options) {
|
|
1629
|
-
return buildCssMap(resolveAllColors(hue, saturation, colorDefs), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
1630
|
-
}
|
|
1693
|
+
lightLightness: false,
|
|
1694
|
+
darkLightness: getConfig().darkLightness
|
|
1631
1695
|
};
|
|
1632
1696
|
}
|
|
1633
|
-
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
1634
|
-
const prefix = options?.prefix ?? defaultPrefix;
|
|
1635
|
-
if (prefix === true) return `${themeName}-`;
|
|
1636
|
-
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
1637
|
-
return "";
|
|
1638
|
-
}
|
|
1639
|
-
function validatePrimaryTheme(primary, themes) {
|
|
1640
|
-
if (primary !== void 0 && !(primary in themes)) {
|
|
1641
|
-
const available = Object.keys(themes).join(", ");
|
|
1642
|
-
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
1697
|
/**
|
|
1646
|
-
*
|
|
1647
|
-
*
|
|
1698
|
+
* Create-time scaling for structured `glaze.color({ hue, saturation,
|
|
1699
|
+
* lightness, ... })`. Both windows come from `globalConfig` so the
|
|
1700
|
+
* token behaves like an ordinary theme color on light and dark sides.
|
|
1648
1701
|
*/
|
|
1649
|
-
function
|
|
1650
|
-
|
|
1651
|
-
return
|
|
1652
|
-
|
|
1702
|
+
function defaultStructuredScaling() {
|
|
1703
|
+
const cfg = getConfig();
|
|
1704
|
+
return {
|
|
1705
|
+
lightLightness: cfg.lightLightness,
|
|
1706
|
+
darkLightness: cfg.darkLightness
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1653
1709
|
/**
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1710
|
+
* Discriminate a `GlazeColorToken` from a raw `GlazeColorValue`.
|
|
1711
|
+
* Used to widen `base?` so it accepts either a token reference or a
|
|
1712
|
+
* raw value (auto-wrapped into `glaze.color(value)`).
|
|
1657
1713
|
*/
|
|
1658
|
-
function
|
|
1659
|
-
|
|
1660
|
-
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
1661
|
-
for (const [name, color] of resolved) {
|
|
1662
|
-
const key = `${prefix}${name}`;
|
|
1663
|
-
if (seen.has(key)) {
|
|
1664
|
-
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
1665
|
-
continue;
|
|
1666
|
-
}
|
|
1667
|
-
seen.set(key, label);
|
|
1668
|
-
filtered.set(name, color);
|
|
1669
|
-
}
|
|
1670
|
-
return filtered;
|
|
1714
|
+
function isGlazeColorToken(candidate) {
|
|
1715
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
1671
1716
|
}
|
|
1672
|
-
function
|
|
1673
|
-
|
|
1674
|
-
return {
|
|
1675
|
-
tokens(options) {
|
|
1676
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1677
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1678
|
-
const modes = resolveModes(options?.modes);
|
|
1679
|
-
const allTokens = {};
|
|
1680
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1681
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1682
|
-
const resolved = theme.resolve();
|
|
1683
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1684
|
-
const tokens = buildFlatTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, modes, options?.format);
|
|
1685
|
-
for (const variant of Object.keys(tokens)) {
|
|
1686
|
-
if (!allTokens[variant]) allTokens[variant] = {};
|
|
1687
|
-
Object.assign(allTokens[variant], tokens[variant]);
|
|
1688
|
-
}
|
|
1689
|
-
if (themeName === effectivePrimary) {
|
|
1690
|
-
const unprefixed = buildFlatTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", modes, options?.format);
|
|
1691
|
-
for (const variant of Object.keys(unprefixed)) Object.assign(allTokens[variant], unprefixed[variant]);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
return allTokens;
|
|
1695
|
-
},
|
|
1696
|
-
tasty(options) {
|
|
1697
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1698
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1699
|
-
const states = {
|
|
1700
|
-
dark: options?.states?.dark ?? globalConfig.states.dark,
|
|
1701
|
-
highContrast: options?.states?.highContrast ?? globalConfig.states.highContrast
|
|
1702
|
-
};
|
|
1703
|
-
const modes = resolveModes(options?.modes);
|
|
1704
|
-
const allTokens = {};
|
|
1705
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1706
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1707
|
-
const resolved = theme.resolve();
|
|
1708
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1709
|
-
const tokens = buildTokenMap(filterCollisions(resolved, prefix, seen, themeName), prefix, states, modes, options?.format);
|
|
1710
|
-
Object.assign(allTokens, tokens);
|
|
1711
|
-
if (themeName === effectivePrimary) {
|
|
1712
|
-
const unprefixed = buildTokenMap(filterCollisions(resolved, "", seen, themeName, true), "", states, modes, options?.format);
|
|
1713
|
-
Object.assign(allTokens, unprefixed);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
return allTokens;
|
|
1717
|
-
},
|
|
1718
|
-
json(options) {
|
|
1719
|
-
const modes = resolveModes(options?.modes);
|
|
1720
|
-
const result = {};
|
|
1721
|
-
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
1722
|
-
return result;
|
|
1723
|
-
},
|
|
1724
|
-
css(options) {
|
|
1725
|
-
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
1726
|
-
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
1727
|
-
const suffix = options?.suffix ?? "-color";
|
|
1728
|
-
const format = options?.format ?? "rgb";
|
|
1729
|
-
const allLines = {
|
|
1730
|
-
light: [],
|
|
1731
|
-
dark: [],
|
|
1732
|
-
lightContrast: [],
|
|
1733
|
-
darkContrast: []
|
|
1734
|
-
};
|
|
1735
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1736
|
-
for (const [themeName, theme] of Object.entries(themes)) {
|
|
1737
|
-
const resolved = theme.resolve();
|
|
1738
|
-
const prefix = resolvePrefix(options, themeName, true);
|
|
1739
|
-
const css = buildCssMap(filterCollisions(resolved, prefix, seen, themeName), prefix, suffix, format);
|
|
1740
|
-
for (const key of [
|
|
1741
|
-
"light",
|
|
1742
|
-
"dark",
|
|
1743
|
-
"lightContrast",
|
|
1744
|
-
"darkContrast"
|
|
1745
|
-
]) if (css[key]) allLines[key].push(css[key]);
|
|
1746
|
-
if (themeName === effectivePrimary) {
|
|
1747
|
-
const unprefixed = buildCssMap(filterCollisions(resolved, "", seen, themeName, true), "", suffix, format);
|
|
1748
|
-
for (const key of [
|
|
1749
|
-
"light",
|
|
1750
|
-
"dark",
|
|
1751
|
-
"lightContrast",
|
|
1752
|
-
"darkContrast"
|
|
1753
|
-
]) if (unprefixed[key]) allLines[key].push(unprefixed[key]);
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
return {
|
|
1757
|
-
light: allLines.light.join("\n"),
|
|
1758
|
-
dark: allLines.dark.join("\n"),
|
|
1759
|
-
lightContrast: allLines.lightContrast.join("\n"),
|
|
1760
|
-
darkContrast: allLines.darkContrast.join("\n")
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
};
|
|
1717
|
+
function isStructuredColorInput(input) {
|
|
1718
|
+
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
1764
1719
|
}
|
|
1765
1720
|
/**
|
|
1766
1721
|
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
@@ -1879,11 +1834,41 @@ function validateOkhslColor(value) {
|
|
|
1879
1834
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1880
1835
|
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, lightness } (which uses 0–100)?");
|
|
1881
1836
|
}
|
|
1882
|
-
/**
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1837
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1838
|
+
function validateRgbColor(value) {
|
|
1839
|
+
for (const key of [
|
|
1840
|
+
"r",
|
|
1841
|
+
"g",
|
|
1842
|
+
"b"
|
|
1843
|
+
]) {
|
|
1844
|
+
const n = value[key];
|
|
1845
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1849
|
+
function validateOklchColor(value) {
|
|
1850
|
+
const { l, c, h } = value;
|
|
1851
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1852
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1853
|
+
}
|
|
1854
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1855
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1856
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1857
|
+
l,
|
|
1858
|
+
c * Math.cos(hRad),
|
|
1859
|
+
c * Math.sin(hRad)
|
|
1860
|
+
]);
|
|
1861
|
+
return {
|
|
1862
|
+
h,
|
|
1863
|
+
s,
|
|
1864
|
+
l: outL
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
function isRgbColorObject(value) {
|
|
1868
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1869
|
+
}
|
|
1870
|
+
function isOklchColorObject(value) {
|
|
1871
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1887
1872
|
}
|
|
1888
1873
|
/**
|
|
1889
1874
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
@@ -1929,18 +1914,17 @@ function validateStandaloneName(name) {
|
|
|
1929
1914
|
/**
|
|
1930
1915
|
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1931
1916
|
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1932
|
-
*
|
|
1917
|
+
* literal objects) go through one parser.
|
|
1933
1918
|
*/
|
|
1934
1919
|
function extractOkhslFromValue(value) {
|
|
1935
1920
|
if (typeof value === "string") return parseColorString(value);
|
|
1936
|
-
if (Array.isArray(value)) {
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
const [r, g, b] = tuple;
|
|
1921
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1922
|
+
if (isRgbColorObject(value)) {
|
|
1923
|
+
validateRgbColor(value);
|
|
1940
1924
|
const [h, s, l] = srgbToOkhsl([
|
|
1941
|
-
r / 255,
|
|
1942
|
-
g / 255,
|
|
1943
|
-
b / 255
|
|
1925
|
+
value.r / 255,
|
|
1926
|
+
value.g / 255,
|
|
1927
|
+
value.b / 255
|
|
1944
1928
|
]);
|
|
1945
1929
|
return {
|
|
1946
1930
|
h,
|
|
@@ -1948,6 +1932,10 @@ function extractOkhslFromValue(value) {
|
|
|
1948
1932
|
l
|
|
1949
1933
|
};
|
|
1950
1934
|
}
|
|
1935
|
+
if (isOklchColorObject(value)) {
|
|
1936
|
+
validateOklchColor(value);
|
|
1937
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1938
|
+
}
|
|
1951
1939
|
validateOkhslColor(value);
|
|
1952
1940
|
return value;
|
|
1953
1941
|
}
|
|
@@ -1955,11 +1943,9 @@ function extractOkhslFromValue(value) {
|
|
|
1955
1943
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
1956
1944
|
*
|
|
1957
1945
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
1958
|
-
* across every value-shorthand form
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
* snapshotted `globalConfig.lightLightness` / `globalConfig.darkLightness`
|
|
1962
|
-
* windows.
|
|
1946
|
+
* across every value-shorthand form, using the snapshotted
|
|
1947
|
+
* `globalConfig.darkLightness` window (light lightness preserved via
|
|
1948
|
+
* `lightLightness: false`).
|
|
1963
1949
|
*
|
|
1964
1950
|
* When the user requests `contrast` or relative `lightness`, a hidden
|
|
1965
1951
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
@@ -2000,17 +1986,20 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
2000
1986
|
primary
|
|
2001
1987
|
};
|
|
2002
1988
|
}
|
|
2003
|
-
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData) {
|
|
1989
|
+
function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip) {
|
|
2004
1990
|
let cached;
|
|
2005
1991
|
const resolveOnce = () => {
|
|
2006
1992
|
if (cached) return cached;
|
|
2007
|
-
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
1993
|
+
cached = resolveAllColors(seedHue, seedSaturation, defs, effectiveScaling, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0, autoFlip);
|
|
2008
1994
|
return cached;
|
|
2009
1995
|
};
|
|
2010
|
-
const resolveStates = (options) =>
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
1996
|
+
const resolveStates = (options) => {
|
|
1997
|
+
const cfg = getConfig();
|
|
1998
|
+
return {
|
|
1999
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2000
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2001
|
+
};
|
|
2002
|
+
};
|
|
2014
2003
|
const tokenLike = (options) => {
|
|
2015
2004
|
return buildTokenMap(resolveOnce(), "", resolveStates(options), resolveModes(options?.modes), options?.format)[`#${primary}`];
|
|
2016
2005
|
};
|
|
@@ -2040,39 +2029,7 @@ function resolveBaseToken(base) {
|
|
|
2040
2029
|
if (isGlazeColorToken(base)) return base;
|
|
2041
2030
|
return createColorTokenFromValue(base, void 0, void 0);
|
|
2042
2031
|
}
|
|
2043
|
-
|
|
2044
|
-
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2045
|
-
* recursively serialized when it was originally a token; raw values are
|
|
2046
|
-
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2047
|
-
*/
|
|
2048
|
-
function buildOverridesExport(options) {
|
|
2049
|
-
const out = {};
|
|
2050
|
-
if (options.hue !== void 0) out.hue = options.hue;
|
|
2051
|
-
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2052
|
-
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2053
|
-
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2054
|
-
if (options.mode !== void 0) out.mode = options.mode;
|
|
2055
|
-
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2056
|
-
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2057
|
-
if (options.name !== void 0) out.name = options.name;
|
|
2058
|
-
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2059
|
-
return out;
|
|
2060
|
-
}
|
|
2061
|
-
function buildStructuredInputExport(input) {
|
|
2062
|
-
const out = {
|
|
2063
|
-
hue: input.hue,
|
|
2064
|
-
saturation: input.saturation,
|
|
2065
|
-
lightness: input.lightness
|
|
2066
|
-
};
|
|
2067
|
-
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2068
|
-
if (input.mode !== void 0) out.mode = input.mode;
|
|
2069
|
-
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2070
|
-
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2071
|
-
if (input.name !== void 0) out.name = input.name;
|
|
2072
|
-
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2073
|
-
return out;
|
|
2074
|
-
}
|
|
2075
|
-
function createColorToken(input, scaling) {
|
|
2032
|
+
function createColorToken(input, scaling, overrideAutoFlip) {
|
|
2076
2033
|
validateStructuredInput(input);
|
|
2077
2034
|
const userName = input.name;
|
|
2078
2035
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
@@ -2093,41 +2050,70 @@ function createColorToken(input, scaling) {
|
|
|
2093
2050
|
saturation: 1,
|
|
2094
2051
|
mode: "static"
|
|
2095
2052
|
};
|
|
2096
|
-
const effectiveScaling = scaling ??
|
|
2053
|
+
const effectiveScaling = scaling ?? defaultStructuredScaling();
|
|
2054
|
+
const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
|
|
2097
2055
|
const exportData = () => ({
|
|
2098
2056
|
form: "structured",
|
|
2099
2057
|
input: buildStructuredInputExport(input),
|
|
2100
|
-
scaling: effectiveScaling
|
|
2058
|
+
scaling: effectiveScaling,
|
|
2059
|
+
autoFlip
|
|
2101
2060
|
});
|
|
2102
|
-
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2061
|
+
return createColorTokenFromDefs(input.hue, input.saturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
|
|
2103
2062
|
}
|
|
2104
|
-
function createColorTokenFromValue(value, options, scaling) {
|
|
2105
|
-
const inputIsString = typeof value === "string";
|
|
2063
|
+
function createColorTokenFromValue(value, options, scaling, overrideAutoFlip) {
|
|
2106
2064
|
const main = extractOkhslFromValue(value);
|
|
2107
2065
|
const baseToken = resolveBaseToken(options?.base);
|
|
2108
2066
|
const { seedHue, seedSaturation, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
2109
|
-
const effectiveScaling = scaling ??
|
|
2067
|
+
const effectiveScaling = scaling ?? defaultValueShorthandScaling();
|
|
2068
|
+
const autoFlip = overrideAutoFlip ?? getConfig().autoFlip;
|
|
2110
2069
|
const exportData = () => ({
|
|
2111
2070
|
form: "value",
|
|
2112
2071
|
input: value,
|
|
2113
2072
|
...options !== void 0 ? { overrides: buildOverridesExport(options) } : {},
|
|
2114
|
-
scaling: effectiveScaling
|
|
2073
|
+
scaling: effectiveScaling,
|
|
2074
|
+
autoFlip
|
|
2115
2075
|
});
|
|
2116
|
-
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData);
|
|
2076
|
+
return createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effectiveScaling, baseToken, exportData, autoFlip);
|
|
2117
2077
|
}
|
|
2118
2078
|
/**
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2079
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
2080
|
+
* recursively serialized when it was originally a token; raw values are
|
|
2081
|
+
* preserved as-is so `glaze.colorFrom(...)` round-trips them.
|
|
2121
2082
|
*/
|
|
2122
|
-
function
|
|
2123
|
-
|
|
2124
|
-
if (
|
|
2125
|
-
if (
|
|
2126
|
-
if (
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2083
|
+
function buildOverridesExport(options) {
|
|
2084
|
+
const out = {};
|
|
2085
|
+
if (options.hue !== void 0) out.hue = options.hue;
|
|
2086
|
+
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
2087
|
+
if (options.lightness !== void 0) out.lightness = options.lightness;
|
|
2088
|
+
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
2089
|
+
if (options.mode !== void 0) out.mode = options.mode;
|
|
2090
|
+
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2091
|
+
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2092
|
+
if (options.name !== void 0) out.name = options.name;
|
|
2093
|
+
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2094
|
+
return out;
|
|
2095
|
+
}
|
|
2096
|
+
function buildStructuredInputExport(input) {
|
|
2097
|
+
const out = {
|
|
2098
|
+
hue: input.hue,
|
|
2099
|
+
saturation: input.saturation,
|
|
2100
|
+
lightness: input.lightness
|
|
2101
|
+
};
|
|
2102
|
+
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
2103
|
+
if (input.mode !== void 0) out.mode = input.mode;
|
|
2104
|
+
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2105
|
+
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2106
|
+
if (input.name !== void 0) out.name = input.name;
|
|
2107
|
+
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2108
|
+
return out;
|
|
2109
|
+
}
|
|
2110
|
+
/**
|
|
2111
|
+
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
2112
|
+
* `GlazeColorTokenExport` always has a `form` field set to either
|
|
2113
|
+
* `'value'` or `'structured'`; raw values never do.
|
|
2114
|
+
*/
|
|
2115
|
+
function isExportedToken(candidate) {
|
|
2116
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "form" in candidate && (candidate.form === "value" || candidate.form === "structured");
|
|
2131
2117
|
}
|
|
2132
2118
|
function rehydrateOverrides(data) {
|
|
2133
2119
|
const out = {};
|
|
@@ -2157,13 +2143,264 @@ function rehydrateStructuredInput(data) {
|
|
|
2157
2143
|
return out;
|
|
2158
2144
|
}
|
|
2159
2145
|
/**
|
|
2160
|
-
*
|
|
2161
|
-
*
|
|
2162
|
-
* `'value'` or `'structured'`; raw values never do.
|
|
2146
|
+
* Rehydrate a token from its `.export()` snapshot. Recursively rebuilds
|
|
2147
|
+
* any base dependency. Inverse of `GlazeColorToken.export()`.
|
|
2163
2148
|
*/
|
|
2164
|
-
function
|
|
2165
|
-
|
|
2149
|
+
function colorFromExport(data) {
|
|
2150
|
+
if (data === null || typeof data !== "object") throw new Error(`glaze.colorFrom: expected an object from token.export(), got ${data === null ? "null" : typeof data}.`);
|
|
2151
|
+
if (data.form !== "value" && data.form !== "structured") throw new Error(`glaze.colorFrom: invalid "form" field — expected "value" or "structured" (got ${JSON.stringify(data.form)}).`);
|
|
2152
|
+
if (data.input === void 0) throw new Error(`glaze.colorFrom: missing "input" field — expected the original ${data.form === "value" ? "GlazeColorValue" : "GlazeColorInput"}.`);
|
|
2153
|
+
if (data.form === "value") {
|
|
2154
|
+
const value = data.input;
|
|
2155
|
+
const overrides = data.overrides ? rehydrateOverrides(data.overrides) : void 0;
|
|
2156
|
+
const cfg = getConfig();
|
|
2157
|
+
const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
|
|
2158
|
+
return createColorTokenFromValue(value, overrides, data.scaling, effectiveAutoFlip);
|
|
2159
|
+
}
|
|
2160
|
+
const input = rehydrateStructuredInput(data.input);
|
|
2161
|
+
const cfg = getConfig();
|
|
2162
|
+
const effectiveAutoFlip = data.autoFlip ?? cfg.autoFlip;
|
|
2163
|
+
return createColorToken(input, data.scaling, effectiveAutoFlip);
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
//#endregion
|
|
2167
|
+
//#region src/palette.ts
|
|
2168
|
+
/**
|
|
2169
|
+
* Palette factory.
|
|
2170
|
+
*
|
|
2171
|
+
* Composes multiple themes into a single token namespace with optional
|
|
2172
|
+
* theme-name prefixes and a "primary theme" that also surfaces an
|
|
2173
|
+
* unprefixed copy of its tokens. All four export methods (`tokens` /
|
|
2174
|
+
* `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
|
|
2175
|
+
* handles validation, per-theme iteration, prefix resolution, collision
|
|
2176
|
+
* filtering, and primary duplication.
|
|
2177
|
+
*/
|
|
2178
|
+
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
2179
|
+
const prefix = options?.prefix ?? defaultPrefix;
|
|
2180
|
+
if (prefix === true) return `${themeName}-`;
|
|
2181
|
+
if (typeof prefix === "object" && prefix !== null) return prefix[themeName] ?? `${themeName}-`;
|
|
2182
|
+
return "";
|
|
2183
|
+
}
|
|
2184
|
+
function validatePrimaryTheme(primary, themes) {
|
|
2185
|
+
if (primary !== void 0 && !(primary in themes)) {
|
|
2186
|
+
const available = Object.keys(themes).join(", ");
|
|
2187
|
+
throw new Error(`glaze: primary theme "${primary}" not found in palette. Available: ${available}.`);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Resolve the effective primary for an export call.
|
|
2192
|
+
* `false` disables, a string overrides, `undefined` inherits from palette.
|
|
2193
|
+
*/
|
|
2194
|
+
function resolveEffectivePrimary(exportPrimary, palettePrimary) {
|
|
2195
|
+
if (exportPrimary === false) return void 0;
|
|
2196
|
+
return exportPrimary ?? palettePrimary;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Filter a resolved color map, skipping keys already in `seen`.
|
|
2200
|
+
* Warns on collision and keeps the first-written value (first-write-wins).
|
|
2201
|
+
* Returns a new map containing only non-colliding entries.
|
|
2202
|
+
*/
|
|
2203
|
+
function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
2204
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
2205
|
+
const label = isPrimary ? `${themeName} (primary)` : themeName;
|
|
2206
|
+
for (const [name, color] of resolved) {
|
|
2207
|
+
const key = `${prefix}${name}`;
|
|
2208
|
+
if (seen.has(key)) {
|
|
2209
|
+
console.warn(`glaze: token "${key}" from theme "${label}" collides with theme "${seen.get(key)}" — skipping.`);
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
seen.set(key, label);
|
|
2213
|
+
filtered.set(name, color);
|
|
2214
|
+
}
|
|
2215
|
+
return filtered;
|
|
2216
|
+
}
|
|
2217
|
+
/**
|
|
2218
|
+
* Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
|
|
2219
|
+
* this because it doesn't do collision filtering or primary duplication.
|
|
2220
|
+
*/
|
|
2221
|
+
function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, empty) {
|
|
2222
|
+
const effectivePrimary = resolveEffectivePrimary(options?.primary, paletteOptions?.primary);
|
|
2223
|
+
if (options?.primary !== void 0) validatePrimaryTheme(effectivePrimary, themes);
|
|
2224
|
+
const acc = empty();
|
|
2225
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2226
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
2227
|
+
const resolved = theme.resolve();
|
|
2228
|
+
const prefix = resolvePrefix(options, themeName, true);
|
|
2229
|
+
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix));
|
|
2230
|
+
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), ""));
|
|
2231
|
+
}
|
|
2232
|
+
return acc;
|
|
2233
|
+
}
|
|
2234
|
+
function createPalette(themes, paletteOptions) {
|
|
2235
|
+
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
2236
|
+
return {
|
|
2237
|
+
tokens(options) {
|
|
2238
|
+
const modes = resolveModes(options?.modes);
|
|
2239
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildFlatTokenMap(filtered, prefix, modes, options?.format), (acc, part) => {
|
|
2240
|
+
for (const variant of Object.keys(part)) {
|
|
2241
|
+
if (!acc[variant]) acc[variant] = {};
|
|
2242
|
+
Object.assign(acc[variant], part[variant]);
|
|
2243
|
+
}
|
|
2244
|
+
}, () => ({}));
|
|
2245
|
+
},
|
|
2246
|
+
tasty(options) {
|
|
2247
|
+
const cfg = getConfig();
|
|
2248
|
+
const states = {
|
|
2249
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2250
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2251
|
+
};
|
|
2252
|
+
const modes = resolveModes(options?.modes);
|
|
2253
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildTokenMap(filtered, prefix, states, modes, options?.format), (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2254
|
+
},
|
|
2255
|
+
json(options) {
|
|
2256
|
+
const modes = resolveModes(options?.modes);
|
|
2257
|
+
const result = {};
|
|
2258
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, options?.format);
|
|
2259
|
+
return result;
|
|
2260
|
+
},
|
|
2261
|
+
css(options) {
|
|
2262
|
+
const suffix = options?.suffix ?? "-color";
|
|
2263
|
+
const format = options?.format ?? "rgb";
|
|
2264
|
+
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix) => buildCssMap(filtered, prefix, suffix, format), (acc, part) => {
|
|
2265
|
+
for (const key of [
|
|
2266
|
+
"light",
|
|
2267
|
+
"dark",
|
|
2268
|
+
"lightContrast",
|
|
2269
|
+
"darkContrast"
|
|
2270
|
+
]) if (part[key]) acc[key].push(part[key]);
|
|
2271
|
+
}, () => ({
|
|
2272
|
+
light: [],
|
|
2273
|
+
dark: [],
|
|
2274
|
+
lightContrast: [],
|
|
2275
|
+
darkContrast: []
|
|
2276
|
+
}));
|
|
2277
|
+
return {
|
|
2278
|
+
light: lines.light.join("\n"),
|
|
2279
|
+
dark: lines.dark.join("\n"),
|
|
2280
|
+
lightContrast: lines.lightContrast.join("\n"),
|
|
2281
|
+
darkContrast: lines.darkContrast.join("\n")
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
//#endregion
|
|
2288
|
+
//#region src/theme.ts
|
|
2289
|
+
/**
|
|
2290
|
+
* Theme factory.
|
|
2291
|
+
*
|
|
2292
|
+
* Wraps a hue/saturation seed and a mutable `ColorMap`, and exposes
|
|
2293
|
+
* `tokens()` / `tasty()` / `json()` / `css()` / `resolve()` / `export()`
|
|
2294
|
+
* / `extend()`. Caches the last resolve result so successive exports
|
|
2295
|
+
* with the same defs and config don't re-run the four-pass resolver.
|
|
2296
|
+
*/
|
|
2297
|
+
function createTheme(hue, saturation, initialColors) {
|
|
2298
|
+
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
2299
|
+
let cache = null;
|
|
2300
|
+
function resolveCached() {
|
|
2301
|
+
const version = getConfigVersion();
|
|
2302
|
+
if (cache && cache.version === version) return cache.map;
|
|
2303
|
+
const map = resolveAllColors(hue, saturation, colorDefs);
|
|
2304
|
+
cache = {
|
|
2305
|
+
map,
|
|
2306
|
+
version
|
|
2307
|
+
};
|
|
2308
|
+
return map;
|
|
2309
|
+
}
|
|
2310
|
+
function invalidate() {
|
|
2311
|
+
cache = null;
|
|
2312
|
+
}
|
|
2313
|
+
return {
|
|
2314
|
+
get hue() {
|
|
2315
|
+
return hue;
|
|
2316
|
+
},
|
|
2317
|
+
get saturation() {
|
|
2318
|
+
return saturation;
|
|
2319
|
+
},
|
|
2320
|
+
colors(defs) {
|
|
2321
|
+
colorDefs = {
|
|
2322
|
+
...colorDefs,
|
|
2323
|
+
...defs
|
|
2324
|
+
};
|
|
2325
|
+
invalidate();
|
|
2326
|
+
},
|
|
2327
|
+
color(name, def) {
|
|
2328
|
+
if (def === void 0) return colorDefs[name];
|
|
2329
|
+
colorDefs[name] = def;
|
|
2330
|
+
invalidate();
|
|
2331
|
+
},
|
|
2332
|
+
remove(names) {
|
|
2333
|
+
const list = Array.isArray(names) ? names : [names];
|
|
2334
|
+
for (const name of list) delete colorDefs[name];
|
|
2335
|
+
invalidate();
|
|
2336
|
+
},
|
|
2337
|
+
has(name) {
|
|
2338
|
+
return name in colorDefs;
|
|
2339
|
+
},
|
|
2340
|
+
list() {
|
|
2341
|
+
return Object.keys(colorDefs);
|
|
2342
|
+
},
|
|
2343
|
+
reset() {
|
|
2344
|
+
colorDefs = {};
|
|
2345
|
+
invalidate();
|
|
2346
|
+
},
|
|
2347
|
+
export() {
|
|
2348
|
+
return {
|
|
2349
|
+
hue,
|
|
2350
|
+
saturation,
|
|
2351
|
+
colors: { ...colorDefs }
|
|
2352
|
+
};
|
|
2353
|
+
},
|
|
2354
|
+
extend(options) {
|
|
2355
|
+
const newHue = options.hue ?? hue;
|
|
2356
|
+
const newSat = options.saturation ?? saturation;
|
|
2357
|
+
const inheritedColors = {};
|
|
2358
|
+
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
2359
|
+
return createTheme(newHue, newSat, options.colors ? {
|
|
2360
|
+
...inheritedColors,
|
|
2361
|
+
...options.colors
|
|
2362
|
+
} : { ...inheritedColors });
|
|
2363
|
+
},
|
|
2364
|
+
resolve() {
|
|
2365
|
+
return new Map(resolveCached());
|
|
2366
|
+
},
|
|
2367
|
+
tokens(options) {
|
|
2368
|
+
const modes = resolveModes(options?.modes);
|
|
2369
|
+
return buildFlatTokenMap(resolveCached(), "", modes, options?.format);
|
|
2370
|
+
},
|
|
2371
|
+
tasty(options) {
|
|
2372
|
+
const cfg = getConfig();
|
|
2373
|
+
const states = {
|
|
2374
|
+
dark: options?.states?.dark ?? cfg.states.dark,
|
|
2375
|
+
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2376
|
+
};
|
|
2377
|
+
const modes = resolveModes(options?.modes);
|
|
2378
|
+
return buildTokenMap(resolveCached(), "", states, modes, options?.format);
|
|
2379
|
+
},
|
|
2380
|
+
json(options) {
|
|
2381
|
+
const modes = resolveModes(options?.modes);
|
|
2382
|
+
return buildJsonMap(resolveCached(), modes, options?.format);
|
|
2383
|
+
},
|
|
2384
|
+
css(options) {
|
|
2385
|
+
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", options?.format ?? "rgb");
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2166
2388
|
}
|
|
2389
|
+
|
|
2390
|
+
//#endregion
|
|
2391
|
+
//#region src/glaze.ts
|
|
2392
|
+
/**
|
|
2393
|
+
* Glaze — OKHSL-based color theme generator.
|
|
2394
|
+
*
|
|
2395
|
+
* Public API entry. Wires `glaze()` and its attached static methods to
|
|
2396
|
+
* the focused modules in this folder:
|
|
2397
|
+
* - `theme.ts` — single-theme factory
|
|
2398
|
+
* - `palette.ts` — multi-theme composition
|
|
2399
|
+
* - `color-token.ts` — standalone single-color tokens (`glaze.color`)
|
|
2400
|
+
* - `shadow.ts` — standalone shadow factory (`glaze.shadow`)
|
|
2401
|
+
* - `formatters.ts` — variant → string (`glaze.format`)
|
|
2402
|
+
* - `config.ts` — global config singleton
|
|
2403
|
+
*/
|
|
2167
2404
|
/**
|
|
2168
2405
|
* Create a single-hue glaze theme.
|
|
2169
2406
|
*
|
|
@@ -2178,41 +2415,18 @@ function glaze(hueOrOptions, saturation) {
|
|
|
2178
2415
|
if (typeof hueOrOptions === "number") return createTheme(hueOrOptions, saturation ?? 100);
|
|
2179
2416
|
return createTheme(hueOrOptions.hue, hueOrOptions.saturation);
|
|
2180
2417
|
}
|
|
2181
|
-
/**
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
glaze.configure = function configure(config) {
|
|
2185
|
-
globalConfig = {
|
|
2186
|
-
lightLightness: config.lightLightness ?? globalConfig.lightLightness,
|
|
2187
|
-
darkLightness: config.darkLightness ?? globalConfig.darkLightness,
|
|
2188
|
-
darkDesaturation: config.darkDesaturation ?? globalConfig.darkDesaturation,
|
|
2189
|
-
darkCurve: config.darkCurve ?? globalConfig.darkCurve,
|
|
2190
|
-
states: {
|
|
2191
|
-
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
2192
|
-
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast
|
|
2193
|
-
},
|
|
2194
|
-
modes: {
|
|
2195
|
-
dark: config.modes?.dark ?? globalConfig.modes.dark,
|
|
2196
|
-
highContrast: config.modes?.highContrast ?? globalConfig.modes.highContrast
|
|
2197
|
-
},
|
|
2198
|
-
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning
|
|
2199
|
-
};
|
|
2418
|
+
/** Configure global glaze settings. */
|
|
2419
|
+
glaze.configure = function configure$1(config) {
|
|
2420
|
+
configure(config);
|
|
2200
2421
|
};
|
|
2201
|
-
/**
|
|
2202
|
-
* Compose multiple themes into a palette.
|
|
2203
|
-
*/
|
|
2422
|
+
/** Compose multiple themes into a palette. */
|
|
2204
2423
|
glaze.palette = function palette(themes, options) {
|
|
2205
2424
|
return createPalette(themes, options);
|
|
2206
2425
|
};
|
|
2207
|
-
/**
|
|
2208
|
-
* Create a theme from a serialized export.
|
|
2209
|
-
*/
|
|
2426
|
+
/** Create a theme from a serialized export. */
|
|
2210
2427
|
glaze.from = function from(data) {
|
|
2211
2428
|
return createTheme(data.hue, data.saturation, data.colors);
|
|
2212
2429
|
};
|
|
2213
|
-
function isStructuredColorInput(input) {
|
|
2214
|
-
return typeof input === "object" && input !== null && !Array.isArray(input) && "hue" in input && "lightness" in input;
|
|
2215
|
-
}
|
|
2216
2430
|
/**
|
|
2217
2431
|
* Create a standalone single-color token.
|
|
2218
2432
|
*
|
|
@@ -2222,22 +2436,16 @@ function isStructuredColorInput(input) {
|
|
|
2222
2436
|
* lightness-window override.
|
|
2223
2437
|
* - `glaze.color(value, overrides?, scaling?)` — value-shorthand: a hex
|
|
2224
2438
|
* string (3/6/8 digits), one of the CSS color functions Glaze itself
|
|
2225
|
-
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`),
|
|
2226
|
-
*
|
|
2439
|
+
* emits (`rgb()`, `hsl()`, `okhsl()`, `oklch()`), or literal objects
|
|
2440
|
+
* `{ r, g, b }` (0–255), `{ h, s, l }` (OKHSL 0–1), `{ l, c, h }`
|
|
2441
|
+
* (OKLCh, matching `oklch()` strings).
|
|
2227
2442
|
*
|
|
2228
|
-
* Defaults: every input form defaults to `mode: 'auto'
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2234
|
-
* exactly; dark Möbius-inverts up to 100, so `glaze.color('#000')`
|
|
2235
|
-
* renders as `#fff` in dark mode (and `glaze.color('#fff')` falls to
|
|
2236
|
-
* the dark `lo` floor).
|
|
2237
|
-
* - `OkhslColor` object / RGB-tuple / structured value-shorthand:
|
|
2238
|
-
* `{ lightLightness: globalConfig.lightLightness, darkLightness:
|
|
2239
|
-
* globalConfig.darkLightness }` — both windows come straight from
|
|
2240
|
-
* `globalConfig`, so the resulting token behaves like a theme color.
|
|
2443
|
+
* Defaults: every input form defaults to `mode: 'auto'`. Value-shorthand
|
|
2444
|
+
* (strings and literal objects) snapshots `{ lightLightness: false,
|
|
2445
|
+
* darkLightness: globalConfig.darkLightness }` — light preserves the
|
|
2446
|
+
* input; dark uses the theme window. Structured `{ hue, saturation,
|
|
2447
|
+
* lightness, ... }` snapshots both `globalConfig` windows like a theme
|
|
2448
|
+
* color.
|
|
2241
2449
|
*
|
|
2242
2450
|
* Pass `{ mode: 'fixed' }` to opt back into the legacy linear, non-
|
|
2243
2451
|
* inverting mapping, or `{ mode: 'static' }` to pin the same lightness
|
|
@@ -2262,7 +2470,7 @@ glaze.color = function color(input, arg2, arg3) {
|
|
|
2262
2470
|
*
|
|
2263
2471
|
* Both `bg` and `fg` accept any `GlazeColorValue` form: hex (`#rgb` /
|
|
2264
2472
|
* `#rrggbb` / `#rrggbbaa`), `rgb()` / `hsl()` / `okhsl()` / `oklch()`
|
|
2265
|
-
* strings, `
|
|
2473
|
+
* strings, or `{ r, g, b }` / `{ h, s, l }` / `{ l, c, h }` objects.
|
|
2266
2474
|
*/
|
|
2267
2475
|
glaze.shadow = function shadow(input) {
|
|
2268
2476
|
const bg = extractOkhslFromValue(input.bg);
|
|
@@ -2276,9 +2484,7 @@ glaze.shadow = function shadow(input) {
|
|
|
2276
2484
|
alpha: 1
|
|
2277
2485
|
} : void 0, input.intensity, tuning);
|
|
2278
2486
|
};
|
|
2279
|
-
/**
|
|
2280
|
-
* Format a resolved color variant as a CSS string.
|
|
2281
|
-
*/
|
|
2487
|
+
/** Format a resolved color variant as a CSS string. */
|
|
2282
2488
|
glaze.format = function format(variant, colorFormat) {
|
|
2283
2489
|
return formatVariant(variant, colorFormat);
|
|
2284
2490
|
};
|
|
@@ -2324,30 +2530,13 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
2324
2530
|
glaze.colorFrom = function colorFrom(data) {
|
|
2325
2531
|
return colorFromExport(data);
|
|
2326
2532
|
};
|
|
2327
|
-
/**
|
|
2328
|
-
* Get the current global configuration (for testing/debugging).
|
|
2329
|
-
*/
|
|
2533
|
+
/** Get the current global configuration (for testing/debugging). */
|
|
2330
2534
|
glaze.getConfig = function getConfig() {
|
|
2331
|
-
return
|
|
2535
|
+
return snapshotConfig();
|
|
2332
2536
|
};
|
|
2333
|
-
/**
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
glaze.resetConfig = function resetConfig() {
|
|
2337
|
-
globalConfig = {
|
|
2338
|
-
lightLightness: [10, 100],
|
|
2339
|
-
darkLightness: [15, 95],
|
|
2340
|
-
darkDesaturation: .1,
|
|
2341
|
-
darkCurve: .5,
|
|
2342
|
-
states: {
|
|
2343
|
-
dark: "@dark",
|
|
2344
|
-
highContrast: "@high-contrast"
|
|
2345
|
-
},
|
|
2346
|
-
modes: {
|
|
2347
|
-
dark: true,
|
|
2348
|
-
highContrast: false
|
|
2349
|
-
}
|
|
2350
|
-
};
|
|
2537
|
+
/** Reset global configuration to defaults. */
|
|
2538
|
+
glaze.resetConfig = function resetConfig$1() {
|
|
2539
|
+
resetConfig();
|
|
2351
2540
|
};
|
|
2352
2541
|
|
|
2353
2542
|
//#endregion
|