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