@tenphi/glaze 1.3.1 → 1.4.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/dist/index.cjs +353 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.mts +31 -1
- package/dist/index.mjs +353 -273
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +58 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -881,52 +881,6 @@ function isPaletteExport(data) {
|
|
|
881
881
|
return hasPaletteShape(data);
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
-
//#endregion
|
|
885
|
-
//#region src/format-guard.ts
|
|
886
|
-
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
887
|
-
/**
|
|
888
|
-
* Throw when a non-native Glaze color space is requested for an export that
|
|
889
|
-
* emits raw CSS or non-Tasty token maps.
|
|
890
|
-
*/
|
|
891
|
-
function assertNativeFormat(format, method) {
|
|
892
|
-
if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
|
|
893
|
-
}
|
|
894
|
-
const SCHEME_FIELDS = [
|
|
895
|
-
{
|
|
896
|
-
field: "light",
|
|
897
|
-
modes: () => true
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
field: "dark",
|
|
901
|
-
modes: (m) => m.dark
|
|
902
|
-
},
|
|
903
|
-
{
|
|
904
|
-
field: "lightContrast",
|
|
905
|
-
modes: (m) => m.highContrast
|
|
906
|
-
},
|
|
907
|
-
{
|
|
908
|
-
field: "darkContrast",
|
|
909
|
-
modes: (m) => m.dark && m.highContrast
|
|
910
|
-
}
|
|
911
|
-
];
|
|
912
|
-
/**
|
|
913
|
-
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
914
|
-
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
915
|
-
* safe chroma (`computeSafeChromaOKLCH`).
|
|
916
|
-
*/
|
|
917
|
-
function assertAllPastel(resolved, modes) {
|
|
918
|
-
const nonPastel = [];
|
|
919
|
-
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
920
|
-
if (!active(modes)) continue;
|
|
921
|
-
if (color[field].pastel !== true) {
|
|
922
|
-
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
if (nonPastel.length === 0) return;
|
|
927
|
-
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
|
|
928
|
-
}
|
|
929
|
-
|
|
930
884
|
//#endregion
|
|
931
885
|
//#region src/hc-pair.ts
|
|
932
886
|
function pairNormal(p) {
|
|
@@ -1252,10 +1206,267 @@ function mapSaturationDark(s, mode, config) {
|
|
|
1252
1206
|
* endpoints (HC bypasses to full range, a manual `contrastLevel` widens
|
|
1253
1207
|
* toward it — see `activeWindow`).
|
|
1254
1208
|
*/
|
|
1255
|
-
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1256
|
-
if (mode === "static") return [0, 1];
|
|
1257
|
-
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1258
|
-
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1209
|
+
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1210
|
+
if (mode === "static") return [0, 1];
|
|
1211
|
+
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1212
|
+
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region src/color-value.ts
|
|
1217
|
+
/**
|
|
1218
|
+
* Parsing and validation for `GlazeColorValue` — the literal color forms.
|
|
1219
|
+
*
|
|
1220
|
+
* Hex, the CSS color functions Glaze emits, and the four value-object shapes all
|
|
1221
|
+
* land here and come out as OKHSL. Split out of `color-token.ts` so both
|
|
1222
|
+
* consumers can reach it without an import cycle: standalone `glaze.color()`
|
|
1223
|
+
* tokens build on it, and the resolver needs it for a theme color's `from`. It
|
|
1224
|
+
* depends on nothing but the color math, which is what keeps that true.
|
|
1225
|
+
*/
|
|
1226
|
+
/**
|
|
1227
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1228
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1229
|
+
*
|
|
1230
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1231
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1232
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1233
|
+
* are out of scope.
|
|
1234
|
+
*/
|
|
1235
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1236
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1237
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1238
|
+
return parseFloat(raw);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Split the body of a CSS color function into its components and detect
|
|
1242
|
+
* whether an alpha channel was present.
|
|
1243
|
+
*
|
|
1244
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1245
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1246
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1247
|
+
*/
|
|
1248
|
+
function splitColorBody(body) {
|
|
1249
|
+
const slashIdx = body.indexOf("/");
|
|
1250
|
+
if (slashIdx !== -1) return {
|
|
1251
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1252
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1253
|
+
};
|
|
1254
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1255
|
+
if (components.length === 4) {
|
|
1256
|
+
components.pop();
|
|
1257
|
+
return {
|
|
1258
|
+
components,
|
|
1259
|
+
hadAlpha: true
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
components,
|
|
1264
|
+
hadAlpha: false
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function warnDroppedAlpha(input) {
|
|
1268
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1269
|
+
}
|
|
1270
|
+
function parseColorString(input) {
|
|
1271
|
+
if (input.startsWith("#")) {
|
|
1272
|
+
const parsed = parseHexAlpha(input);
|
|
1273
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1274
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1275
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1276
|
+
return {
|
|
1277
|
+
h,
|
|
1278
|
+
s,
|
|
1279
|
+
l
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
const m = input.match(COLOR_FN_RE);
|
|
1283
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1284
|
+
const fn = m[1].toLowerCase();
|
|
1285
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1286
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1287
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1288
|
+
switch (fn) {
|
|
1289
|
+
case "rgb":
|
|
1290
|
+
case "rgba": {
|
|
1291
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1292
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1293
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1294
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1295
|
+
]);
|
|
1296
|
+
return {
|
|
1297
|
+
h,
|
|
1298
|
+
s,
|
|
1299
|
+
l
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
case "hsl":
|
|
1303
|
+
case "hsla": {
|
|
1304
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1305
|
+
return {
|
|
1306
|
+
h: oh,
|
|
1307
|
+
s: os,
|
|
1308
|
+
l: ol
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
case "okhsl": return {
|
|
1312
|
+
h: parseFloat(components[0]),
|
|
1313
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1314
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1315
|
+
};
|
|
1316
|
+
case "okhst": return okhstToOkhsl({
|
|
1317
|
+
h: parseFloat(components[0]),
|
|
1318
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1319
|
+
t: parseNumberOrPercent(components[2], 1)
|
|
1320
|
+
});
|
|
1321
|
+
case "oklch": {
|
|
1322
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1323
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1324
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1325
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1326
|
+
L,
|
|
1327
|
+
C * Math.cos(hRad),
|
|
1328
|
+
C * Math.sin(hRad)
|
|
1329
|
+
]);
|
|
1330
|
+
return {
|
|
1331
|
+
h,
|
|
1332
|
+
s,
|
|
1333
|
+
l
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1341
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1342
|
+
*/
|
|
1343
|
+
function validateOkhslColor(value) {
|
|
1344
|
+
const { h, s, l } = value;
|
|
1345
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1346
|
+
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
1347
|
+
}
|
|
1348
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1349
|
+
function validateRgbColor(value) {
|
|
1350
|
+
for (const key of [
|
|
1351
|
+
"r",
|
|
1352
|
+
"g",
|
|
1353
|
+
"b"
|
|
1354
|
+
]) {
|
|
1355
|
+
const n = value[key];
|
|
1356
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1360
|
+
function validateOklchColor(value) {
|
|
1361
|
+
const { l, c, h } = value;
|
|
1362
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1363
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1364
|
+
}
|
|
1365
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1366
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1367
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1368
|
+
l,
|
|
1369
|
+
c * Math.cos(hRad),
|
|
1370
|
+
c * Math.sin(hRad)
|
|
1371
|
+
]);
|
|
1372
|
+
return {
|
|
1373
|
+
h,
|
|
1374
|
+
s,
|
|
1375
|
+
l: outL
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function isRgbColorObject(value) {
|
|
1379
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1380
|
+
}
|
|
1381
|
+
function isOklchColorObject(value) {
|
|
1382
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1383
|
+
}
|
|
1384
|
+
function isOkhstColorObject(value) {
|
|
1385
|
+
return "t" in value && "h" in value && "s" in value;
|
|
1386
|
+
}
|
|
1387
|
+
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
1388
|
+
function validateOkhstColor(value) {
|
|
1389
|
+
const { h, s, t } = value;
|
|
1390
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
1391
|
+
if (s > 1.5 || t > 1.5) throw new Error("glaze.color: OkhstColor s/t must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1395
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1396
|
+
* literal objects) go through one parser.
|
|
1397
|
+
*/
|
|
1398
|
+
function extractOkhslFromValue(value) {
|
|
1399
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1400
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1401
|
+
if (isRgbColorObject(value)) {
|
|
1402
|
+
validateRgbColor(value);
|
|
1403
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1404
|
+
value.r / 255,
|
|
1405
|
+
value.g / 255,
|
|
1406
|
+
value.b / 255
|
|
1407
|
+
]);
|
|
1408
|
+
return {
|
|
1409
|
+
h,
|
|
1410
|
+
s,
|
|
1411
|
+
l
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
if (isOklchColorObject(value)) {
|
|
1415
|
+
validateOklchColor(value);
|
|
1416
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1417
|
+
}
|
|
1418
|
+
if (isOkhstColorObject(value)) {
|
|
1419
|
+
validateOkhstColor(value);
|
|
1420
|
+
return okhstToOkhsl(value);
|
|
1421
|
+
}
|
|
1422
|
+
validateOkhslColor(value);
|
|
1423
|
+
return value;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/format-guard.ts
|
|
1428
|
+
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
1429
|
+
/**
|
|
1430
|
+
* Throw when a non-native Glaze color space is requested for an export that
|
|
1431
|
+
* emits raw CSS or non-Tasty token maps.
|
|
1432
|
+
*/
|
|
1433
|
+
function assertNativeFormat(format, method) {
|
|
1434
|
+
if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
|
|
1435
|
+
}
|
|
1436
|
+
const SCHEME_FIELDS = [
|
|
1437
|
+
{
|
|
1438
|
+
field: "light",
|
|
1439
|
+
modes: () => true
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
field: "dark",
|
|
1443
|
+
modes: (m) => m.dark
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
field: "lightContrast",
|
|
1447
|
+
modes: (m) => m.highContrast
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
field: "darkContrast",
|
|
1451
|
+
modes: (m) => m.dark && m.highContrast
|
|
1452
|
+
}
|
|
1453
|
+
];
|
|
1454
|
+
/**
|
|
1455
|
+
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
1456
|
+
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
1457
|
+
* safe chroma (`computeSafeChromaOKLCH`).
|
|
1458
|
+
*/
|
|
1459
|
+
function assertAllPastel(resolved, modes) {
|
|
1460
|
+
const nonPastel = [];
|
|
1461
|
+
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
1462
|
+
if (!active(modes)) continue;
|
|
1463
|
+
if (color[field].pastel !== true) {
|
|
1464
|
+
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (nonPastel.length === 0) return;
|
|
1469
|
+
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
|
|
1259
1470
|
}
|
|
1260
1471
|
|
|
1261
1472
|
//#endregion
|
|
@@ -1896,6 +2107,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1896
2107
|
* its base / bg / fg / target dependencies.
|
|
1897
2108
|
*/
|
|
1898
2109
|
/**
|
|
2110
|
+
* Does this color sit at an absolute tone — either authored, or carried by a
|
|
2111
|
+
* `from` value? That is the test for "root" (placed on its own) as opposed to
|
|
2112
|
+
* "dependent" (placed relative to a base), and it is what lets a bare
|
|
2113
|
+
* `{ from: '#2F5BFF' }` stand as a complete color definition.
|
|
2114
|
+
*/
|
|
2115
|
+
function hasAbsoluteTone(def) {
|
|
2116
|
+
if (def.tone === void 0) return def.from !== void 0;
|
|
2117
|
+
return isAbsoluteTone(def.tone);
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
1899
2120
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1900
2121
|
*
|
|
1901
2122
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1934,11 +2155,16 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1934
2155
|
}
|
|
1935
2156
|
const regDef = def;
|
|
1936
2157
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
2158
|
+
if (regDef.from !== void 0) try {
|
|
2159
|
+
extractOkhslFromValue(regDef.from);
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
throw new Error(`glaze: color "${name}" has an invalid "from" value. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2162
|
+
}
|
|
1937
2163
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1938
2164
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1939
2165
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1940
2166
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1941
|
-
if (!
|
|
2167
|
+
if (!hasAbsoluteTone(regDef) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`);
|
|
1942
2168
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1943
2169
|
}
|
|
1944
2170
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2195,7 +2421,43 @@ function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config)
|
|
|
2195
2421
|
const shift = extremeLightTone - lightBase.t * 100;
|
|
2196
2422
|
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2197
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2426
|
+
*
|
|
2427
|
+
* Deliberately not memoized. Keying a cache on the def object looks free — a
|
|
2428
|
+
* resolve reads this a handful of times per color — but defs are stored by
|
|
2429
|
+
* reference, so mutating one in place and re-setting it would invalidate the
|
|
2430
|
+
* theme's own cache while a def-keyed cache quietly served the old color. The
|
|
2431
|
+
* parse is a few microseconds against a resolve that is already cached; that is
|
|
2432
|
+
* not a trade worth a staleness bug.
|
|
2433
|
+
*/
|
|
2434
|
+
function fromSeed(def) {
|
|
2435
|
+
if (def.from === void 0) return void 0;
|
|
2436
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2437
|
+
return {
|
|
2438
|
+
hue: h,
|
|
2439
|
+
saturation: s,
|
|
2440
|
+
tone: toTone(l)
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* The config this color resolves against.
|
|
2445
|
+
*
|
|
2446
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2447
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2448
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2449
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2450
|
+
* the color is expected to adapt.
|
|
2451
|
+
*/
|
|
2452
|
+
function configForColor(def, config) {
|
|
2453
|
+
if (def.from === void 0) return config;
|
|
2454
|
+
return {
|
|
2455
|
+
...config,
|
|
2456
|
+
lightTone: false
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2198
2459
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2460
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2199
2461
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2200
2462
|
}
|
|
2201
2463
|
/**
|
|
@@ -2214,16 +2476,20 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2214
2476
|
function resolveChannels(def, ctx, isDark) {
|
|
2215
2477
|
const mode = def.mode ?? "auto";
|
|
2216
2478
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2479
|
+
const seed = fromSeed(def);
|
|
2480
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2481
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2217
2482
|
if (!isDark || mode === "static") return {
|
|
2218
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2219
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2483
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2484
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2220
2485
|
};
|
|
2221
2486
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2222
2487
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2223
|
-
const
|
|
2224
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2488
|
+
const fromDark = fromSaturation !== void 0 && def.darkSaturation === void 0;
|
|
2489
|
+
const raw = fromDark ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2490
|
+
const explicitDark = fromDark ? false : def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2225
2491
|
return {
|
|
2226
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2492
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2227
2493
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2228
2494
|
};
|
|
2229
2495
|
}
|
|
@@ -2234,30 +2500,32 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2234
2500
|
const mode = def.mode ?? "auto";
|
|
2235
2501
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2236
2502
|
const pastel = effectivePastel;
|
|
2503
|
+
const config = configForColor(def, ctx.config);
|
|
2237
2504
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2238
2505
|
const baseTone = baseVariant.t * 100;
|
|
2239
2506
|
let preferredTone;
|
|
2240
2507
|
let isExtreme = false;
|
|
2241
|
-
const
|
|
2508
|
+
const seed = fromSeed(def);
|
|
2509
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2242
2510
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2243
2511
|
else {
|
|
2244
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2512
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2245
2513
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2246
2514
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2247
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2515
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2248
2516
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2249
2517
|
else {
|
|
2250
2518
|
isExtreme = parsed.kind === "extreme";
|
|
2251
|
-
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved,
|
|
2252
|
-
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast,
|
|
2519
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2520
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2253
2521
|
}
|
|
2254
2522
|
}
|
|
2255
2523
|
const rawContrast = def.contrast;
|
|
2256
2524
|
if (rawContrast !== void 0) {
|
|
2257
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2525
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2258
2526
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2259
2527
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2260
|
-
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast,
|
|
2528
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2261
2529
|
let initialDirection;
|
|
2262
2530
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2263
2531
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
@@ -2296,11 +2564,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2296
2564
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2297
2565
|
const regDef = def;
|
|
2298
2566
|
const mode = regDef.mode ?? "auto";
|
|
2299
|
-
const isRoot =
|
|
2567
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2300
2568
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2301
2569
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2302
2570
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2303
|
-
const
|
|
2571
|
+
const config = configForColor(regDef, ctx.config);
|
|
2572
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2304
2573
|
return {
|
|
2305
2574
|
h: channels.hue,
|
|
2306
2575
|
s: channels.saturation,
|
|
@@ -2593,20 +2862,30 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2593
2862
|
value: String(variant.h)
|
|
2594
2863
|
}]
|
|
2595
2864
|
};
|
|
2596
|
-
if (regDef.hue === void 0 && regDef.darkHue === void 0) return {
|
|
2865
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2597
2866
|
hueVar: baseHueVar,
|
|
2598
2867
|
inline: false,
|
|
2599
2868
|
declarations: []
|
|
2600
2869
|
};
|
|
2601
2870
|
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2602
|
-
if (authored === void 0)
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2871
|
+
if (authored === void 0) {
|
|
2872
|
+
if (regDef.from !== void 0) return {
|
|
2873
|
+
hueVar: `var(${prop})`,
|
|
2874
|
+
inline: false,
|
|
2875
|
+
declarations: [{
|
|
2876
|
+
prop,
|
|
2877
|
+
value: String(variant.h)
|
|
2878
|
+
}]
|
|
2879
|
+
};
|
|
2880
|
+
return {
|
|
2881
|
+
hueVar: `var(${prop})`,
|
|
2882
|
+
inline: false,
|
|
2883
|
+
declarations: [{
|
|
2884
|
+
prop,
|
|
2885
|
+
value: baseHueVar
|
|
2886
|
+
}]
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2610
2889
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2611
2890
|
if (parsed.relative) {
|
|
2612
2891
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -3088,173 +3367,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3088
3367
|
};
|
|
3089
3368
|
}
|
|
3090
3369
|
/**
|
|
3091
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
3092
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
3093
|
-
*
|
|
3094
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
3095
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
3096
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
3097
|
-
* are out of scope.
|
|
3098
|
-
*/
|
|
3099
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
3100
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
3101
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
3102
|
-
return parseFloat(raw);
|
|
3103
|
-
}
|
|
3104
|
-
/**
|
|
3105
|
-
* Split the body of a CSS color function into its components and detect
|
|
3106
|
-
* whether an alpha channel was present.
|
|
3107
|
-
*
|
|
3108
|
-
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
3109
|
-
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
3110
|
-
* by the caller — standalone Glaze colors have no opacity field.
|
|
3111
|
-
*/
|
|
3112
|
-
function splitColorBody(body) {
|
|
3113
|
-
const slashIdx = body.indexOf("/");
|
|
3114
|
-
if (slashIdx !== -1) return {
|
|
3115
|
-
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
3116
|
-
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
3117
|
-
};
|
|
3118
|
-
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
3119
|
-
if (components.length === 4) {
|
|
3120
|
-
components.pop();
|
|
3121
|
-
return {
|
|
3122
|
-
components,
|
|
3123
|
-
hadAlpha: true
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
return {
|
|
3127
|
-
components,
|
|
3128
|
-
hadAlpha: false
|
|
3129
|
-
};
|
|
3130
|
-
}
|
|
3131
|
-
function warnDroppedAlpha(input) {
|
|
3132
|
-
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
3133
|
-
}
|
|
3134
|
-
function parseColorString(input) {
|
|
3135
|
-
if (input.startsWith("#")) {
|
|
3136
|
-
const parsed = parseHexAlpha(input);
|
|
3137
|
-
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
3138
|
-
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
3139
|
-
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
3140
|
-
return {
|
|
3141
|
-
h,
|
|
3142
|
-
s,
|
|
3143
|
-
l
|
|
3144
|
-
};
|
|
3145
|
-
}
|
|
3146
|
-
const m = input.match(COLOR_FN_RE);
|
|
3147
|
-
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
3148
|
-
const fn = m[1].toLowerCase();
|
|
3149
|
-
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
3150
|
-
if (hadAlpha) warnDroppedAlpha(input);
|
|
3151
|
-
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
3152
|
-
switch (fn) {
|
|
3153
|
-
case "rgb":
|
|
3154
|
-
case "rgba": {
|
|
3155
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3156
|
-
parseNumberOrPercent(components[0], 255) / 255,
|
|
3157
|
-
parseNumberOrPercent(components[1], 255) / 255,
|
|
3158
|
-
parseNumberOrPercent(components[2], 255) / 255
|
|
3159
|
-
]);
|
|
3160
|
-
return {
|
|
3161
|
-
h,
|
|
3162
|
-
s,
|
|
3163
|
-
l
|
|
3164
|
-
};
|
|
3165
|
-
}
|
|
3166
|
-
case "hsl":
|
|
3167
|
-
case "hsla": {
|
|
3168
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
3169
|
-
return {
|
|
3170
|
-
h: oh,
|
|
3171
|
-
s: os,
|
|
3172
|
-
l: ol
|
|
3173
|
-
};
|
|
3174
|
-
}
|
|
3175
|
-
case "okhsl": return {
|
|
3176
|
-
h: parseFloat(components[0]),
|
|
3177
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3178
|
-
l: parseNumberOrPercent(components[2], 1)
|
|
3179
|
-
};
|
|
3180
|
-
case "okhst": return okhstToOkhsl({
|
|
3181
|
-
h: parseFloat(components[0]),
|
|
3182
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3183
|
-
t: parseNumberOrPercent(components[2], 1)
|
|
3184
|
-
});
|
|
3185
|
-
case "oklch": {
|
|
3186
|
-
const L = parseNumberOrPercent(components[0], 1);
|
|
3187
|
-
const C = parseNumberOrPercent(components[1], .4);
|
|
3188
|
-
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
3189
|
-
const [h, s, l] = oklabToOkhsl([
|
|
3190
|
-
L,
|
|
3191
|
-
C * Math.cos(hRad),
|
|
3192
|
-
C * Math.sin(hRad)
|
|
3193
|
-
]);
|
|
3194
|
-
return {
|
|
3195
|
-
h,
|
|
3196
|
-
s,
|
|
3197
|
-
l
|
|
3198
|
-
};
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3201
|
-
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
3202
|
-
}
|
|
3203
|
-
/**
|
|
3204
|
-
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
3205
|
-
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
3206
|
-
*/
|
|
3207
|
-
function validateOkhslColor(value) {
|
|
3208
|
-
const { h, s, l } = value;
|
|
3209
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3210
|
-
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
3211
|
-
}
|
|
3212
|
-
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
3213
|
-
function validateRgbColor(value) {
|
|
3214
|
-
for (const key of [
|
|
3215
|
-
"r",
|
|
3216
|
-
"g",
|
|
3217
|
-
"b"
|
|
3218
|
-
]) {
|
|
3219
|
-
const n = value[key];
|
|
3220
|
-
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
|
-
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
3224
|
-
function validateOklchColor(value) {
|
|
3225
|
-
const { l, c, h } = value;
|
|
3226
|
-
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3227
|
-
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
3228
|
-
}
|
|
3229
|
-
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
3230
|
-
const hRad = hDeg * Math.PI / 180;
|
|
3231
|
-
const [h, s, outL] = oklabToOkhsl([
|
|
3232
|
-
l,
|
|
3233
|
-
c * Math.cos(hRad),
|
|
3234
|
-
c * Math.sin(hRad)
|
|
3235
|
-
]);
|
|
3236
|
-
return {
|
|
3237
|
-
h,
|
|
3238
|
-
s,
|
|
3239
|
-
l: outL
|
|
3240
|
-
};
|
|
3241
|
-
}
|
|
3242
|
-
function isRgbColorObject(value) {
|
|
3243
|
-
return "r" in value && "g" in value && "b" in value;
|
|
3244
|
-
}
|
|
3245
|
-
function isOklchColorObject(value) {
|
|
3246
|
-
return "c" in value && "l" in value && "h" in value;
|
|
3247
|
-
}
|
|
3248
|
-
function isOkhstColorObject(value) {
|
|
3249
|
-
return "t" in value && "h" in value && "s" in value;
|
|
3250
|
-
}
|
|
3251
|
-
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
3252
|
-
function validateOkhstColor(value) {
|
|
3253
|
-
const { h, s, t } = value;
|
|
3254
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3255
|
-
if (s > 1.5 || t > 1.5) throw new Error("glaze.color: OkhstColor s/t must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
3256
|
-
}
|
|
3257
|
-
/**
|
|
3258
3370
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3259
3371
|
* Must be a finite number in `0..=1`.
|
|
3260
3372
|
*/
|
|
@@ -3304,38 +3416,6 @@ function validateStandaloneName(name) {
|
|
|
3304
3416
|
}
|
|
3305
3417
|
}
|
|
3306
3418
|
/**
|
|
3307
|
-
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
3308
|
-
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
3309
|
-
* literal objects) go through one parser.
|
|
3310
|
-
*/
|
|
3311
|
-
function extractOkhslFromValue(value) {
|
|
3312
|
-
if (typeof value === "string") return parseColorString(value);
|
|
3313
|
-
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
3314
|
-
if (isRgbColorObject(value)) {
|
|
3315
|
-
validateRgbColor(value);
|
|
3316
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3317
|
-
value.r / 255,
|
|
3318
|
-
value.g / 255,
|
|
3319
|
-
value.b / 255
|
|
3320
|
-
]);
|
|
3321
|
-
return {
|
|
3322
|
-
h,
|
|
3323
|
-
s,
|
|
3324
|
-
l
|
|
3325
|
-
};
|
|
3326
|
-
}
|
|
3327
|
-
if (isOklchColorObject(value)) {
|
|
3328
|
-
validateOklchColor(value);
|
|
3329
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3330
|
-
}
|
|
3331
|
-
if (isOkhstColorObject(value)) {
|
|
3332
|
-
validateOkhstColor(value);
|
|
3333
|
-
return okhstToOkhsl(value);
|
|
3334
|
-
}
|
|
3335
|
-
validateOkhslColor(value);
|
|
3336
|
-
return value;
|
|
3337
|
-
}
|
|
3338
|
-
/**
|
|
3339
3419
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3340
3420
|
*
|
|
3341
3421
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|