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