@tenphi/glaze 0.0.0-snapshot.8233469 → 0.0.0-snapshot.84edd5e
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 +402 -317
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -18
- package/dist/index.d.mts +42 -18
- package/dist/index.mjs +402 -317
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +73 -15
- package/docs/okhst.md +6 -14
- 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) {
|
|
@@ -1038,7 +992,6 @@ function parseToneValueAt(p, f) {
|
|
|
1038
992
|
if (!Array.isArray(p)) return parseToneValue(p);
|
|
1039
993
|
if (f <= 0) return parseToneValue(p[0]);
|
|
1040
994
|
if (f >= 1) return parseToneValue(p[1]);
|
|
1041
|
-
if (p[0] === p[1]) return parseToneValue(p[0]);
|
|
1042
995
|
const normal = parseToneValue(p[0]);
|
|
1043
996
|
const hc = parseToneValue(p[1]);
|
|
1044
997
|
if (normal.kind === hc.kind && normal.kind !== "extreme") return {
|
|
@@ -1259,6 +1212,263 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
1259
1212
|
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1260
1213
|
}
|
|
1261
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.`);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1262
1472
|
//#endregion
|
|
1263
1473
|
//#region src/contrast-solver.ts
|
|
1264
1474
|
/**
|
|
@@ -1897,6 +2107,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1897
2107
|
* its base / bg / fg / target dependencies.
|
|
1898
2108
|
*/
|
|
1899
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
|
+
/**
|
|
1900
2120
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1901
2121
|
*
|
|
1902
2122
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1939,7 +2159,7 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1939
2159
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1940
2160
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1941
2161
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1942
|
-
if (!
|
|
2162
|
+
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).`);
|
|
1943
2163
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1944
2164
|
}
|
|
1945
2165
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2155,8 +2375,7 @@ function resolveContrastSpec(spec, isHighContrast, config, polarity) {
|
|
|
2155
2375
|
*/
|
|
2156
2376
|
function passTone(tone, isHighContrast, config) {
|
|
2157
2377
|
const f = contrastFraction(config);
|
|
2158
|
-
|
|
2159
|
-
return parseToneValueAt(tone, f);
|
|
2378
|
+
return f === void 0 || isHighContrast ? parseToneValue(isHighContrast ? pairHC(tone) : pairNormal(tone)) : parseToneValueAt(tone, f);
|
|
2160
2379
|
}
|
|
2161
2380
|
/** The authored numeric pair for this pass (shadow `intensity`, mix `value`). */
|
|
2162
2381
|
function passNumber(p, isHighContrast, config) {
|
|
@@ -2190,11 +2409,52 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
2190
2409
|
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2191
2410
|
* intended, since the author asked for the extreme.
|
|
2192
2411
|
*/
|
|
2193
|
-
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved,
|
|
2194
|
-
const
|
|
2195
|
-
|
|
2412
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config) {
|
|
2413
|
+
const extremeLightTone = mapToneForScheme(authorTone, mode, false, isHighContrast, config);
|
|
2414
|
+
const lightBase = getSchemeVariant(baseResolved, false, isHighContrast);
|
|
2415
|
+
const darkBase = getSchemeVariant(baseResolved, true, isHighContrast);
|
|
2416
|
+
const shift = extremeLightTone - lightBase.t * 100;
|
|
2417
|
+
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2421
|
+
*
|
|
2422
|
+
* Memoized on the def object: a single resolve pass reads it once per scheme per
|
|
2423
|
+
* color, and the parse is pure, so the cache is a `WeakMap` keyed by the def
|
|
2424
|
+
* itself rather than a per-pass structure that would have to be threaded around.
|
|
2425
|
+
*/
|
|
2426
|
+
const fromSeeds = /* @__PURE__ */ new WeakMap();
|
|
2427
|
+
function fromSeed(def) {
|
|
2428
|
+
if (def.from === void 0) return void 0;
|
|
2429
|
+
const cached = fromSeeds.get(def);
|
|
2430
|
+
if (cached) return cached;
|
|
2431
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2432
|
+
const seed = {
|
|
2433
|
+
hue: h,
|
|
2434
|
+
saturation: s,
|
|
2435
|
+
tone: toTone(l)
|
|
2436
|
+
};
|
|
2437
|
+
fromSeeds.set(def, seed);
|
|
2438
|
+
return seed;
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* The config this color resolves against.
|
|
2442
|
+
*
|
|
2443
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2444
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2445
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2446
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2447
|
+
* the color is expected to adapt.
|
|
2448
|
+
*/
|
|
2449
|
+
function configForColor(def, config) {
|
|
2450
|
+
if (def.from === void 0) return config;
|
|
2451
|
+
return {
|
|
2452
|
+
...config,
|
|
2453
|
+
lightTone: false
|
|
2454
|
+
};
|
|
2196
2455
|
}
|
|
2197
2456
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2457
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2198
2458
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2199
2459
|
}
|
|
2200
2460
|
/**
|
|
@@ -2213,16 +2473,19 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2213
2473
|
function resolveChannels(def, ctx, isDark) {
|
|
2214
2474
|
const mode = def.mode ?? "auto";
|
|
2215
2475
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2476
|
+
const seed = fromSeed(def);
|
|
2477
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2478
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2216
2479
|
if (!isDark || mode === "static") return {
|
|
2217
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2218
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2480
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2481
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2219
2482
|
};
|
|
2220
2483
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2221
2484
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2222
2485
|
const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2223
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2486
|
+
const raw = fromSaturation !== void 0 && def.darkSaturation === void 0 ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2224
2487
|
return {
|
|
2225
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2488
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2226
2489
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2227
2490
|
};
|
|
2228
2491
|
}
|
|
@@ -2233,63 +2496,59 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2233
2496
|
const mode = def.mode ?? "auto";
|
|
2234
2497
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2235
2498
|
const pastel = effectivePastel;
|
|
2499
|
+
const config = configForColor(def, ctx.config);
|
|
2236
2500
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2237
2501
|
const baseTone = baseVariant.t * 100;
|
|
2238
2502
|
let preferredTone;
|
|
2239
2503
|
let isExtreme = false;
|
|
2240
|
-
const
|
|
2504
|
+
const seed = fromSeed(def);
|
|
2505
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2241
2506
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2242
2507
|
else {
|
|
2243
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2508
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2244
2509
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2245
2510
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2246
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2511
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2247
2512
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2248
|
-
else
|
|
2249
|
-
isExtreme = true;
|
|
2250
|
-
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2251
|
-
} else {
|
|
2513
|
+
else {
|
|
2252
2514
|
isExtreme = parsed.kind === "extreme";
|
|
2253
|
-
preferredTone =
|
|
2515
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2516
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2254
2517
|
}
|
|
2255
2518
|
}
|
|
2256
2519
|
const rawContrast = def.contrast;
|
|
2257
2520
|
if (rawContrast !== void 0) {
|
|
2258
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2521
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2259
2522
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2260
2523
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2261
|
-
const
|
|
2524
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2262
2525
|
let initialDirection;
|
|
2263
2526
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2264
2527
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2265
|
-
const
|
|
2528
|
+
const solve = {
|
|
2529
|
+
hue: channels.hue,
|
|
2530
|
+
saturation: channels.saturation,
|
|
2531
|
+
preferredTone: clamp(preferredTone / 100, preferredRange[0], preferredRange[1]),
|
|
2532
|
+
baseLinearRgb,
|
|
2533
|
+
toneRange: [0, 1],
|
|
2534
|
+
flip,
|
|
2535
|
+
pastel
|
|
2536
|
+
};
|
|
2266
2537
|
const level = contrastFraction(ctx.config);
|
|
2267
2538
|
let preferInitial = false;
|
|
2268
2539
|
if (level !== void 0 && level > 0 && level < 1) {
|
|
2269
2540
|
initialDirection = findToneForContrast({
|
|
2270
|
-
|
|
2271
|
-
saturation: channels.saturation,
|
|
2272
|
-
preferredTone: seedTone,
|
|
2273
|
-
baseLinearRgb,
|
|
2541
|
+
...solve,
|
|
2274
2542
|
contrast: resolveContrastForLevel(rawContrast, level < PAIR_SWITCH ? 0 : 100, polarity),
|
|
2275
|
-
|
|
2276
|
-
initialDirection,
|
|
2277
|
-
flip,
|
|
2278
|
-
pastel
|
|
2543
|
+
initialDirection
|
|
2279
2544
|
}).tone * 100 < baseTone ? "darker" : "lighter";
|
|
2280
2545
|
preferInitial = true;
|
|
2281
2546
|
}
|
|
2282
2547
|
const result = findToneForContrast({
|
|
2283
|
-
|
|
2284
|
-
saturation: channels.saturation,
|
|
2285
|
-
preferredTone: seedTone,
|
|
2286
|
-
baseLinearRgb,
|
|
2548
|
+
...solve,
|
|
2287
2549
|
contrast: resolvedContrast,
|
|
2288
|
-
toneRange: [0, 1],
|
|
2289
2550
|
initialDirection,
|
|
2290
|
-
|
|
2291
|
-
preferInitial,
|
|
2292
|
-
pastel
|
|
2551
|
+
preferInitial
|
|
2293
2552
|
});
|
|
2294
2553
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2295
2554
|
return result.tone * 100;
|
|
@@ -2301,11 +2560,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2301
2560
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2302
2561
|
const regDef = def;
|
|
2303
2562
|
const mode = regDef.mode ?? "auto";
|
|
2304
|
-
const isRoot =
|
|
2563
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2305
2564
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2306
2565
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2307
2566
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2308
|
-
const
|
|
2567
|
+
const config = configForColor(regDef, ctx.config);
|
|
2568
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2309
2569
|
return {
|
|
2310
2570
|
h: channels.hue,
|
|
2311
2571
|
s: channels.saturation,
|
|
@@ -2479,7 +2739,7 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2479
2739
|
const base = result.get(regDef.base);
|
|
2480
2740
|
if (!color || !base) continue;
|
|
2481
2741
|
const polarity = roleToPolarity(resolveRoleInMap(name, def, defs, config.inferRole, roles));
|
|
2482
|
-
const
|
|
2742
|
+
const schemes = [
|
|
2483
2743
|
{
|
|
2484
2744
|
isDark: false,
|
|
2485
2745
|
isHighContrast: false,
|
|
@@ -2501,8 +2761,9 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2501
2761
|
field: "darkContrast"
|
|
2502
2762
|
}
|
|
2503
2763
|
];
|
|
2504
|
-
const
|
|
2764
|
+
const manual = contrastFraction(config) !== void 0;
|
|
2505
2765
|
for (const s of schemes) {
|
|
2766
|
+
if (manual && s.isHighContrast) continue;
|
|
2506
2767
|
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
|
|
2507
2768
|
const cVariant = color[s.field];
|
|
2508
2769
|
const bVariant = base[s.field];
|
|
@@ -2565,7 +2826,9 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2565
2826
|
*
|
|
2566
2827
|
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2567
2828
|
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2568
|
-
* set is emitted only when it actually differs — see `
|
|
2829
|
+
* set is emitted only when it actually differs — see `buildHueDeclarations`.
|
|
2830
|
+
* A color that authors a hue in *either* scheme gets its own var in both, so
|
|
2831
|
+
* the shared `var()` reference stays valid.
|
|
2569
2832
|
*/
|
|
2570
2833
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2571
2834
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2587,23 +2850,38 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2587
2850
|
const regDef = def;
|
|
2588
2851
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2589
2852
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2590
|
-
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2591
|
-
|
|
2853
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) return {
|
|
2854
|
+
hueVar: `var(${prop})`,
|
|
2855
|
+
inline: false,
|
|
2856
|
+
declarations: [{
|
|
2857
|
+
prop,
|
|
2858
|
+
value: String(variant.h)
|
|
2859
|
+
}]
|
|
2860
|
+
};
|
|
2861
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2862
|
+
hueVar: baseHueVar,
|
|
2863
|
+
inline: false,
|
|
2864
|
+
declarations: []
|
|
2865
|
+
};
|
|
2866
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2867
|
+
if (authored === void 0) {
|
|
2868
|
+
if (regDef.from !== void 0) return {
|
|
2869
|
+
hueVar: `var(${prop})`,
|
|
2870
|
+
inline: false,
|
|
2871
|
+
declarations: [{
|
|
2872
|
+
prop,
|
|
2873
|
+
value: String(variant.h)
|
|
2874
|
+
}]
|
|
2875
|
+
};
|
|
2592
2876
|
return {
|
|
2593
2877
|
hueVar: `var(${prop})`,
|
|
2594
2878
|
inline: false,
|
|
2595
2879
|
declarations: [{
|
|
2596
2880
|
prop,
|
|
2597
|
-
value:
|
|
2881
|
+
value: baseHueVar
|
|
2598
2882
|
}]
|
|
2599
2883
|
};
|
|
2600
2884
|
}
|
|
2601
|
-
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2602
|
-
if (authored === void 0) return {
|
|
2603
|
-
hueVar: baseHueVar,
|
|
2604
|
-
inline: false,
|
|
2605
|
-
declarations: []
|
|
2606
|
-
};
|
|
2607
2885
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2608
2886
|
if (parsed.relative) {
|
|
2609
2887
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2650,7 +2928,7 @@ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
|
2650
2928
|
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2651
2929
|
}
|
|
2652
2930
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2653
|
-
function collectHueDeclarations(resolved, ctx, scheme
|
|
2931
|
+
function collectHueDeclarations(resolved, ctx, scheme) {
|
|
2654
2932
|
if (ctx.emitDeclarations === false) return [];
|
|
2655
2933
|
const seen = /* @__PURE__ */ new Set();
|
|
2656
2934
|
const out = [];
|
|
@@ -2671,21 +2949,24 @@ function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
|
2671
2949
|
return out;
|
|
2672
2950
|
}
|
|
2673
2951
|
/**
|
|
2674
|
-
*
|
|
2675
|
-
* light
|
|
2952
|
+
* Hue declarations for both schemes. `dark` is empty when it resolves to the
|
|
2953
|
+
* same hues as light — the common case, with no `darkHue` anywhere.
|
|
2676
2954
|
*
|
|
2677
|
-
* When they differ, the **whole** set is returned rather than only the
|
|
2678
|
-
* whose text changed. A relative declaration reads
|
|
2955
|
+
* When they differ, the **whole** dark set is returned rather than only the
|
|
2956
|
+
* entries whose text changed. A relative declaration reads
|
|
2679
2957
|
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2680
2958
|
* moves with the seed, and custom properties substitute `var()` at
|
|
2681
2959
|
* computed-value time on the declaring element — so re-declaring
|
|
2682
2960
|
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2683
2961
|
* the light-substituted value.
|
|
2684
2962
|
*/
|
|
2685
|
-
function
|
|
2686
|
-
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2963
|
+
function buildHueDeclarations(resolved, ctx) {
|
|
2687
2964
|
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2688
|
-
|
|
2965
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2966
|
+
return {
|
|
2967
|
+
light,
|
|
2968
|
+
dark: dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark
|
|
2969
|
+
};
|
|
2689
2970
|
}
|
|
2690
2971
|
function buildHuePlans(resolved, ctx) {
|
|
2691
2972
|
const plans = /* @__PURE__ */ new Map();
|
|
@@ -2752,30 +3033,32 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
2752
3033
|
/**
|
|
2753
3034
|
* Resolve the effective output modes for an export.
|
|
2754
3035
|
*
|
|
2755
|
-
* A global manual `contrastLevel`
|
|
2756
|
-
*
|
|
2757
|
-
*
|
|
2758
|
-
*
|
|
2759
|
-
*
|
|
3036
|
+
* A global manual `contrastLevel` turns off high-contrast output outright: the
|
|
3037
|
+
* level *is* the contrast preference, so a separate high-contrast tier has no
|
|
3038
|
+
* meaning alongside it. `modes.highContrast` therefore reads as "emit a separate
|
|
3039
|
+
* high-contrast set **when** contrast is automatic" — it goes inert under a
|
|
3040
|
+
* manual level rather than fighting it, and silently, since switching a
|
|
3041
|
+
* preference from auto to manual is normal use and not a mistake to report.
|
|
2760
3042
|
*
|
|
2761
3043
|
* A level set on a single theme or token does not change which modes are
|
|
2762
3044
|
* emitted: sibling themes in a palette may still have a real high-contrast
|
|
2763
|
-
* tier, and the manual one correctly reports its own
|
|
3045
|
+
* tier, and the manual one correctly reports its own resolved values there —
|
|
3046
|
+
* the alternative would drop its colors from that tier entirely.
|
|
2764
3047
|
*/
|
|
2765
3048
|
function resolveModes(override) {
|
|
2766
3049
|
const cfg = getConfig();
|
|
2767
|
-
const manual = contrastFraction(cfg) !== void 0;
|
|
2768
3050
|
return {
|
|
2769
3051
|
dark: override?.dark ?? cfg.modes.dark,
|
|
2770
|
-
highContrast:
|
|
3052
|
+
highContrast: contrastFraction(cfg) !== void 0 ? false : override?.highContrast ?? cfg.modes.highContrast
|
|
2771
3053
|
};
|
|
2772
3054
|
}
|
|
2773
3055
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|
|
2774
3056
|
const tokens = {};
|
|
2775
3057
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2776
3058
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2777
|
-
|
|
2778
|
-
|
|
3059
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3060
|
+
for (const decl of hueDecls.light) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
3061
|
+
if (modes.dark) for (const decl of hueDecls.dark) {
|
|
2779
3062
|
const key = `$${decl.prop.slice(2)}`;
|
|
2780
3063
|
tokens[key] = {
|
|
2781
3064
|
...tokens[key],
|
|
@@ -2832,8 +3115,9 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2832
3115
|
};
|
|
2833
3116
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2834
3117
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2835
|
-
|
|
2836
|
-
for (const decl of
|
|
3118
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3119
|
+
for (const decl of hueDecls.light) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
3120
|
+
for (const decl of hueDecls.dark) {
|
|
2837
3121
|
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2838
3122
|
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2839
3123
|
}
|
|
@@ -3079,173 +3363,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3079
3363
|
};
|
|
3080
3364
|
}
|
|
3081
3365
|
/**
|
|
3082
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
3083
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
3084
|
-
*
|
|
3085
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
3086
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
3087
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
3088
|
-
* are out of scope.
|
|
3089
|
-
*/
|
|
3090
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
3091
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
3092
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
3093
|
-
return parseFloat(raw);
|
|
3094
|
-
}
|
|
3095
|
-
/**
|
|
3096
|
-
* Split the body of a CSS color function into its components and detect
|
|
3097
|
-
* whether an alpha channel was present.
|
|
3098
|
-
*
|
|
3099
|
-
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
3100
|
-
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
3101
|
-
* by the caller — standalone Glaze colors have no opacity field.
|
|
3102
|
-
*/
|
|
3103
|
-
function splitColorBody(body) {
|
|
3104
|
-
const slashIdx = body.indexOf("/");
|
|
3105
|
-
if (slashIdx !== -1) return {
|
|
3106
|
-
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
3107
|
-
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
3108
|
-
};
|
|
3109
|
-
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
3110
|
-
if (components.length === 4) {
|
|
3111
|
-
components.pop();
|
|
3112
|
-
return {
|
|
3113
|
-
components,
|
|
3114
|
-
hadAlpha: true
|
|
3115
|
-
};
|
|
3116
|
-
}
|
|
3117
|
-
return {
|
|
3118
|
-
components,
|
|
3119
|
-
hadAlpha: false
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
function warnDroppedAlpha(input) {
|
|
3123
|
-
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
3124
|
-
}
|
|
3125
|
-
function parseColorString(input) {
|
|
3126
|
-
if (input.startsWith("#")) {
|
|
3127
|
-
const parsed = parseHexAlpha(input);
|
|
3128
|
-
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
3129
|
-
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
3130
|
-
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
3131
|
-
return {
|
|
3132
|
-
h,
|
|
3133
|
-
s,
|
|
3134
|
-
l
|
|
3135
|
-
};
|
|
3136
|
-
}
|
|
3137
|
-
const m = input.match(COLOR_FN_RE);
|
|
3138
|
-
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
3139
|
-
const fn = m[1].toLowerCase();
|
|
3140
|
-
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
3141
|
-
if (hadAlpha) warnDroppedAlpha(input);
|
|
3142
|
-
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
3143
|
-
switch (fn) {
|
|
3144
|
-
case "rgb":
|
|
3145
|
-
case "rgba": {
|
|
3146
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3147
|
-
parseNumberOrPercent(components[0], 255) / 255,
|
|
3148
|
-
parseNumberOrPercent(components[1], 255) / 255,
|
|
3149
|
-
parseNumberOrPercent(components[2], 255) / 255
|
|
3150
|
-
]);
|
|
3151
|
-
return {
|
|
3152
|
-
h,
|
|
3153
|
-
s,
|
|
3154
|
-
l
|
|
3155
|
-
};
|
|
3156
|
-
}
|
|
3157
|
-
case "hsl":
|
|
3158
|
-
case "hsla": {
|
|
3159
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
3160
|
-
return {
|
|
3161
|
-
h: oh,
|
|
3162
|
-
s: os,
|
|
3163
|
-
l: ol
|
|
3164
|
-
};
|
|
3165
|
-
}
|
|
3166
|
-
case "okhsl": return {
|
|
3167
|
-
h: parseFloat(components[0]),
|
|
3168
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3169
|
-
l: parseNumberOrPercent(components[2], 1)
|
|
3170
|
-
};
|
|
3171
|
-
case "okhst": return okhstToOkhsl({
|
|
3172
|
-
h: parseFloat(components[0]),
|
|
3173
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3174
|
-
t: parseNumberOrPercent(components[2], 1)
|
|
3175
|
-
});
|
|
3176
|
-
case "oklch": {
|
|
3177
|
-
const L = parseNumberOrPercent(components[0], 1);
|
|
3178
|
-
const C = parseNumberOrPercent(components[1], .4);
|
|
3179
|
-
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
3180
|
-
const [h, s, l] = oklabToOkhsl([
|
|
3181
|
-
L,
|
|
3182
|
-
C * Math.cos(hRad),
|
|
3183
|
-
C * Math.sin(hRad)
|
|
3184
|
-
]);
|
|
3185
|
-
return {
|
|
3186
|
-
h,
|
|
3187
|
-
s,
|
|
3188
|
-
l
|
|
3189
|
-
};
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
3193
|
-
}
|
|
3194
|
-
/**
|
|
3195
|
-
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
3196
|
-
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
3197
|
-
*/
|
|
3198
|
-
function validateOkhslColor(value) {
|
|
3199
|
-
const { h, s, l } = value;
|
|
3200
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3201
|
-
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)?");
|
|
3202
|
-
}
|
|
3203
|
-
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
3204
|
-
function validateRgbColor(value) {
|
|
3205
|
-
for (const key of [
|
|
3206
|
-
"r",
|
|
3207
|
-
"g",
|
|
3208
|
-
"b"
|
|
3209
|
-
]) {
|
|
3210
|
-
const n = value[key];
|
|
3211
|
-
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}).`);
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
3215
|
-
function validateOklchColor(value) {
|
|
3216
|
-
const { l, c, h } = value;
|
|
3217
|
-
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3218
|
-
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
3219
|
-
}
|
|
3220
|
-
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
3221
|
-
const hRad = hDeg * Math.PI / 180;
|
|
3222
|
-
const [h, s, outL] = oklabToOkhsl([
|
|
3223
|
-
l,
|
|
3224
|
-
c * Math.cos(hRad),
|
|
3225
|
-
c * Math.sin(hRad)
|
|
3226
|
-
]);
|
|
3227
|
-
return {
|
|
3228
|
-
h,
|
|
3229
|
-
s,
|
|
3230
|
-
l: outL
|
|
3231
|
-
};
|
|
3232
|
-
}
|
|
3233
|
-
function isRgbColorObject(value) {
|
|
3234
|
-
return "r" in value && "g" in value && "b" in value;
|
|
3235
|
-
}
|
|
3236
|
-
function isOklchColorObject(value) {
|
|
3237
|
-
return "c" in value && "l" in value && "h" in value;
|
|
3238
|
-
}
|
|
3239
|
-
function isOkhstColorObject(value) {
|
|
3240
|
-
return "t" in value && "h" in value && "s" in value;
|
|
3241
|
-
}
|
|
3242
|
-
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
3243
|
-
function validateOkhstColor(value) {
|
|
3244
|
-
const { h, s, t } = value;
|
|
3245
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3246
|
-
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)?");
|
|
3247
|
-
}
|
|
3248
|
-
/**
|
|
3249
3366
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3250
3367
|
* Must be a finite number in `0..=1`.
|
|
3251
3368
|
*/
|
|
@@ -3295,38 +3412,6 @@ function validateStandaloneName(name) {
|
|
|
3295
3412
|
}
|
|
3296
3413
|
}
|
|
3297
3414
|
/**
|
|
3298
|
-
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
3299
|
-
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
3300
|
-
* literal objects) go through one parser.
|
|
3301
|
-
*/
|
|
3302
|
-
function extractOkhslFromValue(value) {
|
|
3303
|
-
if (typeof value === "string") return parseColorString(value);
|
|
3304
|
-
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
3305
|
-
if (isRgbColorObject(value)) {
|
|
3306
|
-
validateRgbColor(value);
|
|
3307
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3308
|
-
value.r / 255,
|
|
3309
|
-
value.g / 255,
|
|
3310
|
-
value.b / 255
|
|
3311
|
-
]);
|
|
3312
|
-
return {
|
|
3313
|
-
h,
|
|
3314
|
-
s,
|
|
3315
|
-
l
|
|
3316
|
-
};
|
|
3317
|
-
}
|
|
3318
|
-
if (isOklchColorObject(value)) {
|
|
3319
|
-
validateOklchColor(value);
|
|
3320
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3321
|
-
}
|
|
3322
|
-
if (isOkhstColorObject(value)) {
|
|
3323
|
-
validateOkhstColor(value);
|
|
3324
|
-
return okhstToOkhsl(value);
|
|
3325
|
-
}
|
|
3326
|
-
validateOkhslColor(value);
|
|
3327
|
-
return value;
|
|
3328
|
-
}
|
|
3329
|
-
/**
|
|
3330
3415
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3331
3416
|
*
|
|
3332
3417
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|