@tenphi/glaze 0.15.0 → 0.16.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 +3 -0
- package/dist/index.cjs +884 -113
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -7
- package/dist/index.d.mts +496 -7
- package/dist/index.mjs +875 -114
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +294 -14
- package/docs/methodology.md +437 -222
- package/docs/migration.md +70 -5
- package/docs/okhst.md +45 -2
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -590,6 +590,24 @@ function formatOkhsl(h, s, l, pastel = false) {
|
|
|
590
590
|
return `okhsl(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(l, 2)}%)`;
|
|
591
591
|
}
|
|
592
592
|
/**
|
|
593
|
+
* Format OKHST values as a CSS `okhst(H S% T%)` string.
|
|
594
|
+
* h: 0–360, s: 0–100, t: 0–100 (percentage scale for s and t).
|
|
595
|
+
*
|
|
596
|
+
* Pastel recompute matches `formatOkhsl`: convert via OKLab so external
|
|
597
|
+
* parsers that only understand non-pastel OKHST render identically.
|
|
598
|
+
*/
|
|
599
|
+
function formatOkhst(h, s, t, pastel = false) {
|
|
600
|
+
let outS = s;
|
|
601
|
+
if (pastel) {
|
|
602
|
+
const REF_EPS = .05;
|
|
603
|
+
const den = Math.log(1 + REF_EPS) - Math.log(REF_EPS);
|
|
604
|
+
const y = Math.exp(t / 100 * den + Math.log(REF_EPS)) - REF_EPS;
|
|
605
|
+
const l = toe(Math.cbrt(Math.max(0, y)));
|
|
606
|
+
outS = oklabToOkhsl(okhslToOklab(h, s / 100, l, true), false)[1] * 100;
|
|
607
|
+
}
|
|
608
|
+
return `okhst(${fmt$1(h, 2)} ${fmt$1(outS, 2)}% ${fmt$1(t, 2)}%)`;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
593
611
|
* Format OKHSL values as a CSS `rgb(R G B)` string.
|
|
594
612
|
* Uses 2 decimal places to avoid 8-bit quantization contrast loss.
|
|
595
613
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
@@ -623,12 +641,34 @@ function formatHsl(h, s, l, pastel = false) {
|
|
|
623
641
|
* h: 0–360, s: 0–100, l: 0–100 (percentage scale for s and l).
|
|
624
642
|
*/
|
|
625
643
|
function formatOklch(h, s, l, pastel = false) {
|
|
626
|
-
const [L,
|
|
627
|
-
const C = Math.sqrt(a * a + b * b);
|
|
628
|
-
let hh = Math.atan2(b, a) * (180 / Math.PI);
|
|
629
|
-
hh = constrainAngle(hh);
|
|
644
|
+
const [L, C, hh] = okhslToOklch(h, s / 100, l / 100, pastel);
|
|
630
645
|
return `oklch(${fmt$1(L, 4)} ${fmt$1(C, 4)} ${fmt$1(hh, 2)})`;
|
|
631
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Convert gamma-encoded sRGB channels (0–1) to a 6-digit lowercase hex
|
|
649
|
+
* string (`#rrggbb`). Channels are clamped to [0,1] and rounded to 8-bit.
|
|
650
|
+
* Alpha is not encoded here — DTCG carries it as a separate `alpha` field.
|
|
651
|
+
*/
|
|
652
|
+
function srgbToHex(rgb) {
|
|
653
|
+
const toByte = (c) => Math.max(0, Math.min(255, Math.round(c * 255)));
|
|
654
|
+
const r = toByte(rgb[0]);
|
|
655
|
+
const g = toByte(rgb[1]);
|
|
656
|
+
const b = toByte(rgb[2]);
|
|
657
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Convert OKHSL (h: 0–360, s: 0–1, l: 0–1) to OKLCH components `[L, C, H]`.
|
|
661
|
+
* L: 0–1, C: 0–~0.4 (chroma), H: 0–360 (hue). Shared by `formatOklch` and
|
|
662
|
+
* the DTCG `oklch` colorSpace exporter so the two never drift apart.
|
|
663
|
+
*/
|
|
664
|
+
function okhslToOklch(h, s, l, pastel = false) {
|
|
665
|
+
const [L, a, b] = okhslToOklab(h, s, l, pastel);
|
|
666
|
+
return [
|
|
667
|
+
L,
|
|
668
|
+
Math.sqrt(a * a + b * b),
|
|
669
|
+
constrainAngle(Math.atan2(b, a) * (180 / Math.PI))
|
|
670
|
+
];
|
|
671
|
+
}
|
|
632
672
|
|
|
633
673
|
//#endregion
|
|
634
674
|
//#region src/config.ts
|
|
@@ -658,7 +698,8 @@ function defaultConfig() {
|
|
|
658
698
|
highContrast: false
|
|
659
699
|
},
|
|
660
700
|
autoFlip: true,
|
|
661
|
-
pastel: false
|
|
701
|
+
pastel: false,
|
|
702
|
+
inferRole: true
|
|
662
703
|
};
|
|
663
704
|
}
|
|
664
705
|
let globalConfig = defaultConfig();
|
|
@@ -698,7 +739,8 @@ function configure(config) {
|
|
|
698
739
|
},
|
|
699
740
|
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
700
741
|
autoFlip: config.autoFlip ?? globalConfig.autoFlip,
|
|
701
|
-
pastel: config.pastel ?? globalConfig.pastel
|
|
742
|
+
pastel: config.pastel ?? globalConfig.pastel,
|
|
743
|
+
inferRole: config.inferRole ?? globalConfig.inferRole
|
|
702
744
|
};
|
|
703
745
|
}
|
|
704
746
|
function resetConfig() {
|
|
@@ -721,10 +763,57 @@ function mergeConfig(base, override) {
|
|
|
721
763
|
modes: base.modes,
|
|
722
764
|
shadowTuning: override.shadowTuning ?? base.shadowTuning,
|
|
723
765
|
autoFlip: override.autoFlip ?? base.autoFlip,
|
|
724
|
-
pastel: override.pastel ?? base.pastel
|
|
766
|
+
pastel: override.pastel ?? base.pastel,
|
|
767
|
+
inferRole: override.inferRole ?? base.inferRole
|
|
725
768
|
};
|
|
726
769
|
}
|
|
727
770
|
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/format-guard.ts
|
|
773
|
+
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
774
|
+
/**
|
|
775
|
+
* Throw when a non-native Glaze color space is requested for an export that
|
|
776
|
+
* emits raw CSS or non-Tasty token maps.
|
|
777
|
+
*/
|
|
778
|
+
function assertNativeFormat(format, method) {
|
|
779
|
+
if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
|
|
780
|
+
}
|
|
781
|
+
const SCHEME_FIELDS = [
|
|
782
|
+
{
|
|
783
|
+
field: "light",
|
|
784
|
+
modes: () => true
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
field: "dark",
|
|
788
|
+
modes: (m) => m.dark
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
field: "lightContrast",
|
|
792
|
+
modes: (m) => m.highContrast
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
field: "darkContrast",
|
|
796
|
+
modes: (m) => m.dark && m.highContrast
|
|
797
|
+
}
|
|
798
|
+
];
|
|
799
|
+
/**
|
|
800
|
+
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
801
|
+
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
802
|
+
* safe chroma (`computeSafeChromaOKLCH`).
|
|
803
|
+
*/
|
|
804
|
+
function assertAllPastel(resolved, modes) {
|
|
805
|
+
const nonPastel = [];
|
|
806
|
+
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
807
|
+
if (!active(modes)) continue;
|
|
808
|
+
if (color[field].pastel !== true) {
|
|
809
|
+
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (nonPastel.length === 0) return;
|
|
814
|
+
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (global or per-color) or drop splitHue.`);
|
|
815
|
+
}
|
|
816
|
+
|
|
728
817
|
//#endregion
|
|
729
818
|
//#region src/hc-pair.ts
|
|
730
819
|
function pairNormal(p) {
|
|
@@ -989,6 +1078,22 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
989
1078
|
function metricLuminance(metric, linearRgb) {
|
|
990
1079
|
return metric === "apca" ? apcaLuminanceFromLinearRgb(linearRgb) : gamutClampedLuminance(linearRgb);
|
|
991
1080
|
}
|
|
1081
|
+
const APCA_PRESETS = {
|
|
1082
|
+
preferred: 90,
|
|
1083
|
+
body: 75,
|
|
1084
|
+
content: 60,
|
|
1085
|
+
large: 45,
|
|
1086
|
+
"non-text": 30,
|
|
1087
|
+
min: 15
|
|
1088
|
+
};
|
|
1089
|
+
/**
|
|
1090
|
+
* Resolve an APCA target — a raw Lc number (kept as-is) or an `ApcaPreset`
|
|
1091
|
+
* keyword mapped to its Lc value. The magnitude is forced non-negative.
|
|
1092
|
+
*/
|
|
1093
|
+
function resolveApcaTarget(value) {
|
|
1094
|
+
if (typeof value === "number") return Math.abs(value);
|
|
1095
|
+
return APCA_PRESETS[value];
|
|
1096
|
+
}
|
|
992
1097
|
const CONTRAST_PRESETS = {
|
|
993
1098
|
AA: 4.5,
|
|
994
1099
|
AAA: 7,
|
|
@@ -1005,16 +1110,18 @@ function pickPair(p, isHighContrast) {
|
|
|
1005
1110
|
/**
|
|
1006
1111
|
* Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
|
|
1007
1112
|
* given mode into `{ metric, target }`. Handles the inner metric HC pair and
|
|
1008
|
-
* preset resolution.
|
|
1113
|
+
* preset resolution. `polarity` is passed through to the result for the APCA
|
|
1114
|
+
* branch (it controls argument order in the solver); WCAG ignores it.
|
|
1009
1115
|
*/
|
|
1010
|
-
function resolveContrastForMode(spec, isHighContrast) {
|
|
1116
|
+
function resolveContrastForMode(spec, isHighContrast, polarity) {
|
|
1011
1117
|
if (typeof spec === "number" || typeof spec === "string") return {
|
|
1012
1118
|
metric: "wcag",
|
|
1013
1119
|
target: resolveMinContrast(spec)
|
|
1014
1120
|
};
|
|
1015
1121
|
if ("apca" in spec) return {
|
|
1016
1122
|
metric: "apca",
|
|
1017
|
-
target:
|
|
1123
|
+
target: resolveApcaTarget(pickPair(spec.apca, isHighContrast)),
|
|
1124
|
+
polarity: polarity ?? "fg"
|
|
1018
1125
|
};
|
|
1019
1126
|
return {
|
|
1020
1127
|
metric: "wcag",
|
|
@@ -1079,20 +1186,25 @@ function cachedLuminance(metric, h, s, t, pastel) {
|
|
|
1079
1186
|
/**
|
|
1080
1187
|
* Score a candidate luminance against the base for a metric. Returns a value
|
|
1081
1188
|
* that is `>= target` exactly when the floor is met (WCAG ratio, or APCA Lc
|
|
1082
|
-
* magnitude).
|
|
1189
|
+
* magnitude). For APCA, `polarity` selects the argument order: `'fg'` (the
|
|
1190
|
+
* default) treats the candidate as the text against a background base
|
|
1191
|
+
* (`apcaContrast(yCandidate, yBase)`); `'bg'` treats the candidate as the
|
|
1192
|
+
* background (`apcaContrast(yBase, yCandidate)`). The magnitude is taken
|
|
1193
|
+
* either way. WCAG is symmetric, so polarity is ignored there.
|
|
1083
1194
|
*/
|
|
1084
|
-
function metricScore(metric, yCandidate, yBase) {
|
|
1195
|
+
function metricScore(metric, yCandidate, yBase, polarity) {
|
|
1085
1196
|
if (metric === "wcag") return contrastRatioFromLuminance(yCandidate, yBase);
|
|
1086
|
-
|
|
1197
|
+
const lc = polarity === "bg" ? apcaContrast(yBase, yCandidate) : apcaContrast(yCandidate, yBase);
|
|
1198
|
+
return Math.abs(lc);
|
|
1087
1199
|
}
|
|
1088
1200
|
/**
|
|
1089
1201
|
* Binary search one branch `[lo, hi]` for the position nearest to `anchor`
|
|
1090
1202
|
* that meets `target`. The domain is whatever `lum` interprets (tone 0–1 or
|
|
1091
1203
|
* mix parameter 0–1); the search is identical in both cases.
|
|
1092
1204
|
*/
|
|
1093
|
-
function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anchor) {
|
|
1094
|
-
const scoreLo = metricScore(metric, lum(lo), yBase);
|
|
1095
|
-
const scoreHi = metricScore(metric, lum(hi), yBase);
|
|
1205
|
+
function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anchor, polarity) {
|
|
1206
|
+
const scoreLo = metricScore(metric, lum(lo), yBase, polarity);
|
|
1207
|
+
const scoreHi = metricScore(metric, lum(hi), yBase, polarity);
|
|
1096
1208
|
if (scoreLo < target && scoreHi < target) return scoreLo >= scoreHi ? {
|
|
1097
1209
|
pos: lo,
|
|
1098
1210
|
contrast: scoreLo,
|
|
@@ -1107,13 +1219,13 @@ function searchBranch(lum, lo, hi, yBase, metric, target, epsilon, maxIter, anch
|
|
|
1107
1219
|
for (let i = 0; i < maxIter; i++) {
|
|
1108
1220
|
if (high - low < epsilon) break;
|
|
1109
1221
|
const mid = (low + high) / 2;
|
|
1110
|
-
if (metricScore(metric, lum(mid), yBase) >= target) if (mid < anchor) low = mid;
|
|
1222
|
+
if (metricScore(metric, lum(mid), yBase, polarity) >= target) if (mid < anchor) low = mid;
|
|
1111
1223
|
else high = mid;
|
|
1112
1224
|
else if (mid < anchor) high = mid;
|
|
1113
1225
|
else low = mid;
|
|
1114
1226
|
}
|
|
1115
|
-
const scoreLow = metricScore(metric, lum(low), yBase);
|
|
1116
|
-
const scoreHigh = metricScore(metric, lum(high), yBase);
|
|
1227
|
+
const scoreLow = metricScore(metric, lum(low), yBase, polarity);
|
|
1228
|
+
const scoreHigh = metricScore(metric, lum(high), yBase, polarity);
|
|
1117
1229
|
const lowPasses = scoreLow >= target;
|
|
1118
1230
|
const highPasses = scoreHigh >= target;
|
|
1119
1231
|
if (lowPasses && highPasses) return Math.abs(low - anchor) <= Math.abs(high - anchor) ? {
|
|
@@ -1156,8 +1268,8 @@ function wcagToneSeed(yBase, target, darker) {
|
|
|
1156
1268
|
return Math.max(0, Math.min(1, toneFromY(yClamped, REF_EPS) / 100));
|
|
1157
1269
|
}
|
|
1158
1270
|
function solveNearestContrast(opts) {
|
|
1159
|
-
const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower } = opts;
|
|
1160
|
-
const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor);
|
|
1271
|
+
const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower, polarity } = opts;
|
|
1272
|
+
const runBranch = (lower) => lower ? searchBranch(lum, lo, searchAnchor, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity) : searchBranch(lum, searchAnchor, hi, yBase, metric, searchTarget, epsilon, maxIterations, searchAnchor, polarity);
|
|
1161
1273
|
const initialResult = runBranch(initialIsLower);
|
|
1162
1274
|
initialResult.met = initialResult.contrast >= target;
|
|
1163
1275
|
if (initialResult.met && !flip) return {
|
|
@@ -1188,7 +1300,7 @@ function solveNearestContrast(opts) {
|
|
|
1188
1300
|
const extreme = initialIsLower ? lo : hi;
|
|
1189
1301
|
return {
|
|
1190
1302
|
pos: extreme,
|
|
1191
|
-
contrast: metricScore(metric, lum(extreme), yBase),
|
|
1303
|
+
contrast: metricScore(metric, lum(extreme), yBase, polarity),
|
|
1192
1304
|
met: false,
|
|
1193
1305
|
lower: initialIsLower
|
|
1194
1306
|
};
|
|
@@ -1199,11 +1311,11 @@ function solveNearestContrast(opts) {
|
|
|
1199
1311
|
*/
|
|
1200
1312
|
function findToneForContrast(options) {
|
|
1201
1313
|
const { hue, saturation, preferredTone, baseLinearRgb, contrast, toneRange = [0, 1], epsilon = 1e-4, maxIterations = 18, pastel = false } = options;
|
|
1202
|
-
const { metric, target } = contrast;
|
|
1314
|
+
const { metric, target, polarity } = contrast;
|
|
1203
1315
|
const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
|
|
1204
1316
|
const yBase = metricLuminance(metric, baseLinearRgb);
|
|
1205
1317
|
const lum = (t) => cachedLuminance(metric, hue, saturation, t, pastel);
|
|
1206
|
-
const scorePref = metricScore(metric, lum(preferredTone), yBase);
|
|
1318
|
+
const scorePref = metricScore(metric, lum(preferredTone), yBase, polarity);
|
|
1207
1319
|
if (scorePref >= searchTarget) return {
|
|
1208
1320
|
tone: preferredTone,
|
|
1209
1321
|
contrast: scorePref,
|
|
@@ -1223,7 +1335,7 @@ function findToneForContrast(options) {
|
|
|
1223
1335
|
met: false,
|
|
1224
1336
|
branch: "preferred"
|
|
1225
1337
|
};
|
|
1226
|
-
else initialIsDarker = metricScore(metric, lum(minT), yBase) >= metricScore(metric, lum(maxT), yBase);
|
|
1338
|
+
else initialIsDarker = metricScore(metric, lum(minT), yBase, polarity) >= metricScore(metric, lum(maxT), yBase, polarity);
|
|
1227
1339
|
const solved = solveNearestContrast({
|
|
1228
1340
|
lum,
|
|
1229
1341
|
yBase,
|
|
@@ -1237,7 +1349,8 @@ function findToneForContrast(options) {
|
|
|
1237
1349
|
epsilon,
|
|
1238
1350
|
maxIterations,
|
|
1239
1351
|
flip: options.flip ?? false,
|
|
1240
|
-
initialIsLower: initialIsDarker
|
|
1352
|
+
initialIsLower: initialIsDarker,
|
|
1353
|
+
polarity
|
|
1241
1354
|
});
|
|
1242
1355
|
return {
|
|
1243
1356
|
tone: solved.pos,
|
|
@@ -1253,10 +1366,10 @@ function findToneForContrast(options) {
|
|
|
1253
1366
|
*/
|
|
1254
1367
|
function findValueForMixContrast(options) {
|
|
1255
1368
|
const { preferredValue, baseLinearRgb, contrast, luminanceAtValue, epsilon = 1e-4, maxIterations = 20 } = options;
|
|
1256
|
-
const { metric, target } = contrast;
|
|
1369
|
+
const { metric, target, polarity } = contrast;
|
|
1257
1370
|
const searchTarget = metric === "wcag" ? target * 1.01 : target + .5;
|
|
1258
1371
|
const yBase = metricLuminance(metric, baseLinearRgb);
|
|
1259
|
-
const scorePref = metricScore(metric, luminanceAtValue(preferredValue), yBase);
|
|
1372
|
+
const scorePref = metricScore(metric, luminanceAtValue(preferredValue), yBase, polarity);
|
|
1260
1373
|
if (scorePref >= searchTarget) return {
|
|
1261
1374
|
value: preferredValue,
|
|
1262
1375
|
contrast: scorePref,
|
|
@@ -1272,7 +1385,7 @@ function findValueForMixContrast(options) {
|
|
|
1272
1385
|
contrast: scorePref,
|
|
1273
1386
|
met: false
|
|
1274
1387
|
};
|
|
1275
|
-
else initialIsLower = metricScore(metric, luminanceAtValue(0), yBase) >= metricScore(metric, luminanceAtValue(1), yBase);
|
|
1388
|
+
else initialIsLower = metricScore(metric, luminanceAtValue(0), yBase, polarity) >= metricScore(metric, luminanceAtValue(1), yBase, polarity);
|
|
1276
1389
|
const solved = solveNearestContrast({
|
|
1277
1390
|
lum: luminanceAtValue,
|
|
1278
1391
|
yBase,
|
|
@@ -1286,7 +1399,8 @@ function findValueForMixContrast(options) {
|
|
|
1286
1399
|
epsilon,
|
|
1287
1400
|
maxIterations,
|
|
1288
1401
|
flip: options.flip ?? false,
|
|
1289
|
-
initialIsLower
|
|
1402
|
+
initialIsLower,
|
|
1403
|
+
polarity
|
|
1290
1404
|
});
|
|
1291
1405
|
return {
|
|
1292
1406
|
value: solved.pos,
|
|
@@ -1296,6 +1410,114 @@ function findValueForMixContrast(options) {
|
|
|
1296
1410
|
};
|
|
1297
1411
|
}
|
|
1298
1412
|
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/roles.ts
|
|
1415
|
+
const SURFACE_KEYWORDS = new Set([
|
|
1416
|
+
"surface",
|
|
1417
|
+
"bg",
|
|
1418
|
+
"background",
|
|
1419
|
+
"fill",
|
|
1420
|
+
"canvas",
|
|
1421
|
+
"paper",
|
|
1422
|
+
"layer"
|
|
1423
|
+
]);
|
|
1424
|
+
const TEXT_KEYWORDS = new Set([
|
|
1425
|
+
"text",
|
|
1426
|
+
"fg",
|
|
1427
|
+
"foreground",
|
|
1428
|
+
"content",
|
|
1429
|
+
"ink",
|
|
1430
|
+
"label",
|
|
1431
|
+
"stroke"
|
|
1432
|
+
]);
|
|
1433
|
+
const BORDER_KEYWORDS = new Set([
|
|
1434
|
+
"border",
|
|
1435
|
+
"divider",
|
|
1436
|
+
"outline",
|
|
1437
|
+
"separator",
|
|
1438
|
+
"hairline",
|
|
1439
|
+
"rule"
|
|
1440
|
+
]);
|
|
1441
|
+
const ALIAS_TO_ROLE = {
|
|
1442
|
+
surface: "surface",
|
|
1443
|
+
bg: "surface",
|
|
1444
|
+
background: "surface",
|
|
1445
|
+
fill: "surface",
|
|
1446
|
+
canvas: "surface",
|
|
1447
|
+
paper: "surface",
|
|
1448
|
+
layer: "surface",
|
|
1449
|
+
text: "text",
|
|
1450
|
+
fg: "text",
|
|
1451
|
+
foreground: "text",
|
|
1452
|
+
content: "text",
|
|
1453
|
+
ink: "text",
|
|
1454
|
+
label: "text",
|
|
1455
|
+
stroke: "text",
|
|
1456
|
+
border: "border",
|
|
1457
|
+
divider: "border",
|
|
1458
|
+
outline: "border",
|
|
1459
|
+
separator: "border",
|
|
1460
|
+
hairline: "border",
|
|
1461
|
+
rule: "border"
|
|
1462
|
+
};
|
|
1463
|
+
/**
|
|
1464
|
+
* Normalize a `RoleInput` (canonical value or alias) into a canonical `Role`.
|
|
1465
|
+
* Returns `undefined` for unrecognized strings so callers can fall through to
|
|
1466
|
+
* the next step of the resolution chain.
|
|
1467
|
+
*/
|
|
1468
|
+
function normalizeRole(input) {
|
|
1469
|
+
if (input === void 0) return void 0;
|
|
1470
|
+
return ALIAS_TO_ROLE[input];
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Tokenize a color name into lowercase keyword tokens, splitting on
|
|
1474
|
+
* non-alphanumeric boundaries and at camelCase boundaries. Examples:
|
|
1475
|
+
* - `'button-text'` → `['button', 'text']`
|
|
1476
|
+
* - `'inputBg'` → `['input', 'bg']`
|
|
1477
|
+
* - `'card_border-outline'` → `['card', 'border', 'outline']`
|
|
1478
|
+
*/
|
|
1479
|
+
function tokenizeName(name) {
|
|
1480
|
+
const pieces = name.split(/[^0-9a-zA-Z]+/).filter(Boolean);
|
|
1481
|
+
const tokens = [];
|
|
1482
|
+
for (const piece of pieces) {
|
|
1483
|
+
const sub = piece.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean);
|
|
1484
|
+
for (const s of sub) tokens.push(s.toLowerCase());
|
|
1485
|
+
}
|
|
1486
|
+
return tokens;
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Infer a `Role` from a color name by matching its tokens against the role
|
|
1490
|
+
* keyword sets. When multiple tokens match, the **last** recognized token
|
|
1491
|
+
* wins (so `button-text` → `text`, `input-bg` → `surface`, `card-border` →
|
|
1492
|
+
* `border`). Returns `undefined` when no token matches.
|
|
1493
|
+
*/
|
|
1494
|
+
function inferRoleFromName(name) {
|
|
1495
|
+
const tokens = tokenizeName(name);
|
|
1496
|
+
let inferred;
|
|
1497
|
+
for (const token of tokens) if (SURFACE_KEYWORDS.has(token)) inferred = "surface";
|
|
1498
|
+
else if (TEXT_KEYWORDS.has(token)) inferred = "text";
|
|
1499
|
+
else if (BORDER_KEYWORDS.has(token)) inferred = "border";
|
|
1500
|
+
return inferred;
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Map a role to its APCA polarity. `text` and `border` are foreground spots
|
|
1504
|
+
* against their base (the candidate is the text argument); `surface` is the
|
|
1505
|
+
* background (the base is the text argument).
|
|
1506
|
+
*/
|
|
1507
|
+
function roleToPolarity(role) {
|
|
1508
|
+
return role === "surface" ? "bg" : "fg";
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The opposite role of `role`, used when a color with no explicit role and no
|
|
1512
|
+
* inferable name depends on a base: the dependent color plays the opposite
|
|
1513
|
+
* role of its base. `surface` ↔ `text`; `border` is treated as a foreground
|
|
1514
|
+
* spot, so its opposite is `surface`.
|
|
1515
|
+
*/
|
|
1516
|
+
function oppositeRole(role) {
|
|
1517
|
+
if (role === "surface") return "text";
|
|
1518
|
+
return "surface";
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1299
1521
|
//#endregion
|
|
1300
1522
|
//#region src/shadow.ts
|
|
1301
1523
|
/**
|
|
@@ -1502,7 +1724,7 @@ function warnContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
|
|
|
1502
1724
|
* warning when the actual measured contrast drifts below the target.
|
|
1503
1725
|
*/
|
|
1504
1726
|
function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase) {
|
|
1505
|
-
const actual = contrast.metric === "apca" ? Math.abs(apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
|
|
1727
|
+
const actual = contrast.metric === "apca" ? Math.abs(contrast.polarity === "bg" ? apcaContrast(yBase, yColor) : apcaContrast(yColor, yBase)) : contrastRatioFromLuminance(yColor, yBase);
|
|
1506
1728
|
if (actual >= (contrast.metric === "apca" ? contrast.target - CONTRAST_WARN_SLACK_APCA : contrast.target * CONTRAST_WARN_SLACK_WCAG)) return;
|
|
1507
1729
|
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1508
1730
|
if (dedupe(`drift|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`)) return;
|
|
@@ -1541,7 +1763,8 @@ function toOkhslVariant(v) {
|
|
|
1541
1763
|
h: c.h,
|
|
1542
1764
|
s: c.s,
|
|
1543
1765
|
l: c.l,
|
|
1544
|
-
alpha: v.alpha
|
|
1766
|
+
alpha: v.alpha,
|
|
1767
|
+
pastel: v.pastel
|
|
1545
1768
|
};
|
|
1546
1769
|
}
|
|
1547
1770
|
/** Edge adapter: OKHSL-lightness variant → resolved variant (`t`). */
|
|
@@ -1558,8 +1781,53 @@ function toToneVariant(v) {
|
|
|
1558
1781
|
alpha: v.alpha
|
|
1559
1782
|
};
|
|
1560
1783
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1784
|
+
/**
|
|
1785
|
+
* Resolve the role of a base color referenced by `baseName`, returning the
|
|
1786
|
+
* role the *dependent* color should take (the opposite of the base's role).
|
|
1787
|
+
* A base that lives in `defs` recursively resolves and is inverted via
|
|
1788
|
+
* `oppositeRole`; an external base (no local def, e.g. an injected standalone
|
|
1789
|
+
* token) is treated as a background, so the dependent defaults to foreground
|
|
1790
|
+
* (`'text'`).
|
|
1791
|
+
*/
|
|
1792
|
+
function resolveBaseRoleInMap(baseName, defs, inferRole, roles) {
|
|
1793
|
+
if (!baseName) return void 0;
|
|
1794
|
+
const baseDef = defs[baseName];
|
|
1795
|
+
if (!baseDef) return "text";
|
|
1796
|
+
return oppositeRole(resolveRoleInMap(baseName, baseDef, defs, inferRole, roles));
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Role-resolution core that does not need a full `ResolveContext`. Shared by
|
|
1800
|
+
* the resolver (via `resolveRole`) and `verifyContrastDrift`.
|
|
1801
|
+
*/
|
|
1802
|
+
function resolveRoleInMap(name, def, defs, inferRole, roles) {
|
|
1803
|
+
const cached = roles.get(name);
|
|
1804
|
+
if (cached) return cached;
|
|
1805
|
+
let role;
|
|
1806
|
+
if (isShadowDef(def)) role = "surface";
|
|
1807
|
+
else if (isMixDef(def)) role = normalizeRole(def.role) ?? (inferRole ? inferRoleFromName(name) : void 0) ?? resolveBaseRoleInMap(def.base, defs, inferRole, roles) ?? "text";
|
|
1808
|
+
else {
|
|
1809
|
+
const regDef = def;
|
|
1810
|
+
role = normalizeRole(regDef.role) ?? (inferRole ? inferRoleFromName(name) : void 0) ?? resolveBaseRoleInMap(regDef.base, defs, inferRole, roles) ?? "text";
|
|
1811
|
+
}
|
|
1812
|
+
const finalRole = role ?? "text";
|
|
1813
|
+
roles.set(name, finalRole);
|
|
1814
|
+
return finalRole;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Resolve a color's semantic `role` (text / surface / border) per the chain:
|
|
1818
|
+
* 1. explicit `def.role` (normalized)
|
|
1819
|
+
* 2. inferred from the color name when `config.inferRole` is on
|
|
1820
|
+
* 3. opposite of the base's role
|
|
1821
|
+
* 4. `'text'` (foreground) default
|
|
1822
|
+
*
|
|
1823
|
+
* Memoized on `ctx.roles` so the four scheme passes share one resolution.
|
|
1824
|
+
* Shadows have no contrast participation and default to `'surface'`.
|
|
1825
|
+
*/
|
|
1826
|
+
function resolveRole(name, def, ctx) {
|
|
1827
|
+
return resolveRoleInMap(name, def, ctx.defs, ctx.config.inferRole, ctx.roles);
|
|
1828
|
+
}
|
|
1829
|
+
function resolveContrastSpec(spec, isHighContrast, polarity) {
|
|
1830
|
+
return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast, polarity);
|
|
1563
1831
|
}
|
|
1564
1832
|
/**
|
|
1565
1833
|
* Apply the relative-tone delta against a base, honoring `flip`.
|
|
@@ -1581,13 +1849,14 @@ function resolveRootColor(def, isHighContrast) {
|
|
|
1581
1849
|
satFactor: clamp(def.saturation ?? 1, 0, 1)
|
|
1582
1850
|
};
|
|
1583
1851
|
}
|
|
1584
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue) {
|
|
1852
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effectiveHue, polarity, effectivePastel) {
|
|
1585
1853
|
const baseName = def.base;
|
|
1586
1854
|
const baseResolved = ctx.resolved.get(baseName);
|
|
1587
1855
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
1588
1856
|
const mode = def.mode ?? "auto";
|
|
1589
1857
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
1590
1858
|
const flip = def.flip ?? ctx.config.autoFlip;
|
|
1859
|
+
const pastel = effectivePastel;
|
|
1591
1860
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
1592
1861
|
const baseTone = baseVariant.t * 100;
|
|
1593
1862
|
let preferredTone;
|
|
@@ -1603,10 +1872,10 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1603
1872
|
}
|
|
1604
1873
|
const rawContrast = def.contrast;
|
|
1605
1874
|
if (rawContrast !== void 0) {
|
|
1606
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast);
|
|
1875
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
1607
1876
|
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
1608
1877
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
1609
|
-
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, ctx.config.pastel);
|
|
1878
|
+
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
1610
1879
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
1611
1880
|
let initialDirection;
|
|
1612
1881
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
@@ -1620,7 +1889,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1620
1889
|
toneRange: [0, 1],
|
|
1621
1890
|
initialDirection,
|
|
1622
1891
|
flip,
|
|
1623
|
-
pastel
|
|
1892
|
+
pastel
|
|
1624
1893
|
});
|
|
1625
1894
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
1626
1895
|
return {
|
|
@@ -1635,11 +1904,13 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
1635
1904
|
}
|
|
1636
1905
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1637
1906
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
1638
|
-
if (isMixDef(def)) return resolveMixForScheme(def, ctx, isDark, isHighContrast);
|
|
1907
|
+
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
1639
1908
|
const regDef = def;
|
|
1640
1909
|
const mode = regDef.mode ?? "auto";
|
|
1641
1910
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
1642
1911
|
const effectiveHue = resolveEffectiveHue(ctx.hue, regDef.hue);
|
|
1912
|
+
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
1913
|
+
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
1643
1914
|
let finalTone;
|
|
1644
1915
|
let satFactor;
|
|
1645
1916
|
if (isRoot) {
|
|
@@ -1647,7 +1918,7 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1647
1918
|
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
1648
1919
|
satFactor = root.satFactor;
|
|
1649
1920
|
} else {
|
|
1650
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue);
|
|
1921
|
+
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
1651
1922
|
finalTone = dep.tone;
|
|
1652
1923
|
satFactor = dep.satFactor;
|
|
1653
1924
|
}
|
|
@@ -1658,7 +1929,8 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
1658
1929
|
h: effectiveHue,
|
|
1659
1930
|
s: clamp(finalSat, 0, 1),
|
|
1660
1931
|
t: toneFraction,
|
|
1661
|
-
alpha: regDef.opacity ?? 1
|
|
1932
|
+
alpha: regDef.opacity ?? 1,
|
|
1933
|
+
pastel
|
|
1662
1934
|
};
|
|
1663
1935
|
}
|
|
1664
1936
|
function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
@@ -1667,7 +1939,10 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1667
1939
|
if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
|
|
1668
1940
|
const intensity = isHighContrast ? pairHC(def.intensity) : pairNormal(def.intensity);
|
|
1669
1941
|
const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
|
|
1670
|
-
return
|
|
1942
|
+
return {
|
|
1943
|
+
...toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning)),
|
|
1944
|
+
pastel: def.pastel ?? ctx.config.pastel
|
|
1945
|
+
};
|
|
1671
1946
|
}
|
|
1672
1947
|
function okhslVariantToLinearRgb(v, pastel) {
|
|
1673
1948
|
return okhslToLinearSrgb(v.h, v.s, v.l, pastel);
|
|
@@ -1706,7 +1981,7 @@ function linearRgbToToneVariant(rgb, pastel) {
|
|
|
1706
1981
|
alpha: 1
|
|
1707
1982
|
});
|
|
1708
1983
|
}
|
|
1709
|
-
function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
1984
|
+
function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
1710
1985
|
const baseResolved = ctx.resolved.get(def.base);
|
|
1711
1986
|
const targetResolved = ctx.resolved.get(def.target);
|
|
1712
1987
|
const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
|
|
@@ -1714,15 +1989,17 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1714
1989
|
let t = clamp(isHighContrast ? pairHC(def.value) : pairNormal(def.value), 0, 100) / 100;
|
|
1715
1990
|
const blend = def.blend ?? "opaque";
|
|
1716
1991
|
const space = def.space ?? "okhsl";
|
|
1717
|
-
const
|
|
1718
|
-
const
|
|
1992
|
+
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
1993
|
+
const pastel = def.pastel ?? ctx.config.pastel;
|
|
1994
|
+
const baseLinear = okhslVariantToLinearRgb(baseVariant, baseVariant.pastel ?? ctx.config.pastel);
|
|
1995
|
+
const targetLinear = okhslVariantToLinearRgb(targetVariant, targetVariant.pastel ?? ctx.config.pastel);
|
|
1719
1996
|
if (def.contrast !== void 0) {
|
|
1720
|
-
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast);
|
|
1997
|
+
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, polarity);
|
|
1721
1998
|
const metric = resolvedContrast.metric;
|
|
1722
1999
|
let luminanceAt;
|
|
1723
2000
|
if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
1724
2001
|
else luminanceAt = (v) => {
|
|
1725
|
-
return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v,
|
|
2002
|
+
return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v, pastel));
|
|
1726
2003
|
};
|
|
1727
2004
|
t = findValueForMixContrast({
|
|
1728
2005
|
preferredValue: t,
|
|
@@ -1733,19 +2010,28 @@ function resolveMixForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
1733
2010
|
flip: ctx.config.autoFlip
|
|
1734
2011
|
}).value;
|
|
1735
2012
|
}
|
|
1736
|
-
if (blend === "transparent") return
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
}
|
|
2013
|
+
if (blend === "transparent") return {
|
|
2014
|
+
...toToneVariant({
|
|
2015
|
+
h: targetVariant.h,
|
|
2016
|
+
s: targetVariant.s,
|
|
2017
|
+
l: targetVariant.l,
|
|
2018
|
+
alpha: clamp(t, 0, 1)
|
|
2019
|
+
}),
|
|
2020
|
+
pastel
|
|
2021
|
+
};
|
|
2022
|
+
if (space === "srgb") return {
|
|
2023
|
+
...linearRgbToToneVariant(linearSrgbLerp(baseLinear, targetLinear, t), pastel),
|
|
2024
|
+
pastel
|
|
2025
|
+
};
|
|
2026
|
+
return {
|
|
2027
|
+
...toToneVariant({
|
|
2028
|
+
h: mixHue(baseVariant, targetVariant, t),
|
|
2029
|
+
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
2030
|
+
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
2031
|
+
alpha: 1
|
|
2032
|
+
}),
|
|
2033
|
+
pastel
|
|
2034
|
+
};
|
|
1749
2035
|
}
|
|
1750
2036
|
function defMode(def) {
|
|
1751
2037
|
if (isShadowDef(def) || isMixDef(def)) return void 0;
|
|
@@ -1796,7 +2082,8 @@ function seedField(order, ctx, field, source) {
|
|
|
1796
2082
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
1797
2083
|
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
1798
2084
|
*/
|
|
1799
|
-
function verifyContrastDrift(order, defs, result) {
|
|
2085
|
+
function verifyContrastDrift(order, defs, result, config) {
|
|
2086
|
+
const roles = /* @__PURE__ */ new Map();
|
|
1800
2087
|
for (const name of order) {
|
|
1801
2088
|
const def = defs[name];
|
|
1802
2089
|
if (isShadowDef(def) || isMixDef(def)) continue;
|
|
@@ -1805,6 +2092,7 @@ function verifyContrastDrift(order, defs, result) {
|
|
|
1805
2092
|
const color = result.get(name);
|
|
1806
2093
|
const base = result.get(regDef.base);
|
|
1807
2094
|
if (!color || !base) continue;
|
|
2095
|
+
const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
|
|
1808
2096
|
for (const s of [
|
|
1809
2097
|
{
|
|
1810
2098
|
isDark: false,
|
|
@@ -1827,13 +2115,15 @@ function verifyContrastDrift(order, defs, result) {
|
|
|
1827
2115
|
field: "darkContrast"
|
|
1828
2116
|
}
|
|
1829
2117
|
]) {
|
|
1830
|
-
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast);
|
|
2118
|
+
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, polarity);
|
|
1831
2119
|
const cVariant = color[s.field];
|
|
1832
2120
|
const bVariant = base[s.field];
|
|
1833
2121
|
const cOkhsl = toOkhslVariant(cVariant);
|
|
1834
2122
|
const bOkhsl = toOkhslVariant(bVariant);
|
|
1835
|
-
const
|
|
1836
|
-
const
|
|
2123
|
+
const cPastel = cVariant.pastel ?? config.pastel;
|
|
2124
|
+
const bPastel = bVariant.pastel ?? config.pastel;
|
|
2125
|
+
const yC = metricLuminance(spec.metric, okhslToLinearSrgb(cOkhsl.h, cOkhsl.s, cOkhsl.l, cPastel));
|
|
2126
|
+
const yB = metricLuminance(spec.metric, okhslToLinearSrgb(bOkhsl.h, bOkhsl.s, bOkhsl.l, bPastel));
|
|
1837
2127
|
warnContrastDrift(name, s.isDark, s.isHighContrast, spec, yC, yB);
|
|
1838
2128
|
}
|
|
1839
2129
|
}
|
|
@@ -1846,7 +2136,8 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
1846
2136
|
saturation,
|
|
1847
2137
|
defs,
|
|
1848
2138
|
resolved: /* @__PURE__ */ new Map(),
|
|
1849
|
-
config
|
|
2139
|
+
config,
|
|
2140
|
+
roles: /* @__PURE__ */ new Map()
|
|
1850
2141
|
};
|
|
1851
2142
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
1852
2143
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
@@ -1866,21 +2157,125 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
1866
2157
|
darkContrast: darkHCMap.get(name),
|
|
1867
2158
|
mode: defMode(defs[name])
|
|
1868
2159
|
});
|
|
1869
|
-
verifyContrastDrift(order, defs, result);
|
|
2160
|
+
verifyContrastDrift(order, defs, result, config);
|
|
1870
2161
|
return result;
|
|
1871
2162
|
}
|
|
1872
2163
|
|
|
2164
|
+
//#endregion
|
|
2165
|
+
//#region src/channels.ts
|
|
2166
|
+
/**
|
|
2167
|
+
* Hue channel planning for `splitHue` exports.
|
|
2168
|
+
*
|
|
2169
|
+
* Builds per-color hue var references and scheme-independent `--*-hue`
|
|
2170
|
+
* declarations for oklch CSS / Tasty output when every color is pastel.
|
|
2171
|
+
*/
|
|
2172
|
+
const ACHROMATIC_EPSILON = 1e-6;
|
|
2173
|
+
function cssProp(prefix, name, suffix) {
|
|
2174
|
+
return `--${prefix}${name}${suffix}`;
|
|
2175
|
+
}
|
|
2176
|
+
function isAchromatic(v) {
|
|
2177
|
+
return v.s <= ACHROMATIC_EPSILON;
|
|
2178
|
+
}
|
|
2179
|
+
function themeHuePlan(name, def, variant, ctx) {
|
|
2180
|
+
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2181
|
+
hueVar: "",
|
|
2182
|
+
inline: true,
|
|
2183
|
+
declarations: []
|
|
2184
|
+
};
|
|
2185
|
+
const regDef = def;
|
|
2186
|
+
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2187
|
+
if (regDef.hue === void 0) return {
|
|
2188
|
+
hueVar: baseHueVar,
|
|
2189
|
+
inline: false,
|
|
2190
|
+
declarations: []
|
|
2191
|
+
};
|
|
2192
|
+
const parsed = parseRelativeOrAbsolute(regDef.hue);
|
|
2193
|
+
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2194
|
+
if (parsed.relative) {
|
|
2195
|
+
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2196
|
+
const magnitude = Math.abs(parsed.value);
|
|
2197
|
+
const value = `calc(var(--${ctx.baseName}-hue) ${sign} ${magnitude})`;
|
|
2198
|
+
return {
|
|
2199
|
+
hueVar: `var(${prop})`,
|
|
2200
|
+
inline: false,
|
|
2201
|
+
declarations: [{
|
|
2202
|
+
prop,
|
|
2203
|
+
value
|
|
2204
|
+
}]
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
const absHue = (parsed.value % 360 + 360) % 360;
|
|
2208
|
+
return {
|
|
2209
|
+
hueVar: `var(${prop})`,
|
|
2210
|
+
inline: false,
|
|
2211
|
+
declarations: [{
|
|
2212
|
+
prop,
|
|
2213
|
+
value: String(absHue)
|
|
2214
|
+
}]
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
function standaloneHuePlan(name, variant, ctx) {
|
|
2218
|
+
if (isAchromatic(variant)) return {
|
|
2219
|
+
hueVar: "",
|
|
2220
|
+
inline: true,
|
|
2221
|
+
declarations: []
|
|
2222
|
+
};
|
|
2223
|
+
const hue = ctx.resolvedHue ?? variant.h;
|
|
2224
|
+
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2225
|
+
return {
|
|
2226
|
+
hueVar: `var(${prop})`,
|
|
2227
|
+
inline: false,
|
|
2228
|
+
declarations: [{
|
|
2229
|
+
prop,
|
|
2230
|
+
value: String(hue)
|
|
2231
|
+
}]
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function buildHuePlan(name, def, variant, ctx) {
|
|
2235
|
+
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2236
|
+
return themeHuePlan(name, def, variant, ctx);
|
|
2237
|
+
}
|
|
2238
|
+
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2239
|
+
function collectHueDeclarations(resolved, ctx) {
|
|
2240
|
+
if (ctx.emitDeclarations === false) return [];
|
|
2241
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2242
|
+
const out = [];
|
|
2243
|
+
const push = (decl) => {
|
|
2244
|
+
if (seen.has(decl.prop)) return;
|
|
2245
|
+
seen.add(decl.prop);
|
|
2246
|
+
out.push(decl);
|
|
2247
|
+
};
|
|
2248
|
+
if (ctx.mode === "theme") push({
|
|
2249
|
+
prop: `--${ctx.baseName}-hue`,
|
|
2250
|
+
value: String(ctx.seedHue)
|
|
2251
|
+
});
|
|
2252
|
+
for (const [name, color] of resolved) {
|
|
2253
|
+
const def = ctx.defs[name];
|
|
2254
|
+
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2255
|
+
for (const decl of plan.declarations) push(decl);
|
|
2256
|
+
}
|
|
2257
|
+
return out;
|
|
2258
|
+
}
|
|
2259
|
+
function buildHuePlans(resolved, ctx) {
|
|
2260
|
+
const plans = /* @__PURE__ */ new Map();
|
|
2261
|
+
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
2262
|
+
return plans;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
1873
2265
|
//#endregion
|
|
1874
2266
|
//#region src/formatters.ts
|
|
1875
2267
|
/**
|
|
1876
2268
|
* Output formatting for resolved color maps.
|
|
1877
2269
|
*
|
|
1878
2270
|
* Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
|
|
1879
|
-
* `oklch`) and the
|
|
2271
|
+
* `oklch`) and the token-map shapes Glaze emits:
|
|
1880
2272
|
* - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
|
|
1881
2273
|
* - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
|
|
1882
2274
|
* - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
|
|
1883
2275
|
* - `buildCssMap` — CSS custom property declaration strings per variant.
|
|
2276
|
+
* - `buildDtcgMap` — W3C DTCG (2025.10) token documents, one per scheme.
|
|
2277
|
+
* - `buildDtcgResolver` — W3C DTCG Resolver-Module document (one modifier, a context per scheme).
|
|
2278
|
+
* - `buildTailwindMap` — Tailwind v4 `@theme` block + dark/HC overrides.
|
|
1884
2279
|
*/
|
|
1885
2280
|
const formatters = {
|
|
1886
2281
|
okhsl: formatOkhsl,
|
|
@@ -1892,12 +2287,37 @@ function fmt(value, decimals) {
|
|
|
1892
2287
|
return parseFloat(value.toFixed(decimals)).toString();
|
|
1893
2288
|
}
|
|
1894
2289
|
function formatVariant(v, format = "okhsl", pastel = false) {
|
|
2290
|
+
const effectivePastel = v.pastel ?? pastel;
|
|
2291
|
+
let base;
|
|
2292
|
+
if (format === "okhst") base = formatOkhst(v.h, v.s * 100, v.t * 100, effectivePastel);
|
|
2293
|
+
else {
|
|
2294
|
+
const { l } = variantToOkhsl(v);
|
|
2295
|
+
base = formatters[format](v.h, v.s * 100, l * 100, effectivePastel);
|
|
2296
|
+
}
|
|
2297
|
+
if (v.alpha >= 1) return base;
|
|
2298
|
+
const closing = base.lastIndexOf(")");
|
|
2299
|
+
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Format a resolved variant as `oklch(L C <hueVar>)`, splicing a CSS hue var
|
|
2303
|
+
* for `splitHue` exports. Falls back to inline when the plan is inline.
|
|
2304
|
+
*/
|
|
2305
|
+
function formatVariantHue(v, plan, pastel = false) {
|
|
2306
|
+
const effectivePastel = v.pastel ?? pastel;
|
|
1895
2307
|
const { l } = variantToOkhsl(v);
|
|
1896
|
-
const
|
|
2308
|
+
const [L, C] = okhslToOklch(v.h, v.s, l, effectivePastel);
|
|
2309
|
+
let base;
|
|
2310
|
+
if (plan.inline) if (v.s <= 1e-6) base = `oklch(${fmt(L, 4)} 0 0)`;
|
|
2311
|
+
else base = formatOklch(v.h, v.s * 100, l * 100, effectivePastel);
|
|
2312
|
+
else base = `oklch(${fmt(L, 4)} ${fmt(C, 4)} ${plan.hueVar})`;
|
|
1897
2313
|
if (v.alpha >= 1) return base;
|
|
1898
2314
|
const closing = base.lastIndexOf(")");
|
|
1899
2315
|
return `${base.slice(0, closing)} / ${fmt(v.alpha, 4)})`;
|
|
1900
2316
|
}
|
|
2317
|
+
function formatColorValue(v, format, pastel, huePlan) {
|
|
2318
|
+
if (format === "oklch" && huePlan !== void 0) return formatVariantHue(v, huePlan, pastel);
|
|
2319
|
+
return formatVariant(v, format, pastel);
|
|
2320
|
+
}
|
|
1901
2321
|
function resolveModes(override) {
|
|
1902
2322
|
const cfg = getConfig();
|
|
1903
2323
|
return {
|
|
@@ -1905,18 +2325,36 @@ function resolveModes(override) {
|
|
|
1905
2325
|
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
1906
2326
|
};
|
|
1907
2327
|
}
|
|
1908
|
-
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false) {
|
|
2328
|
+
function buildTokenMap(resolved, prefix, states, modes, format = "okhsl", pastel = false, channelCtx) {
|
|
1909
2329
|
const tokens = {};
|
|
2330
|
+
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2331
|
+
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2332
|
+
const emitDecls = channelCtx.emitDeclarations !== false;
|
|
2333
|
+
if (emitDecls && channelCtx.mode === "theme") tokens[`#${channelCtx.baseName}-hue`] = { "": String(channelCtx.seedHue) };
|
|
2334
|
+
for (const [name, color] of resolved) {
|
|
2335
|
+
const plan = huePlans.get(name);
|
|
2336
|
+
if (emitDecls) for (const decl of plan.declarations) {
|
|
2337
|
+
const key = `#${decl.prop.slice(2)}`;
|
|
2338
|
+
if (!(key in tokens)) tokens[key] = { "": decl.value };
|
|
2339
|
+
}
|
|
2340
|
+
const colorKey = `#${prefix}${name}`;
|
|
2341
|
+
tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2342
|
+
}
|
|
2343
|
+
return tokens;
|
|
2344
|
+
}
|
|
1910
2345
|
for (const [name, color] of resolved) {
|
|
1911
2346
|
const key = `#${prefix}${name}`;
|
|
1912
|
-
|
|
1913
|
-
if (modes.dark) entry[states.dark] = formatVariant(color.dark, format, pastel);
|
|
1914
|
-
if (modes.highContrast) entry[states.highContrast] = formatVariant(color.lightContrast, format, pastel);
|
|
1915
|
-
if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatVariant(color.darkContrast, format, pastel);
|
|
1916
|
-
tokens[key] = entry;
|
|
2347
|
+
tokens[key] = buildTokenEntry(color, states, modes, format, pastel);
|
|
1917
2348
|
}
|
|
1918
2349
|
return tokens;
|
|
1919
2350
|
}
|
|
2351
|
+
function buildTokenEntry(color, states, modes, format, pastel, huePlan) {
|
|
2352
|
+
const entry = { "": formatColorValue(color.light, format, pastel, huePlan) };
|
|
2353
|
+
if (modes.dark) entry[states.dark] = formatColorValue(color.dark, format, pastel, huePlan);
|
|
2354
|
+
if (modes.highContrast) entry[states.highContrast] = formatColorValue(color.lightContrast, format, pastel, huePlan);
|
|
2355
|
+
if (modes.dark && modes.highContrast) entry[`${states.dark} & ${states.highContrast}`] = formatColorValue(color.darkContrast, format, pastel, huePlan);
|
|
2356
|
+
return entry;
|
|
2357
|
+
}
|
|
1920
2358
|
function buildFlatTokenMap(resolved, prefix, modes, format = "okhsl", pastel = false) {
|
|
1921
2359
|
const result = { light: {} };
|
|
1922
2360
|
if (modes.dark) result.dark = {};
|
|
@@ -1942,19 +2380,22 @@ function buildJsonMap(resolved, modes, format = "okhsl", pastel = false) {
|
|
|
1942
2380
|
}
|
|
1943
2381
|
return result;
|
|
1944
2382
|
}
|
|
1945
|
-
function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
|
|
2383
|
+
function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCtx) {
|
|
1946
2384
|
const lines = {
|
|
1947
2385
|
light: [],
|
|
1948
2386
|
dark: [],
|
|
1949
2387
|
lightContrast: [],
|
|
1950
2388
|
darkContrast: []
|
|
1951
2389
|
};
|
|
2390
|
+
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2391
|
+
if (huePlans !== void 0 && channelCtx !== void 0) for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
1952
2392
|
for (const [name, color] of resolved) {
|
|
1953
2393
|
const prop = `--${prefix}${name}${suffix}`;
|
|
1954
|
-
|
|
1955
|
-
lines.
|
|
1956
|
-
lines.
|
|
1957
|
-
lines.
|
|
2394
|
+
const plan = huePlans?.get(name);
|
|
2395
|
+
lines.light.push(`${prop}: ${formatColorValue(color.light, format, pastel, plan)};`);
|
|
2396
|
+
lines.dark.push(`${prop}: ${formatColorValue(color.dark, format, pastel, plan)};`);
|
|
2397
|
+
lines.lightContrast.push(`${prop}: ${formatColorValue(color.lightContrast, format, pastel, plan)};`);
|
|
2398
|
+
lines.darkContrast.push(`${prop}: ${formatColorValue(color.darkContrast, format, pastel, plan)};`);
|
|
1958
2399
|
}
|
|
1959
2400
|
return {
|
|
1960
2401
|
light: lines.light.join("\n"),
|
|
@@ -1963,6 +2404,193 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false) {
|
|
|
1963
2404
|
darkContrast: lines.darkContrast.join("\n")
|
|
1964
2405
|
};
|
|
1965
2406
|
}
|
|
2407
|
+
function roundTo(value, decimals) {
|
|
2408
|
+
return parseFloat(value.toFixed(decimals));
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* Build a DTCG `$value` color object for a resolved variant.
|
|
2412
|
+
*
|
|
2413
|
+
* `srgb` (default) emits gamma sRGB components in 0–1 plus a 6-digit `hex`
|
|
2414
|
+
* hint — the most universally understood form (Figma, Tokens Studio, Style
|
|
2415
|
+
* Dictionary). `oklch` emits `[L, C, H]` components with no hex — Glaze-native
|
|
2416
|
+
* and wide-gamut. `alpha` is included only when below 1.
|
|
2417
|
+
*/
|
|
2418
|
+
function dtcgColorValue(v, colorSpace = "srgb", pastel = false) {
|
|
2419
|
+
const effectivePastel = v.pastel ?? pastel;
|
|
2420
|
+
const { l } = variantToOkhsl(v);
|
|
2421
|
+
const alpha = v.alpha < 1 ? roundTo(v.alpha, 6) : void 0;
|
|
2422
|
+
if (colorSpace === "oklch") {
|
|
2423
|
+
const [L, C, H] = okhslToOklch(v.h, v.s, l, effectivePastel);
|
|
2424
|
+
const value = {
|
|
2425
|
+
colorSpace: "oklch",
|
|
2426
|
+
components: [
|
|
2427
|
+
roundTo(L, 6),
|
|
2428
|
+
roundTo(C, 6),
|
|
2429
|
+
roundTo(H, 4)
|
|
2430
|
+
]
|
|
2431
|
+
};
|
|
2432
|
+
if (alpha !== void 0) value.alpha = alpha;
|
|
2433
|
+
return value;
|
|
2434
|
+
}
|
|
2435
|
+
const [r, g, b] = okhslToSrgb(v.h, v.s, l, effectivePastel);
|
|
2436
|
+
const value = {
|
|
2437
|
+
colorSpace: "srgb",
|
|
2438
|
+
components: [
|
|
2439
|
+
roundTo(r, 6),
|
|
2440
|
+
roundTo(g, 6),
|
|
2441
|
+
roundTo(b, 6)
|
|
2442
|
+
],
|
|
2443
|
+
hex: srgbToHex([
|
|
2444
|
+
r,
|
|
2445
|
+
g,
|
|
2446
|
+
b
|
|
2447
|
+
])
|
|
2448
|
+
};
|
|
2449
|
+
if (alpha !== void 0) value.alpha = alpha;
|
|
2450
|
+
return value;
|
|
2451
|
+
}
|
|
2452
|
+
function dtcgToken(v, colorSpace, pastel) {
|
|
2453
|
+
return {
|
|
2454
|
+
$type: "color",
|
|
2455
|
+
$value: dtcgColorValue(v, colorSpace, pastel)
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
/**
|
|
2459
|
+
* Build a `GlazeDtcgResult`: one spec-conformant DTCG token document per
|
|
2460
|
+
* scheme variant, gated by `modes`. Light is always present.
|
|
2461
|
+
*/
|
|
2462
|
+
function buildDtcgMap(resolved, prefix, modes, colorSpace = "srgb", pastel = false) {
|
|
2463
|
+
const light = {};
|
|
2464
|
+
const dark = modes.dark ? {} : void 0;
|
|
2465
|
+
const lightContrast = modes.highContrast ? {} : void 0;
|
|
2466
|
+
const darkContrast = modes.dark && modes.highContrast ? {} : void 0;
|
|
2467
|
+
for (const [name, color] of resolved) {
|
|
2468
|
+
const key = `${prefix}${name}`;
|
|
2469
|
+
light[key] = dtcgToken(color.light, colorSpace, pastel);
|
|
2470
|
+
if (dark) dark[key] = dtcgToken(color.dark, colorSpace, pastel);
|
|
2471
|
+
if (lightContrast) lightContrast[key] = dtcgToken(color.lightContrast, colorSpace, pastel);
|
|
2472
|
+
if (darkContrast) darkContrast[key] = dtcgToken(color.darkContrast, colorSpace, pastel);
|
|
2473
|
+
}
|
|
2474
|
+
return {
|
|
2475
|
+
light,
|
|
2476
|
+
dark,
|
|
2477
|
+
lightContrast,
|
|
2478
|
+
darkContrast
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
/**
|
|
2482
|
+
* Default context names emitted on the `scheme` modifier — the Glaze variant
|
|
2483
|
+
* keys, so the resolver document mirrors `GlazeDtcgResult` exactly.
|
|
2484
|
+
*/
|
|
2485
|
+
const DEFAULT_DTCG_CONTEXT_NAMES = {
|
|
2486
|
+
light: "light",
|
|
2487
|
+
dark: "dark",
|
|
2488
|
+
lightContrast: "lightContrast",
|
|
2489
|
+
darkContrast: "darkContrast"
|
|
2490
|
+
};
|
|
2491
|
+
/**
|
|
2492
|
+
* Wrap a per-scheme `GlazeDtcgResult` into a single W3C DTCG Resolver-Module
|
|
2493
|
+
* document. The light document becomes `sets[setName].sources[0]` (the default
|
|
2494
|
+
* context); each other present variant becomes a `contexts[ctx]` override
|
|
2495
|
+
* array on a single `modifiers[modifierName]`. Absent variants (per the
|
|
2496
|
+
* `modes` already applied to `result`) are omitted — light is always present
|
|
2497
|
+
* and is the modifier `default`. Only the resolver-specific options are read;
|
|
2498
|
+
* `modes` / `colorSpace` were already consumed by the `buildDtcgMap` call that
|
|
2499
|
+
* produced `result`.
|
|
2500
|
+
*/
|
|
2501
|
+
function buildDtcgResolver(result, options) {
|
|
2502
|
+
const setName = options?.setName ?? "base";
|
|
2503
|
+
const modifierName = options?.modifierName ?? "scheme";
|
|
2504
|
+
const ctx = {
|
|
2505
|
+
...DEFAULT_DTCG_CONTEXT_NAMES,
|
|
2506
|
+
...options?.contextNames
|
|
2507
|
+
};
|
|
2508
|
+
const contexts = { [ctx.light]: [] };
|
|
2509
|
+
if (result.dark) contexts[ctx.dark] = [result.dark];
|
|
2510
|
+
if (result.lightContrast) contexts[ctx.lightContrast] = [result.lightContrast];
|
|
2511
|
+
if (result.darkContrast) contexts[ctx.darkContrast] = [result.darkContrast];
|
|
2512
|
+
return {
|
|
2513
|
+
version: options?.version ?? "2025.10",
|
|
2514
|
+
sets: { [setName]: { sources: [result.light] } },
|
|
2515
|
+
modifiers: { [modifierName]: {
|
|
2516
|
+
default: ctx.light,
|
|
2517
|
+
contexts
|
|
2518
|
+
} },
|
|
2519
|
+
resolutionOrder: [{ $ref: `#/sets/${setName}` }, { $ref: `#/modifiers/${modifierName}` }]
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
function tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel) {
|
|
2523
|
+
const lines = {
|
|
2524
|
+
light: [],
|
|
2525
|
+
dark: [],
|
|
2526
|
+
lightContrast: [],
|
|
2527
|
+
darkContrast: []
|
|
2528
|
+
};
|
|
2529
|
+
for (const [name, color] of resolved) {
|
|
2530
|
+
const prop = `--${cssPrefix}${themePrefix}${name}`;
|
|
2531
|
+
lines.light.push(`${prop}: ${formatVariant(color.light, format, pastel)};`);
|
|
2532
|
+
lines.dark.push(`${prop}: ${formatVariant(color.dark, format, pastel)};`);
|
|
2533
|
+
lines.lightContrast.push(`${prop}: ${formatVariant(color.lightContrast, format, pastel)};`);
|
|
2534
|
+
lines.darkContrast.push(`${prop}: ${formatVariant(color.darkContrast, format, pastel)};`);
|
|
2535
|
+
}
|
|
2536
|
+
return lines;
|
|
2537
|
+
}
|
|
2538
|
+
function indentBlock(text, pad) {
|
|
2539
|
+
return text.split("\n").map((line) => line.length === 0 ? line : pad + line).join("\n");
|
|
2540
|
+
}
|
|
2541
|
+
function emitRule(selector, body) {
|
|
2542
|
+
return `${selector} {\n${indentBlock(body, " ")}\n}`;
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Emit a CSS block for a set of declarations scoped by one or more selectors
|
|
2546
|
+
* / at-rules. Class-like selectors concatenate (`.dark.high-contrast`);
|
|
2547
|
+
* at-rules (`@media …`) nest `:root` (or the chained selector) inside.
|
|
2548
|
+
*/
|
|
2549
|
+
function emitScoped(scopes, declarations) {
|
|
2550
|
+
if (declarations.length === 0) return void 0;
|
|
2551
|
+
const atRules = [];
|
|
2552
|
+
let selectorChain = "";
|
|
2553
|
+
for (const scope of scopes) if (scope.startsWith("@")) atRules.push(scope);
|
|
2554
|
+
else selectorChain += scope;
|
|
2555
|
+
let css = emitRule(selectorChain || ":root", declarations.join("\n"));
|
|
2556
|
+
for (const rule of atRules) css = emitRule(rule, css);
|
|
2557
|
+
return css;
|
|
2558
|
+
}
|
|
2559
|
+
/**
|
|
2560
|
+
* Render accumulated per-scheme declaration lines as a Tailwind v4 CSS string:
|
|
2561
|
+
* an `@theme` block (light baseline) plus dark / high-contrast overrides under
|
|
2562
|
+
* the configured selectors. Empty blocks are skipped.
|
|
2563
|
+
*/
|
|
2564
|
+
function emitTailwindCss(lines, modes, darkSelector, highContrastSelector) {
|
|
2565
|
+
const blocks = [];
|
|
2566
|
+
if (lines.light.length > 0) blocks.push(emitRule("@theme", lines.light.join("\n")));
|
|
2567
|
+
if (modes.dark) {
|
|
2568
|
+
const dark = emitScoped([darkSelector], lines.dark);
|
|
2569
|
+
if (dark) blocks.push(dark);
|
|
2570
|
+
}
|
|
2571
|
+
if (modes.highContrast) {
|
|
2572
|
+
const hc = emitScoped([highContrastSelector], lines.lightContrast);
|
|
2573
|
+
if (hc) blocks.push(hc);
|
|
2574
|
+
}
|
|
2575
|
+
if (modes.dark && modes.highContrast) {
|
|
2576
|
+
const dhc = emitScoped([darkSelector, highContrastSelector], lines.darkContrast);
|
|
2577
|
+
if (dhc) blocks.push(dhc);
|
|
2578
|
+
}
|
|
2579
|
+
return blocks.join("\n\n");
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* Build per-scheme declaration lines for a single theme (used by
|
|
2583
|
+
* `theme.tailwind()` and as the palette `buildOne` step).
|
|
2584
|
+
*/
|
|
2585
|
+
function buildTailwindLines(resolved, themePrefix, cssPrefix, format, pastel) {
|
|
2586
|
+
return tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Build a complete Tailwind v4 CSS string for a single theme.
|
|
2590
|
+
*/
|
|
2591
|
+
function buildTailwindMap(resolved, themePrefix, cssPrefix, modes, format, darkSelector, highContrastSelector, pastel = false) {
|
|
2592
|
+
return emitTailwindCss(tailwindLinesFor(resolved, themePrefix, cssPrefix, format, pastel), modes, darkSelector, highContrastSelector);
|
|
2593
|
+
}
|
|
1966
2594
|
|
|
1967
2595
|
//#endregion
|
|
1968
2596
|
//#region src/color-token.ts
|
|
@@ -2306,6 +2934,8 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
2306
2934
|
mode: options?.mode ?? "auto",
|
|
2307
2935
|
flip: options?.flip,
|
|
2308
2936
|
opacity: options?.opacity,
|
|
2937
|
+
pastel: options?.pastel,
|
|
2938
|
+
role: options?.role,
|
|
2309
2939
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
2310
2940
|
};
|
|
2311
2941
|
const defs = { [primary]: valueDef };
|
|
@@ -2346,10 +2976,51 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, effect
|
|
|
2346
2976
|
token: tokenLike,
|
|
2347
2977
|
tasty: tokenLike,
|
|
2348
2978
|
json(options) {
|
|
2349
|
-
|
|
2979
|
+
const format = options?.format ?? "oklch";
|
|
2980
|
+
assertNativeFormat(format, "json");
|
|
2981
|
+
return buildJsonMap(resolveOnce(), resolveModes(options?.modes), format, effectiveConfig.pastel)[primary];
|
|
2350
2982
|
},
|
|
2351
2983
|
css(options) {
|
|
2352
|
-
|
|
2984
|
+
const format = options.format ?? "rgb";
|
|
2985
|
+
assertNativeFormat(format, "css");
|
|
2986
|
+
const resolved = resolveOnce().get(primary);
|
|
2987
|
+
const renamed = new Map([[options.name, resolved]]);
|
|
2988
|
+
let channelCtx;
|
|
2989
|
+
if (options.splitHue && format === "oklch") {
|
|
2990
|
+
assertAllPastel(renamed, resolveModes());
|
|
2991
|
+
channelCtx = {
|
|
2992
|
+
seedHue,
|
|
2993
|
+
baseName: options.name,
|
|
2994
|
+
prefix: "",
|
|
2995
|
+
defs: { [options.name]: defs[primary] },
|
|
2996
|
+
mode: "standalone",
|
|
2997
|
+
resolvedHue: resolved.light.h
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
return buildCssMap(renamed, "", options.suffix ?? "-color", format, effectiveConfig.pastel, channelCtx);
|
|
3001
|
+
},
|
|
3002
|
+
dtcg(options) {
|
|
3003
|
+
const modes = resolveModes(options?.modes);
|
|
3004
|
+
const doc = buildDtcgMap(resolveOnce(), "", modes, options?.colorSpace ?? "srgb", effectiveConfig.pastel);
|
|
3005
|
+
const result = { light: doc.light[primary] };
|
|
3006
|
+
if (doc.dark) result.dark = doc.dark[primary];
|
|
3007
|
+
if (doc.lightContrast) result.lightContrast = doc.lightContrast[primary];
|
|
3008
|
+
if (doc.darkContrast) result.darkContrast = doc.darkContrast[primary];
|
|
3009
|
+
return result;
|
|
3010
|
+
},
|
|
3011
|
+
dtcgResolver(options) {
|
|
3012
|
+
const doc = buildDtcgMap(resolveOnce(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", effectiveConfig.pastel);
|
|
3013
|
+
const name = options.name;
|
|
3014
|
+
const result = { light: { [name]: doc.light[primary] } };
|
|
3015
|
+
if (doc.dark) result.dark = { [name]: doc.dark[primary] };
|
|
3016
|
+
if (doc.lightContrast) result.lightContrast = { [name]: doc.lightContrast[primary] };
|
|
3017
|
+
if (doc.darkContrast) result.darkContrast = { [name]: doc.darkContrast[primary] };
|
|
3018
|
+
return buildDtcgResolver(result, options);
|
|
3019
|
+
},
|
|
3020
|
+
tailwind(options) {
|
|
3021
|
+
const format = options.format ?? "oklch";
|
|
3022
|
+
assertNativeFormat(format, "tailwind");
|
|
3023
|
+
return buildTailwindMap(new Map([[options.name, resolveOnce().get(primary)]]), "", options.namespace ?? "color-", resolveModes(options?.modes), format, options.darkSelector ?? ".dark", options.highContrastSelector ?? ".high-contrast", effectiveConfig.pastel);
|
|
2353
3024
|
},
|
|
2354
3025
|
export: exportData
|
|
2355
3026
|
};
|
|
@@ -2406,6 +3077,8 @@ function createColorToken(input, configOverride) {
|
|
|
2406
3077
|
flip: input.flip,
|
|
2407
3078
|
contrast: input.contrast,
|
|
2408
3079
|
opacity: input.opacity,
|
|
3080
|
+
pastel: input.pastel,
|
|
3081
|
+
role: input.role,
|
|
2409
3082
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
2410
3083
|
} };
|
|
2411
3084
|
if (needsSeedAnchor) {
|
|
@@ -2455,6 +3128,8 @@ function buildOverridesExport(options) {
|
|
|
2455
3128
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
2456
3129
|
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
2457
3130
|
if (options.name !== void 0) out.name = options.name;
|
|
3131
|
+
if (options.pastel !== void 0) out.pastel = options.pastel;
|
|
3132
|
+
if (options.role !== void 0) out.role = options.role;
|
|
2458
3133
|
if (options.base !== void 0) out.base = isGlazeColorToken(options.base) ? options.base.export() : options.base;
|
|
2459
3134
|
return out;
|
|
2460
3135
|
}
|
|
@@ -2470,6 +3145,8 @@ function buildStructuredInputExport(input) {
|
|
|
2470
3145
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
2471
3146
|
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
2472
3147
|
if (input.name !== void 0) out.name = input.name;
|
|
3148
|
+
if (input.pastel !== void 0) out.pastel = input.pastel;
|
|
3149
|
+
if (input.role !== void 0) out.role = input.role;
|
|
2473
3150
|
if (input.base !== void 0) out.base = isGlazeColorToken(input.base) ? input.base.export() : input.base;
|
|
2474
3151
|
return out;
|
|
2475
3152
|
}
|
|
@@ -2490,6 +3167,8 @@ function rehydrateOverrides(data) {
|
|
|
2490
3167
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2491
3168
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2492
3169
|
if (data.name !== void 0) out.name = data.name;
|
|
3170
|
+
if (data.pastel !== void 0) out.pastel = data.pastel;
|
|
3171
|
+
if (data.role !== void 0) out.role = data.role;
|
|
2493
3172
|
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2494
3173
|
return out;
|
|
2495
3174
|
}
|
|
@@ -2505,6 +3184,8 @@ function rehydrateStructuredInput(data) {
|
|
|
2505
3184
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
2506
3185
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
2507
3186
|
if (data.name !== void 0) out.name = data.name;
|
|
3187
|
+
if (data.pastel !== void 0) out.pastel = data.pastel;
|
|
3188
|
+
if (data.role !== void 0) out.role = data.role;
|
|
2508
3189
|
if (data.base !== void 0) out.base = isExportedToken(data.base) ? colorFromExport(data.base) : data.base;
|
|
2509
3190
|
return out;
|
|
2510
3191
|
}
|
|
@@ -2529,16 +3210,6 @@ function colorFromExport(data) {
|
|
|
2529
3210
|
|
|
2530
3211
|
//#endregion
|
|
2531
3212
|
//#region src/palette.ts
|
|
2532
|
-
/**
|
|
2533
|
-
* Palette factory.
|
|
2534
|
-
*
|
|
2535
|
-
* Composes multiple themes into a single token namespace with optional
|
|
2536
|
-
* theme-name prefixes and a "primary theme" that also surfaces an
|
|
2537
|
-
* unprefixed copy of its tokens. All four export methods (`tokens` /
|
|
2538
|
-
* `tasty` / `json` / `css`) share a `buildPaletteOutput` driver that
|
|
2539
|
-
* handles validation, per-theme iteration, prefix resolution, collision
|
|
2540
|
-
* filtering, and primary duplication.
|
|
2541
|
-
*/
|
|
2542
3213
|
function resolvePrefix(options, themeName, defaultPrefix = false) {
|
|
2543
3214
|
const prefix = options?.prefix ?? defaultPrefix;
|
|
2544
3215
|
if (prefix === true) return `${themeName}-`;
|
|
@@ -2578,6 +3249,26 @@ function filterCollisions(resolved, prefix, seen, themeName, isPrimary) {
|
|
|
2578
3249
|
}
|
|
2579
3250
|
return filtered;
|
|
2580
3251
|
}
|
|
3252
|
+
function colorMapFromTheme(theme) {
|
|
3253
|
+
const defs = {};
|
|
3254
|
+
for (const name of theme.list()) {
|
|
3255
|
+
const def = theme.color(name);
|
|
3256
|
+
if (def !== void 0) defs[name] = def;
|
|
3257
|
+
}
|
|
3258
|
+
return defs;
|
|
3259
|
+
}
|
|
3260
|
+
function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue, format, modes, filtered) {
|
|
3261
|
+
if (!splitHue || format !== "oklch") return void 0;
|
|
3262
|
+
assertAllPastel(filtered, modes);
|
|
3263
|
+
return {
|
|
3264
|
+
seedHue: theme.hue,
|
|
3265
|
+
baseName: themeName,
|
|
3266
|
+
prefix: themedPrefix,
|
|
3267
|
+
defs: colorMapFromTheme(theme),
|
|
3268
|
+
mode: "theme",
|
|
3269
|
+
emitDeclarations: passPrefix === themedPrefix
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
2581
3272
|
/**
|
|
2582
3273
|
* Shared per-theme driver for `tokens` / `tasty` / `css`. `json` skips
|
|
2583
3274
|
* this because it doesn't do collision filtering or primary duplication.
|
|
@@ -2591,17 +3282,29 @@ function buildPaletteOutput(themes, paletteOptions, options, buildOne, merge, em
|
|
|
2591
3282
|
const resolved = theme.resolve();
|
|
2592
3283
|
const pastel = theme.getConfig().pastel;
|
|
2593
3284
|
const prefix = resolvePrefix(options, themeName, true);
|
|
2594
|
-
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel));
|
|
2595
|
-
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel));
|
|
3285
|
+
merge(acc, buildOne(filterCollisions(resolved, prefix, seen, themeName), prefix, pastel, themeName, theme));
|
|
3286
|
+
if (themeName === effectivePrimary) merge(acc, buildOne(filterCollisions(resolved, "", seen, themeName, true), "", pastel, themeName, theme));
|
|
2596
3287
|
}
|
|
2597
3288
|
return acc;
|
|
2598
3289
|
}
|
|
2599
3290
|
function createPalette(themes, paletteOptions) {
|
|
2600
3291
|
validatePrimaryTheme(paletteOptions?.primary, themes);
|
|
3292
|
+
const buildDtcgResult = (options) => {
|
|
3293
|
+
const modes = resolveModes(options?.modes);
|
|
3294
|
+
const colorSpace = options?.colorSpace ?? "srgb";
|
|
3295
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, _themeName, _theme) => buildDtcgMap(filtered, prefix, modes, colorSpace, pastel), (acc, part) => {
|
|
3296
|
+
Object.assign(acc.light, part.light);
|
|
3297
|
+
if (part.dark) acc.dark = Object.assign(acc.dark ?? {}, part.dark);
|
|
3298
|
+
if (part.lightContrast) acc.lightContrast = Object.assign(acc.lightContrast ?? {}, part.lightContrast);
|
|
3299
|
+
if (part.darkContrast) acc.darkContrast = Object.assign(acc.darkContrast ?? {}, part.darkContrast);
|
|
3300
|
+
}, () => ({ light: {} }));
|
|
3301
|
+
};
|
|
2601
3302
|
return {
|
|
2602
3303
|
tokens(options) {
|
|
3304
|
+
const format = options?.format ?? "oklch";
|
|
3305
|
+
assertNativeFormat(format, "tokens");
|
|
2603
3306
|
const modes = resolveModes(options?.modes);
|
|
2604
|
-
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes,
|
|
3307
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel) => buildFlatTokenMap(filtered, prefix, modes, format, pastel), (acc, part) => {
|
|
2605
3308
|
for (const variant of Object.keys(part)) {
|
|
2606
3309
|
if (!acc[variant]) acc[variant] = {};
|
|
2607
3310
|
Object.assign(acc[variant], part[variant]);
|
|
@@ -2615,18 +3318,27 @@ function createPalette(themes, paletteOptions) {
|
|
|
2615
3318
|
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2616
3319
|
};
|
|
2617
3320
|
const modes = resolveModes(options?.modes);
|
|
2618
|
-
|
|
3321
|
+
const format = options?.format ?? "okhsl";
|
|
3322
|
+
return buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, themeName, theme) => {
|
|
3323
|
+
return buildTokenMap(filtered, prefix, states, modes, format, pastel, channelCtxForTheme(theme, themeName, prefix, resolvePrefix(options, themeName, true), options?.splitHue, format, modes, filtered));
|
|
3324
|
+
}, (acc, part) => Object.assign(acc, part), () => ({}));
|
|
2619
3325
|
},
|
|
2620
3326
|
json(options) {
|
|
3327
|
+
const format = options?.format ?? "oklch";
|
|
3328
|
+
assertNativeFormat(format, "json");
|
|
2621
3329
|
const modes = resolveModes(options?.modes);
|
|
2622
3330
|
const result = {};
|
|
2623
|
-
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes,
|
|
3331
|
+
for (const [themeName, theme] of Object.entries(themes)) result[themeName] = buildJsonMap(theme.resolve(), modes, format, theme.getConfig().pastel);
|
|
2624
3332
|
return result;
|
|
2625
3333
|
},
|
|
2626
3334
|
css(options) {
|
|
2627
3335
|
const suffix = options?.suffix ?? "-color";
|
|
2628
3336
|
const format = options?.format ?? "rgb";
|
|
2629
|
-
|
|
3337
|
+
assertNativeFormat(format, "css");
|
|
3338
|
+
const modes = resolveModes();
|
|
3339
|
+
const lines = buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, themeName, theme) => {
|
|
3340
|
+
return buildCssMap(filtered, prefix, suffix, format, pastel, channelCtxForTheme(theme, themeName, prefix, resolvePrefix(options, themeName, true), options?.splitHue, format, modes, filtered));
|
|
3341
|
+
}, (acc, part) => {
|
|
2630
3342
|
for (const key of [
|
|
2631
3343
|
"light",
|
|
2632
3344
|
"dark",
|
|
@@ -2645,24 +3357,39 @@ function createPalette(themes, paletteOptions) {
|
|
|
2645
3357
|
lightContrast: lines.lightContrast.join("\n"),
|
|
2646
3358
|
darkContrast: lines.darkContrast.join("\n")
|
|
2647
3359
|
};
|
|
3360
|
+
},
|
|
3361
|
+
dtcg(options) {
|
|
3362
|
+
return buildDtcgResult(options);
|
|
3363
|
+
},
|
|
3364
|
+
dtcgResolver(options) {
|
|
3365
|
+
return buildDtcgResolver(buildDtcgResult(options), options);
|
|
3366
|
+
},
|
|
3367
|
+
tailwind(options) {
|
|
3368
|
+
const modes = resolveModes(options?.modes);
|
|
3369
|
+
const cssPrefix = options?.namespace ?? "color-";
|
|
3370
|
+
const format = options?.format ?? "oklch";
|
|
3371
|
+
assertNativeFormat(format, "tailwind");
|
|
3372
|
+
const darkSelector = options?.darkSelector ?? ".dark";
|
|
3373
|
+
const highContrastSelector = options?.highContrastSelector ?? ".high-contrast";
|
|
3374
|
+
return emitTailwindCss(buildPaletteOutput(themes, paletteOptions, options, (filtered, prefix, pastel, _themeName, _theme) => buildTailwindLines(filtered, prefix, cssPrefix, format, pastel), (acc, part) => {
|
|
3375
|
+
for (const variant of [
|
|
3376
|
+
"light",
|
|
3377
|
+
"dark",
|
|
3378
|
+
"lightContrast",
|
|
3379
|
+
"darkContrast"
|
|
3380
|
+
]) acc[variant].push(...part[variant]);
|
|
3381
|
+
}, () => ({
|
|
3382
|
+
light: [],
|
|
3383
|
+
dark: [],
|
|
3384
|
+
lightContrast: [],
|
|
3385
|
+
darkContrast: []
|
|
3386
|
+
})), modes, darkSelector, highContrastSelector);
|
|
2648
3387
|
}
|
|
2649
3388
|
};
|
|
2650
3389
|
}
|
|
2651
3390
|
|
|
2652
3391
|
//#endregion
|
|
2653
3392
|
//#region src/theme.ts
|
|
2654
|
-
/**
|
|
2655
|
-
* Theme factory.
|
|
2656
|
-
*
|
|
2657
|
-
* Wraps a hue/saturation seed, a mutable `ColorMap`, and an optional
|
|
2658
|
-
* per-theme `GlazeConfigOverride`. Exposes `tokens()` / `tasty()` /
|
|
2659
|
-
* `json()` / `css()` / `resolve()` / `export()` / `extend()`.
|
|
2660
|
-
*
|
|
2661
|
-
* The per-theme config override is **merged over the live global config at
|
|
2662
|
-
* resolve time** so the theme still reacts to later `configure()` calls
|
|
2663
|
-
* for fields it didn't override. The merged config is memoized by
|
|
2664
|
-
* `configVersion` to avoid rebuilding it on every export call.
|
|
2665
|
-
*/
|
|
2666
3393
|
function createTheme(hue, saturation, initialColors, configOverride) {
|
|
2667
3394
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
2668
3395
|
let cache = null;
|
|
@@ -2686,6 +3413,18 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2686
3413
|
function invalidate() {
|
|
2687
3414
|
cache = null;
|
|
2688
3415
|
}
|
|
3416
|
+
function channelCtxFor(options, formatDefault, prefix) {
|
|
3417
|
+
const format = options?.format ?? formatDefault;
|
|
3418
|
+
if (!options?.splitHue || format !== "oklch") return void 0;
|
|
3419
|
+
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3420
|
+
return {
|
|
3421
|
+
seedHue: hue,
|
|
3422
|
+
baseName: options.name ?? "theme",
|
|
3423
|
+
prefix,
|
|
3424
|
+
defs: colorDefs,
|
|
3425
|
+
mode: "theme"
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
2689
3428
|
return {
|
|
2690
3429
|
get hue() {
|
|
2691
3430
|
return hue;
|
|
@@ -2749,8 +3488,10 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2749
3488
|
return new Map(resolveCached());
|
|
2750
3489
|
},
|
|
2751
3490
|
tokens(options) {
|
|
3491
|
+
const format = options?.format ?? "oklch";
|
|
3492
|
+
assertNativeFormat(format, "tokens");
|
|
2752
3493
|
const modes = resolveModes(options?.modes);
|
|
2753
|
-
return buildFlatTokenMap(resolveCached(), "", modes,
|
|
3494
|
+
return buildFlatTokenMap(resolveCached(), "", modes, format, getEffectiveConfig().pastel);
|
|
2754
3495
|
},
|
|
2755
3496
|
tasty(options) {
|
|
2756
3497
|
const cfg = getEffectiveConfig();
|
|
@@ -2759,14 +3500,34 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
2759
3500
|
highContrast: options?.states?.highContrast ?? cfg.states.highContrast
|
|
2760
3501
|
};
|
|
2761
3502
|
const modes = resolveModes(options?.modes);
|
|
2762
|
-
|
|
3503
|
+
const format = options?.format ?? "okhsl";
|
|
3504
|
+
const channelCtx = channelCtxFor(options, "okhsl", "");
|
|
3505
|
+
return buildTokenMap(resolveCached(), "", states, modes, format, cfg.pastel, channelCtx);
|
|
2763
3506
|
},
|
|
2764
3507
|
json(options) {
|
|
3508
|
+
const format = options?.format ?? "oklch";
|
|
3509
|
+
assertNativeFormat(format, "json");
|
|
2765
3510
|
const modes = resolveModes(options?.modes);
|
|
2766
|
-
return buildJsonMap(resolveCached(), modes,
|
|
3511
|
+
return buildJsonMap(resolveCached(), modes, format, getEffectiveConfig().pastel);
|
|
2767
3512
|
},
|
|
2768
3513
|
css(options) {
|
|
2769
|
-
|
|
3514
|
+
const format = options?.format ?? "rgb";
|
|
3515
|
+
assertNativeFormat(format, "css");
|
|
3516
|
+
const channelCtx = channelCtxFor(options, "rgb", "");
|
|
3517
|
+
return buildCssMap(resolveCached(), "", options?.suffix ?? "-color", format, getEffectiveConfig().pastel, channelCtx);
|
|
3518
|
+
},
|
|
3519
|
+
dtcg(options) {
|
|
3520
|
+
const modes = resolveModes(options?.modes);
|
|
3521
|
+
return buildDtcgMap(resolveCached(), "", modes, options?.colorSpace ?? "srgb", getEffectiveConfig().pastel);
|
|
3522
|
+
},
|
|
3523
|
+
dtcgResolver(options) {
|
|
3524
|
+
return buildDtcgResolver(buildDtcgMap(resolveCached(), "", resolveModes(options?.modes), options?.colorSpace ?? "srgb", getEffectiveConfig().pastel), options);
|
|
3525
|
+
},
|
|
3526
|
+
tailwind(options) {
|
|
3527
|
+
const format = options?.format ?? "oklch";
|
|
3528
|
+
assertNativeFormat(format, "tailwind");
|
|
3529
|
+
const modes = resolveModes(options?.modes);
|
|
3530
|
+
return buildTailwindMap(resolveCached(), "", options?.namespace ?? "color-", modes, format, options?.darkSelector ?? ".dark", options?.highContrastSelector ?? ".high-contrast", getEffectiveConfig().pastel);
|
|
2770
3531
|
}
|
|
2771
3532
|
};
|
|
2772
3533
|
}
|
|
@@ -2960,29 +3721,39 @@ glaze.resetConfig = function resetConfig$1() {
|
|
|
2960
3721
|
//#endregion
|
|
2961
3722
|
exports.REF_EPS = REF_EPS;
|
|
2962
3723
|
exports.apcaContrast = apcaContrast;
|
|
3724
|
+
exports.assertAllPastel = assertAllPastel;
|
|
3725
|
+
exports.assertNativeFormat = assertNativeFormat;
|
|
2963
3726
|
exports.contrastRatioFromLuminance = contrastRatioFromLuminance;
|
|
2964
3727
|
exports.cuspLightness = cuspLightness;
|
|
2965
3728
|
exports.findToneForContrast = findToneForContrast;
|
|
2966
3729
|
exports.findValueForMixContrast = findValueForMixContrast;
|
|
2967
3730
|
exports.formatHsl = formatHsl;
|
|
2968
3731
|
exports.formatOkhsl = formatOkhsl;
|
|
3732
|
+
exports.formatOkhst = formatOkhst;
|
|
2969
3733
|
exports.formatOklch = formatOklch;
|
|
2970
3734
|
exports.formatRgb = formatRgb;
|
|
2971
3735
|
exports.fromTone = fromTone;
|
|
2972
3736
|
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
2973
3737
|
exports.glaze = glaze;
|
|
2974
3738
|
exports.hslToSrgb = hslToSrgb;
|
|
3739
|
+
exports.inferRoleFromName = inferRoleFromName;
|
|
3740
|
+
exports.normalizeRole = normalizeRole;
|
|
2975
3741
|
exports.okhslToLinearSrgb = okhslToLinearSrgb;
|
|
2976
3742
|
exports.okhslToOkhst = okhslToOkhst;
|
|
2977
3743
|
exports.okhslToOklab = okhslToOklab;
|
|
3744
|
+
exports.okhslToOklch = okhslToOklch;
|
|
2978
3745
|
exports.okhslToSrgb = okhslToSrgb;
|
|
2979
3746
|
exports.okhstToOkhsl = okhstToOkhsl;
|
|
2980
3747
|
exports.oklabToOkhsl = oklabToOkhsl;
|
|
3748
|
+
exports.oppositeRole = oppositeRole;
|
|
2981
3749
|
exports.parseHex = parseHex;
|
|
2982
3750
|
exports.parseHexAlpha = parseHexAlpha;
|
|
2983
3751
|
exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
|
|
3752
|
+
exports.resolveApcaTarget = resolveApcaTarget;
|
|
2984
3753
|
exports.resolveContrastForMode = resolveContrastForMode;
|
|
2985
3754
|
exports.resolveMinContrast = resolveMinContrast;
|
|
3755
|
+
exports.roleToPolarity = roleToPolarity;
|
|
3756
|
+
exports.srgbToHex = srgbToHex;
|
|
2986
3757
|
exports.srgbToOkhsl = srgbToOkhsl;
|
|
2987
3758
|
exports.toTone = toTone;
|
|
2988
3759
|
exports.toneFromY = toneFromY;
|