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