@tenphi/glaze 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.cjs +319 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -3
- package/dist/index.d.mts +86 -3
- package/dist/index.mjs +319 -78
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +157 -9
- package/docs/methodology.md +21 -1
- package/docs/okhst.md +13 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -701,9 +701,21 @@ function defaultConfig() {
|
|
|
701
701
|
},
|
|
702
702
|
autoFlip: true,
|
|
703
703
|
pastel: false,
|
|
704
|
-
inferRole: true
|
|
704
|
+
inferRole: true,
|
|
705
|
+
contrastLevel: "auto"
|
|
705
706
|
};
|
|
706
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Validate an authored `contrastLevel`. Numbers clamp to 0–100 so a slider
|
|
710
|
+
* can't produce an out-of-range level; a non-finite number throws rather than
|
|
711
|
+
* silently reading as `'auto'`, which would turn a `NaN` slider value into an
|
|
712
|
+
* invisible no-op.
|
|
713
|
+
*/
|
|
714
|
+
function normalizeContrastLevel(value) {
|
|
715
|
+
if (value === void 0 || value === "auto") return "auto";
|
|
716
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`glaze: contrastLevel must be a finite number 0–100 or 'auto' (got ${String(value)}).`);
|
|
717
|
+
return Math.max(0, Math.min(100, value));
|
|
718
|
+
}
|
|
707
719
|
let globalConfig = defaultConfig();
|
|
708
720
|
/**
|
|
709
721
|
* Monotonic counter incremented on every `configure()` / `resetConfig()`
|
|
@@ -742,7 +754,8 @@ function configure(config) {
|
|
|
742
754
|
shadowTuning: config.shadowTuning ?? globalConfig.shadowTuning,
|
|
743
755
|
autoFlip: config.autoFlip ?? globalConfig.autoFlip,
|
|
744
756
|
pastel: false,
|
|
745
|
-
inferRole: config.inferRole ?? globalConfig.inferRole
|
|
757
|
+
inferRole: config.inferRole ?? globalConfig.inferRole,
|
|
758
|
+
contrastLevel: normalizeContrastLevel(config.contrastLevel ?? globalConfig.contrastLevel)
|
|
746
759
|
};
|
|
747
760
|
}
|
|
748
761
|
function resetConfig() {
|
|
@@ -772,7 +785,8 @@ function mergeConfig(base, override) {
|
|
|
772
785
|
shadowTuning: override.shadowTuning ?? base.shadowTuning,
|
|
773
786
|
autoFlip: override.autoFlip ?? base.autoFlip,
|
|
774
787
|
pastel: override.pastel ?? false,
|
|
775
|
-
inferRole: override.inferRole ?? base.inferRole
|
|
788
|
+
inferRole: override.inferRole ?? base.inferRole,
|
|
789
|
+
contrastLevel: normalizeContrastLevel(override.contrastLevel ?? base.contrastLevel)
|
|
776
790
|
};
|
|
777
791
|
}
|
|
778
792
|
/**
|
|
@@ -796,6 +810,8 @@ function freezeConfigForExport(instanceLocal, exportArg) {
|
|
|
796
810
|
inferRole: effective.inferRole
|
|
797
811
|
};
|
|
798
812
|
if (effective.shadowTuning !== void 0) out.shadowTuning = effective.shadowTuning;
|
|
813
|
+
const authoredLevel = exportArg?.contrastLevel ?? instanceLocal?.contrastLevel;
|
|
814
|
+
if (authoredLevel !== void 0) out.contrastLevel = normalizeContrastLevel(authoredLevel);
|
|
799
815
|
return structuredClone(out);
|
|
800
816
|
}
|
|
801
817
|
|
|
@@ -922,6 +938,43 @@ function pairHC(p) {
|
|
|
922
938
|
function clamp(v, min, max) {
|
|
923
939
|
return Math.max(min, Math.min(max, v));
|
|
924
940
|
}
|
|
941
|
+
function lerp(a, b, f) {
|
|
942
|
+
return a + (b - a) * f;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Blend fraction at/above which an un-interpolable HC pair switches from its
|
|
946
|
+
* normal entry to its high-contrast one (level 50).
|
|
947
|
+
*/
|
|
948
|
+
const PAIR_SWITCH = .5;
|
|
949
|
+
/** Blend fraction (0–1) for an authored 0–100 contrast level. */
|
|
950
|
+
function levelFraction(level) {
|
|
951
|
+
return clamp(level, 0, 100) / 100;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
|
|
955
|
+
* in `'auto'` mode (the two-tier normal + high-contrast model).
|
|
956
|
+
*
|
|
957
|
+
* Note `contrastLevel: 0` yields `0`, not `undefined`: "pinned to normal
|
|
958
|
+
* contrast with no high-contrast tier" is a distinct state from `'auto'`.
|
|
959
|
+
*/
|
|
960
|
+
function contrastFraction(config) {
|
|
961
|
+
const level = config.contrastLevel;
|
|
962
|
+
if (typeof level !== "number" || !Number.isFinite(level)) return void 0;
|
|
963
|
+
return levelFraction(level);
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
|
|
967
|
+
*
|
|
968
|
+
* The endpoints return the authored entry *by identity* rather than through
|
|
969
|
+
* float arithmetic — `a + (b - a) * 1` is not bit-exactly `b` in IEEE 754, and
|
|
970
|
+
* levels 0 / 100 must reproduce the classic output exactly.
|
|
971
|
+
*/
|
|
972
|
+
function numberAt(p, f) {
|
|
973
|
+
if (!Array.isArray(p)) return p;
|
|
974
|
+
if (f <= 0) return p[0];
|
|
975
|
+
if (f >= 1) return p[1];
|
|
976
|
+
return lerp(p[0], p[1], f);
|
|
977
|
+
}
|
|
925
978
|
/** Whether a tone value is an extreme keyword (`'max'` / `'min'`). */
|
|
926
979
|
function isExtremeTone(value) {
|
|
927
980
|
return value === "max" || value === "min";
|
|
@@ -966,6 +1019,34 @@ function parseToneValue(value) {
|
|
|
966
1019
|
};
|
|
967
1020
|
}
|
|
968
1021
|
/**
|
|
1022
|
+
* Parse an authored tone pair at a manual-contrast blend fraction, returning
|
|
1023
|
+
* the same normalized shape as {@link parseToneValue}.
|
|
1024
|
+
*
|
|
1025
|
+
* Same-kind numeric pairs interpolate their magnitude — absolute tones
|
|
1026
|
+
* (`[30, 20]` → 25 at level 50) and relative deltas (`['+10', '+20']` → +15).
|
|
1027
|
+
* Everything else is un-interpolable and switches at {@link PAIR_SWITCH}:
|
|
1028
|
+
* extremes carry no magnitude, and a mixed-kind pair would change *which*
|
|
1029
|
+
* resolver branch runs mid-ramp (absolute remap vs. extreme-against-base vs.
|
|
1030
|
+
* base-anchored delta), putting a discontinuity at level 100 — exactly where
|
|
1031
|
+
* the output must stay bit-exact.
|
|
1032
|
+
*
|
|
1033
|
+
* Returns a parsed struct rather than a `ToneValue` so a blended relative
|
|
1034
|
+
* delta never round-trips through a `'+15.5'` string, and so `kind` — which
|
|
1035
|
+
* selects the resolver branch — cannot drift.
|
|
1036
|
+
*/
|
|
1037
|
+
function parseToneValueAt(p, f) {
|
|
1038
|
+
if (!Array.isArray(p)) return parseToneValue(p);
|
|
1039
|
+
if (f <= 0) return parseToneValue(p[0]);
|
|
1040
|
+
if (f >= 1) return parseToneValue(p[1]);
|
|
1041
|
+
const normal = parseToneValue(p[0]);
|
|
1042
|
+
const hc = parseToneValue(p[1]);
|
|
1043
|
+
if (normal.kind === hc.kind && normal.kind !== "extreme") return {
|
|
1044
|
+
kind: normal.kind,
|
|
1045
|
+
value: lerp(normal.value, hc.value, f)
|
|
1046
|
+
};
|
|
1047
|
+
return f < PAIR_SWITCH ? normal : hc;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
969
1050
|
* Compute the effective hue for a color, given the theme seed hue
|
|
970
1051
|
* and an optional per-color hue override.
|
|
971
1052
|
*/
|
|
@@ -1001,7 +1082,9 @@ function isAbsoluteTone(tone) {
|
|
|
1001
1082
|
* - the resolved-variant edge adapter (`variantToOkhsl`),
|
|
1002
1083
|
* - the per-scheme tone mapping that replaced the Möbius dark curve
|
|
1003
1084
|
* (`mapToneForScheme`), the dark desaturation reducer, and the solver's scheme
|
|
1004
|
-
* tone range
|
|
1085
|
+
* tone range,
|
|
1086
|
+
* - the scheme tone window, including the high-contrast bypass and its
|
|
1087
|
+
* continuous generalization under a manual contrast level.
|
|
1005
1088
|
*
|
|
1006
1089
|
* See `docs/okhst.md` for the full specification and the calibrated
|
|
1007
1090
|
* default constants.
|
|
@@ -1100,7 +1183,12 @@ function normalizeToneWindow(win) {
|
|
|
1100
1183
|
/**
|
|
1101
1184
|
* Resolve the active tone window for a scheme as OKHSL-lightness endpoints.
|
|
1102
1185
|
* - HC variants always return the full range `[0, 100]` with the mode eps.
|
|
1103
|
-
* -
|
|
1186
|
+
* - Under a manual `contrastLevel`, each endpoint is interpolated toward that
|
|
1187
|
+
* full range: light `10 → 0` / `100 → 100`, dark `15 → 0` / `95 → 100`. The
|
|
1188
|
+
* endpoints are exact — level 0 returns the configured window and level 100
|
|
1189
|
+
* the same literal the HC branch returns.
|
|
1190
|
+
* - `false` (= "no clamping") is treated as `[0, 100]` with the reference eps,
|
|
1191
|
+
* and is therefore level-invariant.
|
|
1104
1192
|
*/
|
|
1105
1193
|
function activeWindow(isHighContrast, kind, config) {
|
|
1106
1194
|
const win = normalizeToneWindow(kind === "dark" ? config.darkTone : config.lightTone);
|
|
@@ -1109,7 +1197,18 @@ function activeWindow(isHighContrast, kind, config) {
|
|
|
1109
1197
|
hi: 100,
|
|
1110
1198
|
eps: win.eps
|
|
1111
1199
|
};
|
|
1112
|
-
|
|
1200
|
+
const f = contrastFraction(config);
|
|
1201
|
+
if (f === void 0 || f <= 0) return win;
|
|
1202
|
+
if (f >= 1) return {
|
|
1203
|
+
lo: 0,
|
|
1204
|
+
hi: 100,
|
|
1205
|
+
eps: win.eps
|
|
1206
|
+
};
|
|
1207
|
+
return {
|
|
1208
|
+
lo: lerp(win.lo, 0, f),
|
|
1209
|
+
hi: lerp(win.hi, 100, f),
|
|
1210
|
+
eps: win.eps
|
|
1211
|
+
};
|
|
1113
1212
|
}
|
|
1114
1213
|
/**
|
|
1115
1214
|
* Remap an authored tone (0–100) into a scheme window and return the final
|
|
@@ -1133,6 +1232,9 @@ function remapToneToLightness(authorTone, win) {
|
|
|
1133
1232
|
* The window remap uses the mode's render eps to land a final OKHSL
|
|
1134
1233
|
* lightness; that lightness is then re-expressed as canonical tone so
|
|
1135
1234
|
* relative offsets and contrast stay comparable across schemes.
|
|
1235
|
+
*
|
|
1236
|
+
* A manual `contrastLevel` widens the window continuously (see
|
|
1237
|
+
* `activeWindow`); inversion is unaffected.
|
|
1136
1238
|
*/
|
|
1137
1239
|
function mapToneForScheme(authorTone, mode, isDark, isHighContrast, config) {
|
|
1138
1240
|
if (mode === "static") return clamp(authorTone, 0, 100);
|
|
@@ -1147,7 +1249,8 @@ function mapSaturationDark(s, mode, config) {
|
|
|
1147
1249
|
/**
|
|
1148
1250
|
* Tone search range (0–1) for the contrast solver in a given scheme.
|
|
1149
1251
|
* `static` searches the full range; otherwise the scheme window's tone
|
|
1150
|
-
* endpoints (HC bypasses to full range
|
|
1252
|
+
* endpoints (HC bypasses to full range, a manual `contrastLevel` widens
|
|
1253
|
+
* toward it — see `activeWindow`).
|
|
1151
1254
|
*/
|
|
1152
1255
|
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1153
1256
|
if (mode === "static") return [0, 1];
|
|
@@ -1166,7 +1269,8 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
1166
1269
|
* converges quickly.
|
|
1167
1270
|
*
|
|
1168
1271
|
* Public API: `findToneForContrast`, `findValueForMixContrast`,
|
|
1169
|
-
* `resolveMinContrast`, `resolveContrastForMode`, `
|
|
1272
|
+
* `resolveMinContrast`, `resolveContrastForMode`, `resolveContrastForLevel`,
|
|
1273
|
+
* `apcaContrast`.
|
|
1170
1274
|
*/
|
|
1171
1275
|
/**
|
|
1172
1276
|
* Luminance of a linear-sRGB color in the basis the metric expects: WCAG
|
|
@@ -1239,6 +1343,13 @@ function pickPair(p, isHighContrast) {
|
|
|
1239
1343
|
return Array.isArray(p) ? isHighContrast ? p[1] : p[0] : p;
|
|
1240
1344
|
}
|
|
1241
1345
|
/**
|
|
1346
|
+
* The metric a single `ContrastSpec` measures in. Bare numbers and WCAG
|
|
1347
|
+
* presets are `'wcag'`; only an `{ apca }` object is `'apca'`.
|
|
1348
|
+
*/
|
|
1349
|
+
function contrastMetricOf(spec) {
|
|
1350
|
+
return typeof spec === "object" && "apca" in spec ? "apca" : "wcag";
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1242
1353
|
* Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
|
|
1243
1354
|
* given mode into `{ metric, target }`. Handles the inner metric HC pair and
|
|
1244
1355
|
* preset resolution. `polarity` is passed through to the result for the APCA
|
|
@@ -1274,6 +1385,36 @@ function resolveContrastForMode(spec, isHighContrast, polarity, outerExplicitHC)
|
|
|
1274
1385
|
target: resolveWcagTarget(pickPair(spec.wcag, isHighContrast), isHighContrast, !!outerExplicitHC || innerExplicitHC)
|
|
1275
1386
|
};
|
|
1276
1387
|
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Resolve a `contrast` spec — including its outer `[normal, highContrast]`
|
|
1390
|
+
* pair — at a manual contrast `level` (0–100), by resolving both ends and
|
|
1391
|
+
* interpolating the numeric target.
|
|
1392
|
+
*
|
|
1393
|
+
* Level 0 / 100 delegate verbatim to {@link resolveContrastForMode}, so the
|
|
1394
|
+
* endpoints are bit-identical to classic normal / high-contrast resolution.
|
|
1395
|
+
* Resolving each end fully first also means every rule that governs the
|
|
1396
|
+
* escalation still applies at both ends for free: preset mapping, the
|
|
1397
|
+
* `APCA_MAX_LC` clamp, and the explicit-HC suppression that cancels the
|
|
1398
|
+
* `AA → AAA` promotion and the `+15 Lc` enhancement.
|
|
1399
|
+
*
|
|
1400
|
+
* Throws when the two ends measure in different metrics — a WCAG ratio and an
|
|
1401
|
+
* APCA Lc live on different scales, so there is no target between them. Such a
|
|
1402
|
+
* pair is rejected at authoring time by `validateColorDefs`; this is the guard
|
|
1403
|
+
* for direct callers.
|
|
1404
|
+
*/
|
|
1405
|
+
function resolveContrastForLevel(spec, level, polarity) {
|
|
1406
|
+
const outerExplicitHC = Array.isArray(spec);
|
|
1407
|
+
const f = levelFraction(level);
|
|
1408
|
+
const normal = resolveContrastForMode(pairNormal(spec), false, polarity, outerExplicitHC);
|
|
1409
|
+
if (f <= 0) return normal;
|
|
1410
|
+
const hc = resolveContrastForMode(pairHC(spec), true, polarity, outerExplicitHC);
|
|
1411
|
+
if (f >= 1) return hc;
|
|
1412
|
+
if (normal.metric !== hc.metric) throw new Error(`glaze: a "contrast" pair cannot switch metric between its normal (${normal.metric}) and high-contrast (${hc.metric}) entry — the two targets are on different scales. Use one metric for both entries.`);
|
|
1413
|
+
return {
|
|
1414
|
+
...normal,
|
|
1415
|
+
target: lerp(normal.target, hc.target, f)
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1277
1418
|
const APCA_EXPONENTS = {
|
|
1278
1419
|
mainTRC: 2.4,
|
|
1279
1420
|
normBG: .56,
|
|
@@ -1414,7 +1555,7 @@ function wcagToneSeed(yBase, target, darker) {
|
|
|
1414
1555
|
return Math.max(0, Math.min(1, toneFromY(yClamped, REF_EPS) / 100));
|
|
1415
1556
|
}
|
|
1416
1557
|
function solveNearestContrast(opts) {
|
|
1417
|
-
const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, initialIsLower, polarity } = opts;
|
|
1558
|
+
const { lum, yBase, metric, target, searchTarget, lo, hi, searchAnchor, distanceAnchor, epsilon, maxIterations, flip, preferInitial, initialIsLower, polarity } = opts;
|
|
1418
1559
|
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);
|
|
1419
1560
|
const initialResult = runBranch(initialIsLower);
|
|
1420
1561
|
initialResult.met = initialResult.contrast >= target;
|
|
@@ -1425,14 +1566,20 @@ function solveNearestContrast(opts) {
|
|
|
1425
1566
|
if (flip) {
|
|
1426
1567
|
const oppositeResult = (initialIsLower ? distanceAnchor < hi : distanceAnchor > lo) ? runBranch(!initialIsLower) : null;
|
|
1427
1568
|
if (oppositeResult) oppositeResult.met = oppositeResult.contrast >= target;
|
|
1428
|
-
if (initialResult.met && oppositeResult?.met)
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1569
|
+
if (initialResult.met && oppositeResult?.met) {
|
|
1570
|
+
if (preferInitial) return {
|
|
1571
|
+
...initialResult,
|
|
1572
|
+
lower: initialIsLower
|
|
1573
|
+
};
|
|
1574
|
+
return Math.abs(initialResult.pos - distanceAnchor) <= Math.abs(oppositeResult.pos - distanceAnchor) ? {
|
|
1575
|
+
...initialResult,
|
|
1576
|
+
lower: initialIsLower
|
|
1577
|
+
} : {
|
|
1578
|
+
...oppositeResult,
|
|
1579
|
+
lower: !initialIsLower,
|
|
1580
|
+
flipped: true
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1436
1583
|
if (initialResult.met) return {
|
|
1437
1584
|
...initialResult,
|
|
1438
1585
|
lower: initialIsLower
|
|
@@ -1495,6 +1642,7 @@ function findToneForContrast(options) {
|
|
|
1495
1642
|
epsilon,
|
|
1496
1643
|
maxIterations,
|
|
1497
1644
|
flip: options.flip ?? false,
|
|
1645
|
+
preferInitial: options.preferInitial ?? false,
|
|
1498
1646
|
initialIsLower: initialIsDarker,
|
|
1499
1647
|
polarity
|
|
1500
1648
|
});
|
|
@@ -1545,6 +1693,7 @@ function findValueForMixContrast(options) {
|
|
|
1545
1693
|
epsilon,
|
|
1546
1694
|
maxIterations,
|
|
1547
1695
|
flip: options.flip ?? false,
|
|
1696
|
+
preferInitial: false,
|
|
1548
1697
|
initialIsLower,
|
|
1549
1698
|
polarity
|
|
1550
1699
|
});
|
|
@@ -1746,6 +1895,22 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1746
1895
|
* resolver runs. `topoSort` orders defs so each color is processed after
|
|
1747
1896
|
* its base / bg / fg / target dependencies.
|
|
1748
1897
|
*/
|
|
1898
|
+
/**
|
|
1899
|
+
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1900
|
+
*
|
|
1901
|
+
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
1902
|
+
* asks one color to be judged two incompatible ways. There is no target between
|
|
1903
|
+
* them for a manual `contrastLevel` to resolve, and even in `'auto'` mode the
|
|
1904
|
+
* two variants become incomparable. Pick one metric and pair its values
|
|
1905
|
+
* instead: `{ wcag: [4.5, 7] }` or `{ apca: [60, 90] }`.
|
|
1906
|
+
*/
|
|
1907
|
+
function assertConsistentContrastMetric(name, contrast) {
|
|
1908
|
+
if (contrast === void 0 || !Array.isArray(contrast)) return;
|
|
1909
|
+
const normal = contrastMetricOf(pairNormal(contrast));
|
|
1910
|
+
const hc = contrastMetricOf(pairHC(contrast));
|
|
1911
|
+
if (normal === hc) return;
|
|
1912
|
+
throw new Error(`glaze: color "${name}" has a "contrast" pair that switches metric (${normal} → ${hc}). A WCAG ratio and an APCA Lc are different scales; use one metric for both entries, e.g. { ${normal}: [normal, highContrast] }.`);
|
|
1913
|
+
}
|
|
1749
1914
|
function validateColorDefs(defs, externalBases) {
|
|
1750
1915
|
const localNames = new Set(Object.keys(defs));
|
|
1751
1916
|
const allNames = new Set([...localNames, ...externalBases ? externalBases.keys() : []]);
|
|
@@ -1764,9 +1929,11 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1764
1929
|
if (!allNames.has(def.target)) throw new Error(`glaze: mix "${name}" references non-existent target "${def.target}".`);
|
|
1765
1930
|
if (localNames.has(def.base) && isShadowDef(defs[def.base])) throw new Error(`glaze: mix "${name}" base "${def.base}" references a shadow color.`);
|
|
1766
1931
|
if (localNames.has(def.target) && isShadowDef(defs[def.target])) throw new Error(`glaze: mix "${name}" target "${def.target}" references a shadow color.`);
|
|
1932
|
+
assertConsistentContrastMetric(name, def.contrast);
|
|
1767
1933
|
continue;
|
|
1768
1934
|
}
|
|
1769
1935
|
const regDef = def;
|
|
1936
|
+
assertConsistentContrastMetric(name, regDef.contrast);
|
|
1770
1937
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1771
1938
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1772
1939
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
@@ -1883,7 +2050,10 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
1883
2050
|
* Color resolution engine.
|
|
1884
2051
|
*
|
|
1885
2052
|
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
1886
|
-
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name
|
|
2053
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name — or,
|
|
2054
|
+
* under a manual `contrastLevel`, a two-pass solver (light → dark) whose
|
|
2055
|
+
* authored HC pairs, tone window, and contrast targets are interpolated at
|
|
2056
|
+
* that level and whose high-contrast slots mirror the normal ones.
|
|
1887
2057
|
* Owns the per-scheme resolve helpers for regular, shadow, and mix
|
|
1888
2058
|
* color defs.
|
|
1889
2059
|
*
|
|
@@ -1972,11 +2142,27 @@ function resolveRoleInMap(name, def, defs, inferRole, roles) {
|
|
|
1972
2142
|
function resolveRole(name, def, ctx) {
|
|
1973
2143
|
return resolveRoleInMap(name, def, ctx.defs, ctx.config.inferRole, ctx.roles);
|
|
1974
2144
|
}
|
|
1975
|
-
function resolveContrastSpec(spec, isHighContrast, polarity) {
|
|
2145
|
+
function resolveContrastSpec(spec, isHighContrast, config, polarity) {
|
|
2146
|
+
if (!isHighContrast && contrastFraction(config) !== void 0) return resolveContrastForLevel(spec, config.contrastLevel, polarity);
|
|
1976
2147
|
const outerExplicitHC = Array.isArray(spec);
|
|
1977
2148
|
return resolveContrastForMode(isHighContrast ? pairHC(spec) : pairNormal(spec), isHighContrast, polarity, outerExplicitHC);
|
|
1978
2149
|
}
|
|
1979
2150
|
/**
|
|
2151
|
+
* The authored tone for this pass: the pair's own entry in `'auto'` mode, or
|
|
2152
|
+
* the manual-level interpolation. Returns the parsed struct so the `kind` —
|
|
2153
|
+
* which selects the branch in `resolveDependentColor` — is fixed here once.
|
|
2154
|
+
*/
|
|
2155
|
+
function passTone(tone, isHighContrast, config) {
|
|
2156
|
+
const f = contrastFraction(config);
|
|
2157
|
+
return f === void 0 || isHighContrast ? parseToneValue(isHighContrast ? pairHC(tone) : pairNormal(tone)) : parseToneValueAt(tone, f);
|
|
2158
|
+
}
|
|
2159
|
+
/** The authored numeric pair for this pass (shadow `intensity`, mix `value`). */
|
|
2160
|
+
function passNumber(p, isHighContrast, config) {
|
|
2161
|
+
const f = contrastFraction(config);
|
|
2162
|
+
if (f === void 0 || isHighContrast) return isHighContrast ? pairHC(p) : pairNormal(p);
|
|
2163
|
+
return numberAt(p, f);
|
|
2164
|
+
}
|
|
2165
|
+
/**
|
|
1980
2166
|
* Apply the relative-tone delta against a base, honoring `flip`.
|
|
1981
2167
|
*
|
|
1982
2168
|
* When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
|
|
@@ -2002,13 +2188,15 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
2002
2188
|
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2003
2189
|
* intended, since the author asked for the extreme.
|
|
2004
2190
|
*/
|
|
2005
|
-
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved,
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2191
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config) {
|
|
2192
|
+
const extremeLightTone = mapToneForScheme(authorTone, mode, false, isHighContrast, config);
|
|
2193
|
+
const lightBase = getSchemeVariant(baseResolved, false, isHighContrast);
|
|
2194
|
+
const darkBase = getSchemeVariant(baseResolved, true, isHighContrast);
|
|
2195
|
+
const shift = extremeLightTone - lightBase.t * 100;
|
|
2196
|
+
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2008
2197
|
}
|
|
2009
|
-
function resolveRootColor(def, isHighContrast) {
|
|
2010
|
-
|
|
2011
|
-
return clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100);
|
|
2198
|
+
function resolveRootColor(def, isHighContrast, config) {
|
|
2199
|
+
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2012
2200
|
}
|
|
2013
2201
|
/**
|
|
2014
2202
|
* Effective hue and saturation for one color in one scheme.
|
|
@@ -2053,39 +2241,50 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2053
2241
|
const rawTone = def.tone;
|
|
2054
2242
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2055
2243
|
else {
|
|
2056
|
-
const parsed =
|
|
2244
|
+
const parsed = passTone(rawTone, isHighContrast, ctx.config);
|
|
2057
2245
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2058
2246
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2059
2247
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2060
2248
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2061
|
-
else
|
|
2062
|
-
isExtreme = true;
|
|
2063
|
-
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2064
|
-
} else {
|
|
2249
|
+
else {
|
|
2065
2250
|
isExtreme = parsed.kind === "extreme";
|
|
2066
|
-
preferredTone =
|
|
2251
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, ctx.config);
|
|
2252
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
|
|
2067
2253
|
}
|
|
2068
2254
|
}
|
|
2069
2255
|
const rawContrast = def.contrast;
|
|
2070
2256
|
if (rawContrast !== void 0) {
|
|
2071
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2257
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, ctx.config, polarity);
|
|
2072
2258
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2073
2259
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2074
|
-
const
|
|
2260
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2075
2261
|
let initialDirection;
|
|
2076
2262
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2077
2263
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2078
|
-
const
|
|
2079
|
-
const result = findToneForContrast({
|
|
2264
|
+
const solve = {
|
|
2080
2265
|
hue: channels.hue,
|
|
2081
2266
|
saturation: channels.saturation,
|
|
2082
|
-
preferredTone:
|
|
2267
|
+
preferredTone: clamp(preferredTone / 100, preferredRange[0], preferredRange[1]),
|
|
2083
2268
|
baseLinearRgb,
|
|
2084
|
-
contrast: resolvedContrast,
|
|
2085
2269
|
toneRange: [0, 1],
|
|
2086
|
-
initialDirection,
|
|
2087
2270
|
flip,
|
|
2088
2271
|
pastel
|
|
2272
|
+
};
|
|
2273
|
+
const level = contrastFraction(ctx.config);
|
|
2274
|
+
let preferInitial = false;
|
|
2275
|
+
if (level !== void 0 && level > 0 && level < 1) {
|
|
2276
|
+
initialDirection = findToneForContrast({
|
|
2277
|
+
...solve,
|
|
2278
|
+
contrast: resolveContrastForLevel(rawContrast, level < PAIR_SWITCH ? 0 : 100, polarity),
|
|
2279
|
+
initialDirection
|
|
2280
|
+
}).tone * 100 < baseTone ? "darker" : "lighter";
|
|
2281
|
+
preferInitial = true;
|
|
2282
|
+
}
|
|
2283
|
+
const result = findToneForContrast({
|
|
2284
|
+
...solve,
|
|
2285
|
+
contrast: resolvedContrast,
|
|
2286
|
+
initialDirection,
|
|
2287
|
+
preferInitial
|
|
2089
2288
|
});
|
|
2090
2289
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2091
2290
|
return result.tone * 100;
|
|
@@ -2101,7 +2300,7 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2101
2300
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2102
2301
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2103
2302
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2104
|
-
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2303
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, ctx.config), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2105
2304
|
return {
|
|
2106
2305
|
h: channels.hue,
|
|
2107
2306
|
s: channels.saturation,
|
|
@@ -2114,7 +2313,7 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
2114
2313
|
const bgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast));
|
|
2115
2314
|
let fgVariant;
|
|
2116
2315
|
if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
|
|
2117
|
-
const intensity =
|
|
2316
|
+
const intensity = passNumber(def.intensity, isHighContrast, ctx.config);
|
|
2118
2317
|
const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
|
|
2119
2318
|
return {
|
|
2120
2319
|
...toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning)),
|
|
@@ -2163,7 +2362,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2163
2362
|
const targetResolved = ctx.resolved.get(def.target);
|
|
2164
2363
|
const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
|
|
2165
2364
|
const targetVariant = toOkhslVariant(getSchemeVariant(targetResolved, isDark, isHighContrast));
|
|
2166
|
-
let t = clamp(
|
|
2365
|
+
let t = clamp(passNumber(def.value, isHighContrast, ctx.config), 0, 100) / 100;
|
|
2167
2366
|
const blend = def.blend ?? "opaque";
|
|
2168
2367
|
const space = def.space ?? "okhsl";
|
|
2169
2368
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
@@ -2171,7 +2370,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2171
2370
|
const baseLinear = okhslVariantToLinearRgb(baseVariant, baseVariant.pastel ?? ctx.config.pastel);
|
|
2172
2371
|
const targetLinear = okhslVariantToLinearRgb(targetVariant, targetVariant.pastel ?? ctx.config.pastel);
|
|
2173
2372
|
if (def.contrast !== void 0) {
|
|
2174
|
-
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, polarity);
|
|
2373
|
+
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, ctx.config, polarity);
|
|
2175
2374
|
const metric = resolvedContrast.metric;
|
|
2176
2375
|
let luminanceAt;
|
|
2177
2376
|
if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
@@ -2255,9 +2454,14 @@ function seedField(order, ctx, field, source) {
|
|
|
2255
2454
|
}
|
|
2256
2455
|
}
|
|
2257
2456
|
/**
|
|
2258
|
-
* After the
|
|
2457
|
+
* After the passes, surface chromatic contrast drift (§10): a color
|
|
2259
2458
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
2260
2459
|
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
2460
|
+
*
|
|
2461
|
+
* Under a manual `contrastLevel` only the two emitted variants are checked
|
|
2462
|
+
* (the high-contrast slots are mirrors), and the spec is resolved at the level
|
|
2463
|
+
* so the check measures the output against the target it was actually solved
|
|
2464
|
+
* for.
|
|
2261
2465
|
*/
|
|
2262
2466
|
function verifyContrastDrift(order, defs, result, config) {
|
|
2263
2467
|
const roles = /* @__PURE__ */ new Map();
|
|
@@ -2270,7 +2474,7 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2270
2474
|
const base = result.get(regDef.base);
|
|
2271
2475
|
if (!color || !base) continue;
|
|
2272
2476
|
const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
|
|
2273
|
-
|
|
2477
|
+
const schemes = [
|
|
2274
2478
|
{
|
|
2275
2479
|
isDark: false,
|
|
2276
2480
|
isHighContrast: false,
|
|
@@ -2291,8 +2495,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2291
2495
|
isHighContrast: true,
|
|
2292
2496
|
field: "darkContrast"
|
|
2293
2497
|
}
|
|
2294
|
-
]
|
|
2295
|
-
|
|
2498
|
+
];
|
|
2499
|
+
const manual = contrastFraction(config) !== void 0;
|
|
2500
|
+
for (const s of schemes) {
|
|
2501
|
+
if (manual && s.isHighContrast) continue;
|
|
2502
|
+
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
|
|
2296
2503
|
const cVariant = color[s.field];
|
|
2297
2504
|
const bVariant = base[s.field];
|
|
2298
2505
|
const cOkhsl = toOkhslVariant(cVariant);
|
|
@@ -2316,14 +2523,21 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2316
2523
|
roles: /* @__PURE__ */ new Map()
|
|
2317
2524
|
};
|
|
2318
2525
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
2526
|
+
const manual = contrastFraction(config) !== void 0;
|
|
2319
2527
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
2320
|
-
|
|
2321
|
-
|
|
2528
|
+
let lightHCMap = lightMap;
|
|
2529
|
+
if (!manual) {
|
|
2530
|
+
seedField(order, ctx, "lightContrast", lightMap);
|
|
2531
|
+
lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
2532
|
+
}
|
|
2322
2533
|
seedField(order, ctx, "dark", lightMap);
|
|
2323
2534
|
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
2324
2535
|
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
2325
|
-
|
|
2326
|
-
|
|
2536
|
+
let darkHCMap = darkMap;
|
|
2537
|
+
if (!manual) {
|
|
2538
|
+
seedField(order, ctx, "darkContrast", darkMap);
|
|
2539
|
+
darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
2540
|
+
}
|
|
2327
2541
|
const result = /* @__PURE__ */ new Map();
|
|
2328
2542
|
for (const name of order) result.set(name, {
|
|
2329
2543
|
name,
|
|
@@ -2347,7 +2561,9 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2347
2561
|
*
|
|
2348
2562
|
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2349
2563
|
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2350
|
-
* set is emitted only when it actually differs — see `
|
|
2564
|
+
* set is emitted only when it actually differs — see `buildHueDeclarations`.
|
|
2565
|
+
* A color that authors a hue in *either* scheme gets its own var in both, so
|
|
2566
|
+
* the shared `var()` reference stays valid.
|
|
2351
2567
|
*/
|
|
2352
2568
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2353
2569
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2369,23 +2585,28 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2369
2585
|
const regDef = def;
|
|
2370
2586
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2371
2587
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2372
|
-
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2384
|
-
if (authored === void 0) return {
|
|
2588
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) return {
|
|
2589
|
+
hueVar: `var(${prop})`,
|
|
2590
|
+
inline: false,
|
|
2591
|
+
declarations: [{
|
|
2592
|
+
prop,
|
|
2593
|
+
value: String(variant.h)
|
|
2594
|
+
}]
|
|
2595
|
+
};
|
|
2596
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0) return {
|
|
2385
2597
|
hueVar: baseHueVar,
|
|
2386
2598
|
inline: false,
|
|
2387
2599
|
declarations: []
|
|
2388
2600
|
};
|
|
2601
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2602
|
+
if (authored === void 0) return {
|
|
2603
|
+
hueVar: `var(${prop})`,
|
|
2604
|
+
inline: false,
|
|
2605
|
+
declarations: [{
|
|
2606
|
+
prop,
|
|
2607
|
+
value: baseHueVar
|
|
2608
|
+
}]
|
|
2609
|
+
};
|
|
2389
2610
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2390
2611
|
if (parsed.relative) {
|
|
2391
2612
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2432,7 +2653,7 @@ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
|
2432
2653
|
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2433
2654
|
}
|
|
2434
2655
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2435
|
-
function collectHueDeclarations(resolved, ctx, scheme
|
|
2656
|
+
function collectHueDeclarations(resolved, ctx, scheme) {
|
|
2436
2657
|
if (ctx.emitDeclarations === false) return [];
|
|
2437
2658
|
const seen = /* @__PURE__ */ new Set();
|
|
2438
2659
|
const out = [];
|
|
@@ -2453,21 +2674,24 @@ function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
|
2453
2674
|
return out;
|
|
2454
2675
|
}
|
|
2455
2676
|
/**
|
|
2456
|
-
*
|
|
2457
|
-
* light
|
|
2677
|
+
* Hue declarations for both schemes. `dark` is empty when it resolves to the
|
|
2678
|
+
* same hues as light — the common case, with no `darkHue` anywhere.
|
|
2458
2679
|
*
|
|
2459
|
-
* When they differ, the **whole** set is returned rather than only the
|
|
2460
|
-
* whose text changed. A relative declaration reads
|
|
2680
|
+
* When they differ, the **whole** dark set is returned rather than only the
|
|
2681
|
+
* entries whose text changed. A relative declaration reads
|
|
2461
2682
|
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2462
2683
|
* moves with the seed, and custom properties substitute `var()` at
|
|
2463
2684
|
* computed-value time on the declaring element — so re-declaring
|
|
2464
2685
|
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2465
2686
|
* the light-substituted value.
|
|
2466
2687
|
*/
|
|
2467
|
-
function
|
|
2468
|
-
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2688
|
+
function buildHueDeclarations(resolved, ctx) {
|
|
2469
2689
|
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2470
|
-
|
|
2690
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2691
|
+
return {
|
|
2692
|
+
light,
|
|
2693
|
+
dark: dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark
|
|
2694
|
+
};
|
|
2471
2695
|
}
|
|
2472
2696
|
function buildHuePlans(resolved, ctx) {
|
|
2473
2697
|
const plans = /* @__PURE__ */ new Map();
|
|
@@ -2531,19 +2755,35 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
2531
2755
|
if (format === "oklch" && huePlan !== void 0) return formatVariantHue(v, huePlan, pastel);
|
|
2532
2756
|
return formatVariant(v, format, pastel);
|
|
2533
2757
|
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Resolve the effective output modes for an export.
|
|
2760
|
+
*
|
|
2761
|
+
* A global manual `contrastLevel` turns off high-contrast output outright: the
|
|
2762
|
+
* level *is* the contrast preference, so a separate high-contrast tier has no
|
|
2763
|
+
* meaning alongside it. `modes.highContrast` therefore reads as "emit a separate
|
|
2764
|
+
* high-contrast set **when** contrast is automatic" — it goes inert under a
|
|
2765
|
+
* manual level rather than fighting it, and silently, since switching a
|
|
2766
|
+
* preference from auto to manual is normal use and not a mistake to report.
|
|
2767
|
+
*
|
|
2768
|
+
* A level set on a single theme or token does not change which modes are
|
|
2769
|
+
* emitted: sibling themes in a palette may still have a real high-contrast
|
|
2770
|
+
* tier, and the manual one correctly reports its own resolved values there —
|
|
2771
|
+
* the alternative would drop its colors from that tier entirely.
|
|
2772
|
+
*/
|
|
2534
2773
|
function resolveModes(override) {
|
|
2535
2774
|
const cfg = getConfig();
|
|
2536
2775
|
return {
|
|
2537
2776
|
dark: override?.dark ?? cfg.modes.dark,
|
|
2538
|
-
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
2777
|
+
highContrast: contrastFraction(cfg) !== void 0 ? false : override?.highContrast ?? cfg.modes.highContrast
|
|
2539
2778
|
};
|
|
2540
2779
|
}
|
|
2541
2780
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|
|
2542
2781
|
const tokens = {};
|
|
2543
2782
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2544
2783
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2545
|
-
|
|
2546
|
-
|
|
2784
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
2785
|
+
for (const decl of hueDecls.light) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
2786
|
+
if (modes.dark) for (const decl of hueDecls.dark) {
|
|
2547
2787
|
const key = `$${decl.prop.slice(2)}`;
|
|
2548
2788
|
tokens[key] = {
|
|
2549
2789
|
...tokens[key],
|
|
@@ -2600,8 +2840,9 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2600
2840
|
};
|
|
2601
2841
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2602
2842
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2603
|
-
|
|
2604
|
-
for (const decl of
|
|
2843
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
2844
|
+
for (const decl of hueDecls.light) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
2845
|
+
for (const decl of hueDecls.dark) {
|
|
2605
2846
|
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2606
2847
|
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2607
2848
|
}
|
|
@@ -4112,6 +4353,7 @@ exports.parseHex = parseHex;
|
|
|
4112
4353
|
exports.parseHexAlpha = parseHexAlpha;
|
|
4113
4354
|
exports.relativeLuminanceFromLinearRgb = relativeLuminanceFromLinearRgb;
|
|
4114
4355
|
exports.resolveApcaTarget = resolveApcaTarget;
|
|
4356
|
+
exports.resolveContrastForLevel = resolveContrastForLevel;
|
|
4115
4357
|
exports.resolveContrastForMode = resolveContrastForMode;
|
|
4116
4358
|
exports.resolveMinContrast = resolveMinContrast;
|
|
4117
4359
|
exports.roleToPolarity = roleToPolarity;
|