@tenphi/glaze 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.cjs +507 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -7
- package/dist/index.d.mts +181 -7
- package/dist/index.mjs +507 -130
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +241 -11
- package/docs/methodology.md +49 -1
- package/docs/okhst.md +34 -1
- 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
|
|
@@ -1990,65 +2176,118 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
1990
2176
|
if (mirrored >= 0 && mirrored <= 100) return -delta;
|
|
1991
2177
|
return delta;
|
|
1992
2178
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
2179
|
+
/**
|
|
2180
|
+
* Dark position for an extreme tone (`'max'` / `'min'`) anchored to a base.
|
|
2181
|
+
*
|
|
2182
|
+
* A windowed dark remap compresses the base→extreme span, so the pair loses
|
|
2183
|
+
* contrast in dark. Instead, measure the tone shift the light scheme applied
|
|
2184
|
+
* between the base and the extreme, then replay it against the base's dark
|
|
2185
|
+
* tone: `auto` mirrors the sign (both ends invert), `fixed` keeps it. The
|
|
2186
|
+
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2187
|
+
* intended, since the author asked for the extreme.
|
|
2188
|
+
*/
|
|
2189
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, baseDarkTone, config) {
|
|
2190
|
+
const delta = mapToneForScheme(authorTone, mode, false, isHighContrast, config) - getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2191
|
+
return clamp(baseDarkTone + (mode === "auto" ? -delta : delta), 0, 100);
|
|
2192
|
+
}
|
|
2193
|
+
function resolveRootColor(def, isHighContrast, config) {
|
|
2194
|
+
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Effective hue and saturation for one color in one scheme.
|
|
2198
|
+
*
|
|
2199
|
+
* Dark schemes read the dark seed pair and the color's `darkHue` /
|
|
2200
|
+
* `darkSaturation`, each falling back to its light counterpart. An explicit
|
|
2201
|
+
* dark saturation — on the seed or the def — is taken literally; the global
|
|
2202
|
+
* `darkDesaturation` reduction applies only when neither is authored.
|
|
2203
|
+
* `mode: 'static'` pins one value across every scheme, so it skips all of it.
|
|
2204
|
+
*
|
|
2205
|
+
* Shared by `resolveColorForScheme` (the emitted variant) and
|
|
2206
|
+
* `resolveDependentColor` (the contrast solver's inputs) so the solver always
|
|
2207
|
+
* measures the color it is actually going to produce.
|
|
2208
|
+
*/
|
|
2209
|
+
function resolveChannels(def, ctx, isDark) {
|
|
2210
|
+
const mode = def.mode ?? "auto";
|
|
2211
|
+
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2212
|
+
if (!isDark || mode === "static") return {
|
|
2213
|
+
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2214
|
+
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2215
|
+
};
|
|
2216
|
+
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2217
|
+
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2218
|
+
const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2219
|
+
const raw = darkFactor * darkSeedSaturation / 100;
|
|
1995
2220
|
return {
|
|
1996
|
-
|
|
1997
|
-
|
|
2221
|
+
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2222
|
+
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
1998
2223
|
};
|
|
1999
2224
|
}
|
|
2000
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark,
|
|
2225
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
|
|
2001
2226
|
const baseName = def.base;
|
|
2002
2227
|
const baseResolved = ctx.resolved.get(baseName);
|
|
2003
2228
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
2004
2229
|
const mode = def.mode ?? "auto";
|
|
2005
|
-
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2006
2230
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2007
2231
|
const pastel = effectivePastel;
|
|
2008
2232
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2009
2233
|
const baseTone = baseVariant.t * 100;
|
|
2010
2234
|
let preferredTone;
|
|
2235
|
+
let isExtreme = false;
|
|
2011
2236
|
const rawTone = def.tone;
|
|
2012
2237
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2013
2238
|
else {
|
|
2014
|
-
const parsed =
|
|
2239
|
+
const parsed = passTone(rawTone, isHighContrast, ctx.config);
|
|
2015
2240
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2016
2241
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2017
2242
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2018
2243
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2019
|
-
else
|
|
2244
|
+
else if (parsed.kind === "extreme" && isDark && mode !== "static") {
|
|
2245
|
+
isExtreme = true;
|
|
2246
|
+
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2247
|
+
} else {
|
|
2248
|
+
isExtreme = parsed.kind === "extreme";
|
|
2249
|
+
preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
|
|
2250
|
+
}
|
|
2020
2251
|
}
|
|
2021
2252
|
const rawContrast = def.contrast;
|
|
2022
2253
|
if (rawContrast !== void 0) {
|
|
2023
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2024
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
2254
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, ctx.config, polarity);
|
|
2025
2255
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2026
2256
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2027
2257
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2028
2258
|
let initialDirection;
|
|
2029
2259
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2030
2260
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2261
|
+
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2262
|
+
const solve = {
|
|
2263
|
+
hue: channels.hue,
|
|
2264
|
+
saturation: channels.saturation,
|
|
2265
|
+
preferredTone: seedTone,
|
|
2035
2266
|
baseLinearRgb,
|
|
2036
|
-
contrast: resolvedContrast,
|
|
2037
2267
|
toneRange: [0, 1],
|
|
2038
|
-
initialDirection,
|
|
2039
2268
|
flip,
|
|
2040
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
|
|
2041
2286
|
});
|
|
2042
2287
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2043
|
-
return
|
|
2044
|
-
tone: result.tone * 100,
|
|
2045
|
-
satFactor
|
|
2046
|
-
};
|
|
2288
|
+
return result.tone * 100;
|
|
2047
2289
|
}
|
|
2048
|
-
return
|
|
2049
|
-
tone: clamp(preferredTone, 0, 100),
|
|
2050
|
-
satFactor
|
|
2051
|
-
};
|
|
2290
|
+
return clamp(preferredTone, 0, 100);
|
|
2052
2291
|
}
|
|
2053
2292
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2054
2293
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -2056,27 +2295,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2056
2295
|
const regDef = def;
|
|
2057
2296
|
const mode = regDef.mode ?? "auto";
|
|
2058
2297
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
2059
|
-
const
|
|
2298
|
+
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2060
2299
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2061
2300
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2062
|
-
|
|
2063
|
-
let satFactor;
|
|
2064
|
-
if (isRoot) {
|
|
2065
|
-
const root = resolveRootColor(regDef, isHighContrast);
|
|
2066
|
-
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
2067
|
-
satFactor = root.satFactor;
|
|
2068
|
-
} else {
|
|
2069
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
2070
|
-
finalTone = dep.tone;
|
|
2071
|
-
satFactor = dep.satFactor;
|
|
2072
|
-
}
|
|
2073
|
-
const baseSat = satFactor * ctx.saturation / 100;
|
|
2074
|
-
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
2075
|
-
const toneFraction = clamp(finalTone / 100, 0, 1);
|
|
2301
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, ctx.config), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2076
2302
|
return {
|
|
2077
|
-
h:
|
|
2078
|
-
s:
|
|
2079
|
-
t:
|
|
2303
|
+
h: channels.hue,
|
|
2304
|
+
s: channels.saturation,
|
|
2305
|
+
t: clamp(finalTone / 100, 0, 1),
|
|
2080
2306
|
alpha: regDef.opacity ?? 1,
|
|
2081
2307
|
pastel
|
|
2082
2308
|
};
|
|
@@ -2085,7 +2311,7 @@ function resolveShadowForScheme(def, ctx, isDark, isHighContrast) {
|
|
|
2085
2311
|
const bgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.bg), isDark, isHighContrast));
|
|
2086
2312
|
let fgVariant;
|
|
2087
2313
|
if (def.fg) fgVariant = toOkhslVariant(getSchemeVariant(ctx.resolved.get(def.fg), isDark, isHighContrast));
|
|
2088
|
-
const intensity =
|
|
2314
|
+
const intensity = passNumber(def.intensity, isHighContrast, ctx.config);
|
|
2089
2315
|
const tuning = resolveShadowTuning(def.tuning, ctx.config.shadowTuning);
|
|
2090
2316
|
return {
|
|
2091
2317
|
...toToneVariant(computeShadow(bgVariant, fgVariant, intensity, tuning)),
|
|
@@ -2134,7 +2360,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2134
2360
|
const targetResolved = ctx.resolved.get(def.target);
|
|
2135
2361
|
const baseVariant = toOkhslVariant(getSchemeVariant(baseResolved, isDark, isHighContrast));
|
|
2136
2362
|
const targetVariant = toOkhslVariant(getSchemeVariant(targetResolved, isDark, isHighContrast));
|
|
2137
|
-
let t = clamp(
|
|
2363
|
+
let t = clamp(passNumber(def.value, isHighContrast, ctx.config), 0, 100) / 100;
|
|
2138
2364
|
const blend = def.blend ?? "opaque";
|
|
2139
2365
|
const space = def.space ?? "okhsl";
|
|
2140
2366
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
@@ -2142,7 +2368,7 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2142
2368
|
const baseLinear = okhslVariantToLinearRgb(baseVariant, baseVariant.pastel ?? ctx.config.pastel);
|
|
2143
2369
|
const targetLinear = okhslVariantToLinearRgb(targetVariant, targetVariant.pastel ?? ctx.config.pastel);
|
|
2144
2370
|
if (def.contrast !== void 0) {
|
|
2145
|
-
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, polarity);
|
|
2371
|
+
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, ctx.config, polarity);
|
|
2146
2372
|
const metric = resolvedContrast.metric;
|
|
2147
2373
|
let luminanceAt;
|
|
2148
2374
|
if (blend === "transparent" || space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
@@ -2226,9 +2452,14 @@ function seedField(order, ctx, field, source) {
|
|
|
2226
2452
|
}
|
|
2227
2453
|
}
|
|
2228
2454
|
/**
|
|
2229
|
-
* After the
|
|
2455
|
+
* After the passes, surface chromatic contrast drift (§10): a color
|
|
2230
2456
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
2231
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.
|
|
2232
2463
|
*/
|
|
2233
2464
|
function verifyContrastDrift(order, defs, result, config) {
|
|
2234
2465
|
const roles = /* @__PURE__ */ new Map();
|
|
@@ -2241,7 +2472,7 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2241
2472
|
const base = result.get(regDef.base);
|
|
2242
2473
|
if (!color || !base) continue;
|
|
2243
2474
|
const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
|
|
2244
|
-
|
|
2475
|
+
const schemes = [
|
|
2245
2476
|
{
|
|
2246
2477
|
isDark: false,
|
|
2247
2478
|
isHighContrast: false,
|
|
@@ -2262,8 +2493,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2262
2493
|
isHighContrast: true,
|
|
2263
2494
|
field: "darkContrast"
|
|
2264
2495
|
}
|
|
2265
|
-
]
|
|
2266
|
-
|
|
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);
|
|
2267
2501
|
const cVariant = color[s.field];
|
|
2268
2502
|
const bVariant = base[s.field];
|
|
2269
2503
|
const cOkhsl = toOkhslVariant(cVariant);
|
|
@@ -2276,26 +2510,32 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2276
2510
|
}
|
|
2277
2511
|
}
|
|
2278
2512
|
}
|
|
2279
|
-
function resolveAllColors(
|
|
2513
|
+
function resolveAllColors(seed, defs, config, externalBases) {
|
|
2280
2514
|
validateColorDefs(defs, externalBases);
|
|
2281
2515
|
const order = topoSort(defs);
|
|
2282
2516
|
const ctx = {
|
|
2283
|
-
|
|
2284
|
-
saturation,
|
|
2517
|
+
...seed,
|
|
2285
2518
|
defs,
|
|
2286
2519
|
resolved: /* @__PURE__ */ new Map(),
|
|
2287
2520
|
config,
|
|
2288
2521
|
roles: /* @__PURE__ */ new Map()
|
|
2289
2522
|
};
|
|
2290
2523
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
2524
|
+
const manual = contrastFraction(config) !== void 0;
|
|
2291
2525
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
2292
|
-
|
|
2293
|
-
|
|
2526
|
+
let lightHCMap = lightMap;
|
|
2527
|
+
if (!manual) {
|
|
2528
|
+
seedField(order, ctx, "lightContrast", lightMap);
|
|
2529
|
+
lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
2530
|
+
}
|
|
2294
2531
|
seedField(order, ctx, "dark", lightMap);
|
|
2295
2532
|
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
2296
2533
|
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
2297
|
-
|
|
2298
|
-
|
|
2534
|
+
let darkHCMap = darkMap;
|
|
2535
|
+
if (!manual) {
|
|
2536
|
+
seedField(order, ctx, "darkContrast", darkMap);
|
|
2537
|
+
darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
2538
|
+
}
|
|
2299
2539
|
const result = /* @__PURE__ */ new Map();
|
|
2300
2540
|
for (const name of order) result.set(name, {
|
|
2301
2541
|
name,
|
|
@@ -2314,8 +2554,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
2314
2554
|
/**
|
|
2315
2555
|
* Hue channel planning for `splitHue` exports.
|
|
2316
2556
|
*
|
|
2317
|
-
* Builds per-color hue var references and
|
|
2318
|
-
*
|
|
2557
|
+
* Builds per-color hue var references and the `--*-hue` declarations backing
|
|
2558
|
+
* them for oklch CSS / Tasty output when every color is pastel.
|
|
2559
|
+
*
|
|
2560
|
+
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2561
|
+
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2562
|
+
* set is emitted only when it actually differs — see `darkHueDeclarations`.
|
|
2319
2563
|
*/
|
|
2320
2564
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2321
2565
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2324,7 +2568,11 @@ function cssProp(prefix, name, suffix) {
|
|
|
2324
2568
|
function isAchromatic(v) {
|
|
2325
2569
|
return v.s <= ACHROMATIC_EPSILON;
|
|
2326
2570
|
}
|
|
2327
|
-
|
|
2571
|
+
/** Seed hue backing `--{baseName}-hue` in a given scheme. */
|
|
2572
|
+
function schemeSeedHue(ctx, scheme) {
|
|
2573
|
+
return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
|
|
2574
|
+
}
|
|
2575
|
+
function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
2328
2576
|
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2329
2577
|
hueVar: "",
|
|
2330
2578
|
inline: true,
|
|
@@ -2332,13 +2580,25 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2332
2580
|
};
|
|
2333
2581
|
const regDef = def;
|
|
2334
2582
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2335
|
-
|
|
2583
|
+
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2584
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2585
|
+
const pinned = resolveEffectiveHue(ctx.seedHue, regDef.hue);
|
|
2586
|
+
return {
|
|
2587
|
+
hueVar: `var(${prop})`,
|
|
2588
|
+
inline: false,
|
|
2589
|
+
declarations: [{
|
|
2590
|
+
prop,
|
|
2591
|
+
value: String(pinned)
|
|
2592
|
+
}]
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2596
|
+
if (authored === void 0) return {
|
|
2336
2597
|
hueVar: baseHueVar,
|
|
2337
2598
|
inline: false,
|
|
2338
2599
|
declarations: []
|
|
2339
2600
|
};
|
|
2340
|
-
const parsed = parseRelativeOrAbsolute(
|
|
2341
|
-
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2601
|
+
const parsed = parseRelativeOrAbsolute(authored);
|
|
2342
2602
|
if (parsed.relative) {
|
|
2343
2603
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2344
2604
|
const magnitude = Math.abs(parsed.value);
|
|
@@ -2362,13 +2622,13 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2362
2622
|
}]
|
|
2363
2623
|
};
|
|
2364
2624
|
}
|
|
2365
|
-
function standaloneHuePlan(name, variant, ctx) {
|
|
2625
|
+
function standaloneHuePlan(name, variant, ctx, scheme) {
|
|
2366
2626
|
if (isAchromatic(variant)) return {
|
|
2367
2627
|
hueVar: "",
|
|
2368
2628
|
inline: true,
|
|
2369
2629
|
declarations: []
|
|
2370
2630
|
};
|
|
2371
|
-
const hue = ctx.resolvedHue ?? variant.h;
|
|
2631
|
+
const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
|
|
2372
2632
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2373
2633
|
return {
|
|
2374
2634
|
hueVar: `var(${prop})`,
|
|
@@ -2379,12 +2639,12 @@ function standaloneHuePlan(name, variant, ctx) {
|
|
|
2379
2639
|
}]
|
|
2380
2640
|
};
|
|
2381
2641
|
}
|
|
2382
|
-
function buildHuePlan(name, def, variant, ctx) {
|
|
2383
|
-
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2384
|
-
return themeHuePlan(name, def, variant, ctx);
|
|
2642
|
+
function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
2643
|
+
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx, scheme);
|
|
2644
|
+
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2385
2645
|
}
|
|
2386
2646
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2387
|
-
function collectHueDeclarations(resolved, ctx) {
|
|
2647
|
+
function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
2388
2648
|
if (ctx.emitDeclarations === false) return [];
|
|
2389
2649
|
const seen = /* @__PURE__ */ new Set();
|
|
2390
2650
|
const out = [];
|
|
@@ -2395,15 +2655,32 @@ function collectHueDeclarations(resolved, ctx) {
|
|
|
2395
2655
|
};
|
|
2396
2656
|
if (ctx.mode === "theme") push({
|
|
2397
2657
|
prop: `--${ctx.baseName}-hue`,
|
|
2398
|
-
value: String(ctx
|
|
2658
|
+
value: String(schemeSeedHue(ctx, scheme))
|
|
2399
2659
|
});
|
|
2400
2660
|
for (const [name, color] of resolved) {
|
|
2401
2661
|
const def = ctx.defs[name];
|
|
2402
|
-
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2662
|
+
const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
|
|
2403
2663
|
for (const decl of plan.declarations) push(decl);
|
|
2404
2664
|
}
|
|
2405
2665
|
return out;
|
|
2406
2666
|
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Dark-scheme hue declarations, or `[]` when dark resolves to the same hues as
|
|
2669
|
+
* light (the common case — no `darkHue` anywhere).
|
|
2670
|
+
*
|
|
2671
|
+
* When they differ, the **whole** set is returned rather than only the entries
|
|
2672
|
+
* whose text changed. A relative declaration reads
|
|
2673
|
+
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2674
|
+
* moves with the seed, and custom properties substitute `var()` at
|
|
2675
|
+
* computed-value time on the declaring element — so re-declaring
|
|
2676
|
+
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2677
|
+
* the light-substituted value.
|
|
2678
|
+
*/
|
|
2679
|
+
function darkHueDeclarations(resolved, ctx) {
|
|
2680
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2681
|
+
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2682
|
+
return dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark;
|
|
2683
|
+
}
|
|
2407
2684
|
function buildHuePlans(resolved, ctx) {
|
|
2408
2685
|
const plans = /* @__PURE__ */ new Map();
|
|
2409
2686
|
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
@@ -2466,28 +2743,41 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
2466
2743
|
if (format === "oklch" && huePlan !== void 0) return formatVariantHue(v, huePlan, pastel);
|
|
2467
2744
|
return formatVariant(v, format, pastel);
|
|
2468
2745
|
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Resolve the effective output modes for an export.
|
|
2748
|
+
*
|
|
2749
|
+
* A global manual `contrastLevel` turns off high-contrast output outright: the
|
|
2750
|
+
* level *is* the contrast preference, so a separate high-contrast tier has no
|
|
2751
|
+
* meaning alongside it. `modes.highContrast` therefore reads as "emit a separate
|
|
2752
|
+
* high-contrast set **when** contrast is automatic" — it goes inert under a
|
|
2753
|
+
* manual level rather than fighting it, and silently, since switching a
|
|
2754
|
+
* preference from auto to manual is normal use and not a mistake to report.
|
|
2755
|
+
*
|
|
2756
|
+
* A level set on a single theme or token does not change which modes are
|
|
2757
|
+
* emitted: sibling themes in a palette may still have a real high-contrast
|
|
2758
|
+
* tier, and the manual one correctly reports its own resolved values there —
|
|
2759
|
+
* the alternative would drop its colors from that tier entirely.
|
|
2760
|
+
*/
|
|
2469
2761
|
function resolveModes(override) {
|
|
2470
2762
|
const cfg = getConfig();
|
|
2471
2763
|
return {
|
|
2472
2764
|
dark: override?.dark ?? cfg.modes.dark,
|
|
2473
|
-
highContrast: override?.highContrast ?? cfg.modes.highContrast
|
|
2765
|
+
highContrast: contrastFraction(cfg) !== void 0 ? false : override?.highContrast ?? cfg.modes.highContrast
|
|
2474
2766
|
};
|
|
2475
2767
|
}
|
|
2476
2768
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|
|
2477
2769
|
const tokens = {};
|
|
2478
2770
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2479
2771
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2480
|
-
const
|
|
2481
|
-
if (
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
}
|
|
2488
|
-
const colorKey = `#${prefix}${name}`;
|
|
2489
|
-
tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2772
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
2773
|
+
if (modes.dark) for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2774
|
+
const key = `$${decl.prop.slice(2)}`;
|
|
2775
|
+
tokens[key] = {
|
|
2776
|
+
...tokens[key],
|
|
2777
|
+
[states.dark]: decl.value
|
|
2778
|
+
};
|
|
2490
2779
|
}
|
|
2780
|
+
for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2491
2781
|
return tokens;
|
|
2492
2782
|
}
|
|
2493
2783
|
for (const [name, color] of resolved) {
|
|
@@ -2536,7 +2826,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2536
2826
|
darkContrast: []
|
|
2537
2827
|
};
|
|
2538
2828
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2539
|
-
if (huePlans !== void 0 && channelCtx !== void 0)
|
|
2829
|
+
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2830
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
2831
|
+
for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2832
|
+
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2833
|
+
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2540
2836
|
for (const [name, color] of resolved) {
|
|
2541
2837
|
const prop = `--${prefix}${name}${suffix}`;
|
|
2542
2838
|
const plan = huePlans?.get(name);
|
|
@@ -2973,6 +3269,13 @@ function validateStructuredInput(input) {
|
|
|
2973
3269
|
if (input.saturationFactor !== void 0) {
|
|
2974
3270
|
if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
|
|
2975
3271
|
}
|
|
3272
|
+
if (typeof input.darkHue === "number" && !Number.isFinite(input.darkHue)) throw new Error(`glaze.color: structured darkHue must be a finite number (got ${input.darkHue}).`);
|
|
3273
|
+
if (input.darkSaturation !== void 0) {
|
|
3274
|
+
if (!Number.isFinite(input.darkSaturation) || input.darkSaturation < 0 || input.darkSaturation > 100) throw new Error(`glaze.color: structured darkSaturation must be a finite number in 0–100 (got ${input.darkSaturation}).`);
|
|
3275
|
+
}
|
|
3276
|
+
if (input.darkSaturationFactor !== void 0) {
|
|
3277
|
+
if (!Number.isFinite(input.darkSaturationFactor) || input.darkSaturationFactor < 0 || input.darkSaturationFactor > 1) throw new Error(`glaze.color: structured darkSaturationFactor must be a finite number in 0–1 (got ${input.darkSaturationFactor}).`);
|
|
3278
|
+
}
|
|
2976
3279
|
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
2977
3280
|
}
|
|
2978
3281
|
/**
|
|
@@ -3019,11 +3322,27 @@ function extractOkhslFromValue(value) {
|
|
|
3019
3322
|
return value;
|
|
3020
3323
|
}
|
|
3021
3324
|
/**
|
|
3325
|
+
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3326
|
+
*
|
|
3327
|
+
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|
|
3328
|
+
* to measure against — it is not an authored base. Pinning the extreme keeps
|
|
3329
|
+
* the token on the plain scheme mapping instead of the base-anchored dark
|
|
3330
|
+
* replay the resolver applies to real `base` references.
|
|
3331
|
+
*/
|
|
3332
|
+
function pinExtremeTone(tone) {
|
|
3333
|
+
const pin = (value) => value === "max" ? 100 : value === "min" ? 0 : value;
|
|
3334
|
+
return Array.isArray(tone) ? [pin(tone[0]), pin(tone[1])] : pin(tone);
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3022
3337
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
3023
3338
|
*
|
|
3024
3339
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
3025
3340
|
* across every value-shorthand form.
|
|
3026
3341
|
*
|
|
3342
|
+
* An absolute `hue` override becomes the seed while a relative one becomes a
|
|
3343
|
+
* def field; `darkHue` always lands on the def in either form, so an absolute
|
|
3344
|
+
* `darkHue` wins outright instead of being re-offset by a relative `hue`.
|
|
3345
|
+
*
|
|
3027
3346
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
3028
3347
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
3029
3348
|
* the seed pinned to the literal user-provided color across all four
|
|
@@ -3041,10 +3360,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3041
3360
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
3042
3361
|
const primary = userName ?? STANDALONE_VALUE;
|
|
3043
3362
|
const seedTone = toTone(main.l);
|
|
3363
|
+
const primaryTone = toneOption ?? seedTone;
|
|
3044
3364
|
const valueDef = {
|
|
3045
3365
|
hue: relativeHue,
|
|
3046
3366
|
saturation: options?.saturationFactor,
|
|
3047
|
-
|
|
3367
|
+
darkHue: options?.darkHue,
|
|
3368
|
+
darkSaturation: options?.darkSaturationFactor,
|
|
3369
|
+
tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
|
|
3048
3370
|
contrast: options?.contrast,
|
|
3049
3371
|
mode: options?.mode ?? "auto",
|
|
3050
3372
|
autoFlip: options?.autoFlip,
|
|
@@ -3061,13 +3383,16 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3061
3383
|
mode: "static"
|
|
3062
3384
|
};
|
|
3063
3385
|
return {
|
|
3064
|
-
|
|
3065
|
-
|
|
3386
|
+
seed: {
|
|
3387
|
+
hue: seedHue,
|
|
3388
|
+
saturation: seedSaturation,
|
|
3389
|
+
darkSaturation: options?.darkSaturation
|
|
3390
|
+
},
|
|
3066
3391
|
defs,
|
|
3067
3392
|
primary
|
|
3068
3393
|
};
|
|
3069
3394
|
}
|
|
3070
|
-
function createColorTokenFromDefs(
|
|
3395
|
+
function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
|
|
3071
3396
|
let cache = null;
|
|
3072
3397
|
function getEffectiveConfig() {
|
|
3073
3398
|
const version = getConfigVersion();
|
|
@@ -3084,7 +3409,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3084
3409
|
const version = getConfigVersion();
|
|
3085
3410
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3086
3411
|
const effectiveConfig = getEffectiveConfig();
|
|
3087
|
-
const map = resolveAllColors(
|
|
3412
|
+
const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
3088
3413
|
cache = {
|
|
3089
3414
|
map,
|
|
3090
3415
|
version,
|
|
@@ -3122,7 +3447,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3122
3447
|
if (options.splitHue && format === "oklch") {
|
|
3123
3448
|
assertAllPastel(renamed, resolveModes());
|
|
3124
3449
|
channelCtx = {
|
|
3125
|
-
seedHue,
|
|
3450
|
+
seedHue: seed.hue,
|
|
3126
3451
|
baseName: options.name,
|
|
3127
3452
|
prefix: "",
|
|
3128
3453
|
defs: { [options.name]: defs[primary] },
|
|
@@ -3206,8 +3531,10 @@ function createColorToken(input, configOverride) {
|
|
|
3206
3531
|
const hasExternalBase = baseToken !== void 0;
|
|
3207
3532
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
3208
3533
|
const defs = { [primary]: {
|
|
3209
|
-
tone: input.tone,
|
|
3534
|
+
tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
|
|
3210
3535
|
saturation: input.saturationFactor,
|
|
3536
|
+
darkHue: input.darkHue,
|
|
3537
|
+
darkSaturation: input.darkSaturationFactor,
|
|
3211
3538
|
mode: input.mode ?? "auto",
|
|
3212
3539
|
autoFlip: input.autoFlip,
|
|
3213
3540
|
contrast: input.contrast,
|
|
@@ -3216,16 +3543,17 @@ function createColorToken(input, configOverride) {
|
|
|
3216
3543
|
role: input.role,
|
|
3217
3544
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
3218
3545
|
} };
|
|
3219
|
-
if (needsSeedAnchor) {
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
mode: "static"
|
|
3225
|
-
};
|
|
3226
|
-
}
|
|
3546
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
3547
|
+
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
3548
|
+
saturation: 1,
|
|
3549
|
+
mode: "static"
|
|
3550
|
+
};
|
|
3227
3551
|
const localOverride = configOverride;
|
|
3228
|
-
return createColorTokenFromDefs(
|
|
3552
|
+
return createColorTokenFromDefs({
|
|
3553
|
+
hue: input.hue,
|
|
3554
|
+
saturation: input.saturation,
|
|
3555
|
+
darkSaturation: input.darkSaturation
|
|
3556
|
+
}, defs, primary, localOverride, baseToken, (exportArg) => ({
|
|
3229
3557
|
kind: "color",
|
|
3230
3558
|
version: GLAZE_EXPORT_VERSION,
|
|
3231
3559
|
form: "structured",
|
|
@@ -3236,9 +3564,9 @@ function createColorToken(input, configOverride) {
|
|
|
3236
3564
|
function createColorTokenFromValue(value, options, configOverride) {
|
|
3237
3565
|
const main = extractOkhslFromValue(value);
|
|
3238
3566
|
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
3239
|
-
const {
|
|
3567
|
+
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
3240
3568
|
const localOverride = sparseValueFormLocal(configOverride);
|
|
3241
|
-
return createColorTokenFromDefs(
|
|
3569
|
+
return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
|
|
3242
3570
|
kind: "color",
|
|
3243
3571
|
version: GLAZE_EXPORT_VERSION,
|
|
3244
3572
|
form: "value",
|
|
@@ -3258,6 +3586,9 @@ function buildOverridesExport(options, exportArg) {
|
|
|
3258
3586
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
3259
3587
|
if (options.tone !== void 0) out.tone = options.tone;
|
|
3260
3588
|
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
3589
|
+
if (options.darkHue !== void 0) out.darkHue = options.darkHue;
|
|
3590
|
+
if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
|
|
3591
|
+
if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
|
|
3261
3592
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
3262
3593
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
3263
3594
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
@@ -3275,6 +3606,9 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
3275
3606
|
tone: input.tone
|
|
3276
3607
|
};
|
|
3277
3608
|
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
3609
|
+
if (input.darkHue !== void 0) out.darkHue = input.darkHue;
|
|
3610
|
+
if (input.darkSaturation !== void 0) out.darkSaturation = input.darkSaturation;
|
|
3611
|
+
if (input.darkSaturationFactor !== void 0) out.darkSaturationFactor = input.darkSaturationFactor;
|
|
3278
3612
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
3279
3613
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
3280
3614
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
@@ -3297,6 +3631,9 @@ function rehydrateOverrides(data) {
|
|
|
3297
3631
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
3298
3632
|
if (data.tone !== void 0) out.tone = data.tone;
|
|
3299
3633
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3634
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3635
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3636
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3300
3637
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3301
3638
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3302
3639
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
@@ -3314,6 +3651,9 @@ function rehydrateStructuredInput(data) {
|
|
|
3314
3651
|
tone: data.tone
|
|
3315
3652
|
};
|
|
3316
3653
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3654
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3655
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3656
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3317
3657
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3318
3658
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3319
3659
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
@@ -3347,7 +3687,8 @@ function colorFromExport(data) {
|
|
|
3347
3687
|
|
|
3348
3688
|
//#endregion
|
|
3349
3689
|
//#region src/theme.ts
|
|
3350
|
-
function createTheme(
|
|
3690
|
+
function createTheme(seed, initialColors, configOverride) {
|
|
3691
|
+
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
3351
3692
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
3352
3693
|
let cache = null;
|
|
3353
3694
|
function getEffectiveConfig() {
|
|
@@ -3365,7 +3706,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3365
3706
|
const version = getConfigVersion();
|
|
3366
3707
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3367
3708
|
const effectiveConfig = getEffectiveConfig();
|
|
3368
|
-
const map = resolveAllColors(
|
|
3709
|
+
const map = resolveAllColors(seed, colorDefs, effectiveConfig);
|
|
3369
3710
|
cache = {
|
|
3370
3711
|
map,
|
|
3371
3712
|
version,
|
|
@@ -3382,6 +3723,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3382
3723
|
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3383
3724
|
return {
|
|
3384
3725
|
seedHue: hue,
|
|
3726
|
+
darkSeedHue: darkHue,
|
|
3385
3727
|
baseName: options.name ?? "theme",
|
|
3386
3728
|
prefix,
|
|
3387
3729
|
defs: colorDefs,
|
|
@@ -3395,6 +3737,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3395
3737
|
get saturation() {
|
|
3396
3738
|
return saturation;
|
|
3397
3739
|
},
|
|
3740
|
+
get darkHue() {
|
|
3741
|
+
return darkHue;
|
|
3742
|
+
},
|
|
3743
|
+
get darkSaturation() {
|
|
3744
|
+
return darkSaturation;
|
|
3745
|
+
},
|
|
3398
3746
|
getConfig() {
|
|
3399
3747
|
return getEffectiveConfig();
|
|
3400
3748
|
},
|
|
@@ -3431,16 +3779,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3431
3779
|
version: GLAZE_EXPORT_VERSION,
|
|
3432
3780
|
hue,
|
|
3433
3781
|
saturation,
|
|
3782
|
+
...darkHue !== void 0 ? { darkHue } : {},
|
|
3783
|
+
...darkSaturation !== void 0 ? { darkSaturation } : {},
|
|
3434
3784
|
colors: structuredClone(colorDefs),
|
|
3435
3785
|
config: freezeConfigForExport(configOverride, override)
|
|
3436
3786
|
};
|
|
3437
3787
|
},
|
|
3438
3788
|
extend(options) {
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3789
|
+
const childSeed = {
|
|
3790
|
+
hue: options.hue ?? hue,
|
|
3791
|
+
saturation: options.saturation ?? saturation,
|
|
3792
|
+
darkHue: options.darkHue ?? darkHue,
|
|
3793
|
+
darkSaturation: options.darkSaturation ?? darkSaturation
|
|
3794
|
+
};
|
|
3441
3795
|
const inheritedColors = {};
|
|
3442
3796
|
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
3443
|
-
return createTheme(
|
|
3797
|
+
return createTheme(childSeed, options.colors ? {
|
|
3444
3798
|
...inheritedColors,
|
|
3445
3799
|
...options.colors
|
|
3446
3800
|
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
@@ -3550,6 +3904,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
|
|
|
3550
3904
|
assertAllPastel(filtered, modes);
|
|
3551
3905
|
return {
|
|
3552
3906
|
seedHue: theme.hue,
|
|
3907
|
+
darkSeedHue: theme.darkHue,
|
|
3553
3908
|
baseName: themeName,
|
|
3554
3909
|
prefix: themedPrefix,
|
|
3555
3910
|
defs: colorMapFromTheme(theme),
|
|
@@ -3581,7 +3936,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
|
3581
3936
|
assertExportKind(data, "theme", factory);
|
|
3582
3937
|
assertExportVersion(data, factory);
|
|
3583
3938
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
3584
|
-
|
|
3939
|
+
for (const field of ["darkHue", "darkSaturation"]) if (data[field] !== void 0 && typeof data[field] !== "number") throw new Error(`${factory}: expected "${field}" to be a number when present (got ${JSON.stringify(data[field])}).`);
|
|
3940
|
+
return createTheme({
|
|
3941
|
+
hue: data.hue,
|
|
3942
|
+
saturation: data.saturation,
|
|
3943
|
+
darkHue: data.darkHue,
|
|
3944
|
+
darkSaturation: data.darkSaturation
|
|
3945
|
+
}, data.colors, data.config);
|
|
3585
3946
|
}
|
|
3586
3947
|
/**
|
|
3587
3948
|
* Rebuild a palette from a `palette.export()` snapshot.
|
|
@@ -3743,18 +4104,28 @@ function createPalette(themes, paletteOptions) {
|
|
|
3743
4104
|
* the theme still reacts to later `configure()` calls for fields it
|
|
3744
4105
|
* didn't override.
|
|
3745
4106
|
*
|
|
4107
|
+
* The object form also accepts `darkHue` / `darkSaturation` — a separate
|
|
4108
|
+
* seed pair for the dark and dark-high-contrast schemes. Both default to
|
|
4109
|
+
* their light counterparts, and an explicit `darkSaturation` opts the theme
|
|
4110
|
+
* out of the global `darkDesaturation` reduction.
|
|
4111
|
+
*
|
|
3746
4112
|
* @example
|
|
3747
4113
|
* ```ts
|
|
3748
4114
|
* const primary = glaze(280, 80);
|
|
3749
4115
|
* // or shorthand:
|
|
3750
4116
|
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
4117
|
+
* // with a retuned dark seed:
|
|
4118
|
+
* const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
|
|
3751
4119
|
* // with config override:
|
|
3752
4120
|
* const raw = glaze(280, 80, { lightTone: false });
|
|
3753
4121
|
* ```
|
|
3754
4122
|
*/
|
|
3755
4123
|
function glaze(hueOrOptions, saturation, config) {
|
|
3756
|
-
if (typeof hueOrOptions === "number") return createTheme(
|
|
3757
|
-
|
|
4124
|
+
if (typeof hueOrOptions === "number") return createTheme({
|
|
4125
|
+
hue: hueOrOptions,
|
|
4126
|
+
saturation: saturation ?? 100
|
|
4127
|
+
}, void 0, config);
|
|
4128
|
+
return createTheme(hueOrOptions, void 0, config);
|
|
3758
4129
|
}
|
|
3759
4130
|
/** Configure global glaze settings. */
|
|
3760
4131
|
glaze.configure = function configure$1(config) {
|
|
@@ -3868,7 +4239,10 @@ glaze.fromHex = function fromHex(hex) {
|
|
|
3868
4239
|
const rgb = parseHex(hex);
|
|
3869
4240
|
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
3870
4241
|
const [h, s] = srgbToOkhsl(rgb);
|
|
3871
|
-
return createTheme(
|
|
4242
|
+
return createTheme({
|
|
4243
|
+
hue: h,
|
|
4244
|
+
saturation: s * 100
|
|
4245
|
+
});
|
|
3872
4246
|
};
|
|
3873
4247
|
/**
|
|
3874
4248
|
* Create a theme from RGB values (0–255).
|
|
@@ -3880,7 +4254,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
3880
4254
|
g / 255,
|
|
3881
4255
|
b / 255
|
|
3882
4256
|
]);
|
|
3883
|
-
return createTheme(
|
|
4257
|
+
return createTheme({
|
|
4258
|
+
hue: h,
|
|
4259
|
+
saturation: s * 100
|
|
4260
|
+
});
|
|
3884
4261
|
};
|
|
3885
4262
|
/**
|
|
3886
4263
|
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|
|
@@ -3924,5 +4301,5 @@ glaze.resetConfig = function resetConfig$1() {
|
|
|
3924
4301
|
};
|
|
3925
4302
|
|
|
3926
4303
|
//#endregion
|
|
3927
|
-
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 };
|
|
4304
|
+
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 };
|
|
3928
4305
|
//# sourceMappingURL=index.mjs.map
|