@tenphi/glaze 1.4.0 → 2.0.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 +1 -1
- package/dist/index.cjs +104 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -21
- package/dist/index.d.mts +29 -21
- package/dist/index.mjs +104 -47
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +96 -44
- package/docs/methodology.md +15 -13
- package/docs/migration.md +36 -0
- package/docs/okhst.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ contrast floor.
|
|
|
32
32
|
- **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
|
|
33
33
|
- **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
|
|
34
34
|
- **Light + Dark + High-Contrast** — all schemes from one definition
|
|
35
|
-
- **Manual contrast level** —
|
|
35
|
+
- **Manual contrast level** — put your normal palette on a `0–100` slider between itself and the high-contrast one; every level in between is solved, not approximated, and the high-contrast tier stays available on top
|
|
36
36
|
- **Per-color hue override** — absolute or relative hue shifts within a theme
|
|
37
37
|
- **Multi-format output** — native `rgb`, `hsl`, and `oklch`, plus
|
|
38
38
|
[Tasty](https://tasty.style)-compatible `okhsl` and `okhst`
|
package/dist/index.cjs
CHANGED
|
@@ -372,6 +372,7 @@ const sRGBGammaToLinear = (val) => {
|
|
|
372
372
|
};
|
|
373
373
|
/**
|
|
374
374
|
* Convert OKHSL to gamma-encoded sRGB (clamped to 0–1).
|
|
375
|
+
* h: 0–360, s: 0–1, l: 0–1. Returns [r, g, b] with each channel 0–1.
|
|
375
376
|
*/
|
|
376
377
|
function okhslToSrgb(h, s, l, pastel = false) {
|
|
377
378
|
const lin = okhslToLinearSrgb(h, s, l, pastel);
|
|
@@ -581,47 +582,72 @@ function fmt$1(value, decimals) {
|
|
|
581
582
|
return parseFloat(value.toFixed(decimals)).toString();
|
|
582
583
|
}
|
|
583
584
|
/**
|
|
585
|
+
* Every `format*` writer takes `s` / `l` / `t` on the **0–1 factor scale** —
|
|
586
|
+
* the same scale every producer in the library hands back (`resolve()`,
|
|
587
|
+
* `variantToOkhsl`, `srgbToOkhsl`, `oklabToOkhsl`, `okhslToOklab`,
|
|
588
|
+
* `okhslToSrgb`). The percentages are an output detail: the writers multiply
|
|
589
|
+
* by 100 themselves when the CSS syntax asks for a percentage.
|
|
590
|
+
*
|
|
591
|
+
* Before 2.0 the writers took 0–100 while everything producing those values
|
|
592
|
+
* returned 0–1, so the obvious composition was off by 100x and failed
|
|
593
|
+
* silently — `0.7` is a legal percentage, so the result was a valid CSS string
|
|
594
|
+
* naming a near-black color. A value above 1 can only be that old
|
|
595
|
+
* percentage-scale input, so warn about it loudly rather than emit a wrong
|
|
596
|
+
* color quietly.
|
|
597
|
+
*/
|
|
598
|
+
const scaleWarnCache = /* @__PURE__ */ new Set();
|
|
599
|
+
function warnPercentScale(fn, ...values) {
|
|
600
|
+
if (!values.some((v) => v > 1.000001)) return;
|
|
601
|
+
if (!scaleWarnCache.has(fn)) {
|
|
602
|
+
scaleWarnCache.add(fn);
|
|
603
|
+
console.warn(`glaze: ${fn}() got a value above 1 — it takes the 0-1 factors every Glaze producer returns (resolve(), variantToOkhsl, srgbToOkhsl), not 0-100 percentages. Divide by 100, or drop the "* 100" left over from the pre-2.0 signature.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
584
607
|
* Format OKHSL values as a CSS `okhsl(H S% L%)` string.
|
|
585
|
-
* h: 0–360, s: 0–
|
|
608
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
586
609
|
*/
|
|
587
610
|
function formatOkhsl(h, s, l, pastel = false) {
|
|
611
|
+
warnPercentScale("formatOkhsl", s, l);
|
|
588
612
|
let outS = s;
|
|
589
|
-
if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s
|
|
590
|
-
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
|
|
613
|
+
if (pastel) outS = oklabToOkhsl(okhslToOklab(h, s, l, true), false)[1];
|
|
614
|
+
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(l * 100, 2)}%)`;
|
|
591
615
|
}
|
|
592
616
|
/**
|
|
593
617
|
* Format OKHST values as a CSS `okhst(H S% T%)` string.
|
|
594
|
-
* h: 0–360, s: 0–
|
|
618
|
+
* h: 0–360, s: 0–1, t: 0–1 (factor scale, matching `ResolvedColorVariant`).
|
|
595
619
|
*
|
|
596
620
|
* Pastel recompute matches `formatOkhsl`: convert via OKLab so external
|
|
597
621
|
* parsers that only understand non-pastel OKHST render identically.
|
|
598
622
|
*/
|
|
599
623
|
function formatOkhst(h, s, t, pastel = false) {
|
|
624
|
+
warnPercentScale("formatOkhst", s, t);
|
|
600
625
|
let outS = s;
|
|
601
626
|
if (pastel) {
|
|
602
627
|
const REF_EPS = .05;
|
|
603
628
|
const den = Math.log(1 + REF_EPS) - Math.log(REF_EPS);
|
|
604
|
-
const y = Math.exp(t
|
|
605
|
-
|
|
606
|
-
outS = oklabToOkhsl(okhslToOklab(h, s / 100, l, true), false)[1] * 100;
|
|
629
|
+
const y = Math.exp(t * den + Math.log(REF_EPS)) - REF_EPS;
|
|
630
|
+
outS = oklabToOkhsl(okhslToOklab(h, s, toe(Math.cbrt(Math.max(0, y))), true), false)[1];
|
|
607
631
|
}
|
|
608
|
-
return `okhst(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(t, 2)}%)`;
|
|
632
|
+
return `okhst(${fmt$1(h, 2)} ${fmt$1(outS * 100, 2)}% ${fmt$1(t * 100, 2)}%)`;
|
|
609
633
|
}
|
|
610
634
|
/**
|
|
611
635
|
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
612
636
|
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
613
|
-
* h: 0–360, s: 0–
|
|
637
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
614
638
|
*/
|
|
615
639
|
function formatRgb(h, s, l, pastel = false) {
|
|
616
|
-
|
|
640
|
+
warnPercentScale("formatRgb", s, l);
|
|
641
|
+
const [r, g, b] = okhslToSrgb(h, s, l, pastel);
|
|
617
642
|
return `rgb(${parseFloat((r * 255).toFixed(2))} ${parseFloat((g * 255).toFixed(2))} ${parseFloat((b * 255).toFixed(2))})`;
|
|
618
643
|
}
|
|
619
644
|
/**
|
|
620
645
|
* Format OKHSL values as a CSS `hsl(H S% L%)` string.
|
|
621
|
-
* h: 0–360, s: 0–
|
|
646
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
622
647
|
*/
|
|
623
648
|
function formatHsl(h, s, l, pastel = false) {
|
|
624
|
-
|
|
649
|
+
warnPercentScale("formatHsl", s, l);
|
|
650
|
+
const [r, g, b] = okhslToSrgb(h, s, l, pastel);
|
|
625
651
|
const max = Math.max(r, g, b);
|
|
626
652
|
const min = Math.min(r, g, b);
|
|
627
653
|
const delta = max - min;
|
|
@@ -638,10 +664,11 @@ function formatHsl(h, s, l, pastel = false) {
|
|
|
638
664
|
}
|
|
639
665
|
/**
|
|
640
666
|
* Format OKHSL values as a CSS `oklch(L C H)` string.
|
|
641
|
-
* h: 0–360, s: 0–
|
|
667
|
+
* h: 0–360, s: 0–1, l: 0–1 (factor scale, as returned by every converter).
|
|
642
668
|
*/
|
|
643
669
|
function formatOklch(h, s, l, pastel = false) {
|
|
644
|
-
|
|
670
|
+
warnPercentScale("formatOklch", s, l);
|
|
671
|
+
const [L, C, hh] = okhslToOklch(h, s, l, pastel);
|
|
645
672
|
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
646
673
|
}
|
|
647
674
|
/**
|
|
@@ -906,10 +933,14 @@ function levelFraction(level) {
|
|
|
906
933
|
}
|
|
907
934
|
/**
|
|
908
935
|
* Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
|
|
909
|
-
* in `'auto'` mode
|
|
936
|
+
* in `'auto'` mode, where the normal variants take the authored normal entries
|
|
937
|
+
* as-is.
|
|
910
938
|
*
|
|
911
|
-
* Note `contrastLevel: 0` yields `0`, not `undefined
|
|
912
|
-
*
|
|
939
|
+
* Note `contrastLevel: 0` yields `0`, not `undefined`. The two select different
|
|
940
|
+
* branches, and an authored `0` is a slider position that `.export()` freezes
|
|
941
|
+
* where an absent level is not — but they are not an *output* distinction: at
|
|
942
|
+
* fraction 0 every interpolation returns its normal entry verbatim, so level 0
|
|
943
|
+
* reproduces `'auto'` bit for bit.
|
|
913
944
|
*/
|
|
914
945
|
function contrastFraction(config) {
|
|
915
946
|
const level = config.contrastLevel;
|
|
@@ -917,6 +948,20 @@ function contrastFraction(config) {
|
|
|
917
948
|
return levelFraction(level);
|
|
918
949
|
}
|
|
919
950
|
/**
|
|
951
|
+
* Whether the high-contrast variants are exact duplicates of the normal ones —
|
|
952
|
+
* true only at `contrastLevel: 100`, where the normal passes already resolve at
|
|
953
|
+
* full contrast.
|
|
954
|
+
*
|
|
955
|
+
* The level otherwise says nothing about high contrast: it positions the normal
|
|
956
|
+
* variants, while `lightContrast` / `darkContrast` stay the true high-contrast
|
|
957
|
+
* resolution at every level. This predicate marks the one level where that tier
|
|
958
|
+
* carries no new information, so callers can skip the two high-contrast passes,
|
|
959
|
+
* their drift checks, and the redundant output tier.
|
|
960
|
+
*/
|
|
961
|
+
function hcMirrorsNormal(config) {
|
|
962
|
+
return contrastFraction(config) === 1;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
920
965
|
* A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
|
|
921
966
|
*
|
|
922
967
|
* The endpoints return the authored entry *by identity* rather than through
|
|
@@ -1101,7 +1146,11 @@ function okhslToOkhst(c) {
|
|
|
1101
1146
|
}
|
|
1102
1147
|
/**
|
|
1103
1148
|
* Edge adapter: a resolved variant stores canonical tone `t` (0–1). Convert
|
|
1104
|
-
* it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect
|
|
1149
|
+
* it to the OKHSL `{ h, s, l }` the formatters and luminance pipeline expect —
|
|
1150
|
+
* `s` and `l` on the same 0–1 factor scale, so `const { h, s, l } =
|
|
1151
|
+
* variantToOkhsl(v)` destructures straight into `formatOkhsl` / `formatRgb` /
|
|
1152
|
+
* `okhslToLinearSrgb` with no rescaling. `alpha` stays on the variant; use
|
|
1153
|
+
* `glaze.format(v, format)` for a string that carries it.
|
|
1105
1154
|
*/
|
|
1106
1155
|
function variantToOkhsl(v) {
|
|
1107
1156
|
return {
|
|
@@ -2276,12 +2325,14 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
2276
2325
|
* Color resolution engine.
|
|
2277
2326
|
*
|
|
2278
2327
|
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
2279
|
-
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
*
|
|
2283
|
-
*
|
|
2284
|
-
*
|
|
2328
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name. A manual
|
|
2329
|
+
* `contrastLevel` interpolates the authored HC pairs, tone window, and contrast
|
|
2330
|
+
* targets at that level for the two *normal* passes only; the high-contrast
|
|
2331
|
+
* passes bypass the level entirely, so the high-contrast slots are the same
|
|
2332
|
+
* values `'auto'` resolves. At level 100 the normal passes already are the
|
|
2333
|
+
* high-contrast ones, so the solver drops to two passes (light → dark) and
|
|
2334
|
+
* mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow,
|
|
2335
|
+
* and mix color defs.
|
|
2285
2336
|
*
|
|
2286
2337
|
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
2287
2338
|
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
@@ -2538,7 +2589,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2538
2589
|
flip,
|
|
2539
2590
|
pastel
|
|
2540
2591
|
};
|
|
2541
|
-
const level = contrastFraction(
|
|
2592
|
+
const level = isHighContrast ? void 0 : contrastFraction(config);
|
|
2542
2593
|
let preferInitial = false;
|
|
2543
2594
|
if (level !== void 0 && level > 0 && level < 1) {
|
|
2544
2595
|
initialDirection = findToneForContrast({
|
|
@@ -2727,10 +2778,11 @@ function seedField(order, ctx, field, source) {
|
|
|
2727
2778
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
2728
2779
|
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
2729
2780
|
*
|
|
2730
|
-
* Under a manual `contrastLevel`
|
|
2731
|
-
*
|
|
2732
|
-
*
|
|
2733
|
-
*
|
|
2781
|
+
* Under a manual `contrastLevel` the spec is resolved at the level, so the check
|
|
2782
|
+
* measures each normal variant against the target it was actually solved for. At
|
|
2783
|
+
* level 100 the high-contrast slots are mirrors and are skipped: the normal
|
|
2784
|
+
* schemes already carry that check, and repeating it would emit a byte-identical
|
|
2785
|
+
* advisory under a different scheme label, which dedupes as a separate key.
|
|
2734
2786
|
*/
|
|
2735
2787
|
function verifyContrastDrift(order, defs, result, config) {
|
|
2736
2788
|
const roles = /* @__PURE__ */ new Map();
|
|
@@ -2765,9 +2817,9 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2765
2817
|
field: "darkContrast"
|
|
2766
2818
|
}
|
|
2767
2819
|
];
|
|
2768
|
-
const
|
|
2820
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2769
2821
|
for (const s of schemes) {
|
|
2770
|
-
if (
|
|
2822
|
+
if (mirrored && s.isHighContrast) continue;
|
|
2771
2823
|
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
|
|
2772
2824
|
const cVariant = color[s.field];
|
|
2773
2825
|
const bVariant = base[s.field];
|
|
@@ -2792,10 +2844,10 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2792
2844
|
roles: /* @__PURE__ */ new Map()
|
|
2793
2845
|
};
|
|
2794
2846
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
2795
|
-
const
|
|
2847
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2796
2848
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
2797
2849
|
let lightHCMap = lightMap;
|
|
2798
|
-
if (!
|
|
2850
|
+
if (!mirrored) {
|
|
2799
2851
|
seedField(order, ctx, "lightContrast", lightMap);
|
|
2800
2852
|
lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
2801
2853
|
}
|
|
@@ -2803,7 +2855,7 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2803
2855
|
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
2804
2856
|
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
2805
2857
|
let darkHCMap = darkMap;
|
|
2806
|
-
if (!
|
|
2858
|
+
if (!mirrored) {
|
|
2807
2859
|
seedField(order, ctx, "darkContrast", darkMap);
|
|
2808
2860
|
darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
2809
2861
|
}
|
|
@@ -3005,10 +3057,10 @@ function fmt(value, decimals) {
|
|
|
3005
3057
|
function formatVariant(v, format = "oklch", pastel = false) {
|
|
3006
3058
|
const effectivePastel = v.pastel ?? pastel;
|
|
3007
3059
|
let base;
|
|
3008
|
-
if (format === "okhst") base = formatOkhst(v.h, v.s
|
|
3060
|
+
if (format === "okhst") base = formatOkhst(v.h, v.s, v.t, effectivePastel);
|
|
3009
3061
|
else {
|
|
3010
3062
|
const { l } = variantToOkhsl(v);
|
|
3011
|
-
base = formatters[format](v.h, v.s
|
|
3063
|
+
base = formatters[format](v.h, v.s, l, effectivePastel);
|
|
3012
3064
|
}
|
|
3013
3065
|
if (v.alpha >= 1) return base;
|
|
3014
3066
|
const closing = base.lastIndexOf(")");
|
|
@@ -3024,7 +3076,7 @@ function formatVariantHue(v, plan, pastel = false) {
|
|
|
3024
3076
|
const [L, C] = okhslToOklch(v.h, v.s, l, effectivePastel);
|
|
3025
3077
|
let base;
|
|
3026
3078
|
if (plan.inline) if (v.s <= 1e-6) base = `oklch(${fmt(L, 4)} 0 0)`;
|
|
3027
|
-
else base = formatOklch(v.h, v.s
|
|
3079
|
+
else base = formatOklch(v.h, v.s, l, effectivePastel);
|
|
3028
3080
|
else base = `oklch(${fmt(L, 4)} ${fmt(C, 4)} ${plan.hueVar})`;
|
|
3029
3081
|
if (v.alpha >= 1) return base;
|
|
3030
3082
|
const closing = base.lastIndexOf(")");
|
|
@@ -3037,23 +3089,28 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
3037
3089
|
/**
|
|
3038
3090
|
* Resolve the effective output modes for an export.
|
|
3039
3091
|
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
*
|
|
3043
|
-
*
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
3092
|
+
* `modes.highContrast` and `contrastLevel` are independent: the level positions
|
|
3093
|
+
* the normal variants, the high-contrast tier stays the true high-contrast
|
|
3094
|
+
* resolution, and the two compose — a slider raises the baseline while a
|
|
3095
|
+
* `prefers-contrast: more` block still escalates on top of it.
|
|
3096
|
+
*
|
|
3097
|
+
* The one exception is a **global** level of 100, where the normal variants
|
|
3098
|
+
* already *are* the high-contrast ones. The tier would be an exact duplicate, so
|
|
3099
|
+
* it is dropped and a single light/dark set is emitted — including against an
|
|
3100
|
+
* explicit `highContrast: true` override, which has nothing different left to
|
|
3101
|
+
* ask for.
|
|
3046
3102
|
*
|
|
3047
3103
|
* A level set on a single theme or token does not change which modes are
|
|
3048
|
-
* emitted:
|
|
3049
|
-
*
|
|
3050
|
-
* the
|
|
3104
|
+
* emitted: `modes` is global-only, and a palette must not have one sibling
|
|
3105
|
+
* collapse the shared token structure. A per-instance level of 100 therefore
|
|
3106
|
+
* still reports into the tier, with values equal to its own normal ones.
|
|
3051
3107
|
*/
|
|
3052
3108
|
function resolveModes(override) {
|
|
3053
3109
|
const cfg = getConfig();
|
|
3110
|
+
const highContrast = override?.highContrast ?? cfg.modes.highContrast;
|
|
3054
3111
|
return {
|
|
3055
3112
|
dark: override?.dark ?? cfg.modes.dark,
|
|
3056
|
-
highContrast:
|
|
3113
|
+
highContrast: highContrast && !hcMirrorsNormal(cfg)
|
|
3057
3114
|
};
|
|
3058
3115
|
}
|
|
3059
3116
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|