@tenphi/glaze 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +408 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -16
- package/dist/index.d.mts +49 -16
- package/dist/index.mjs +408 -302
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +116 -37
- package/docs/methodology.md +15 -13
- package/docs/okhst.md +4 -0
- 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) {
|
|
@@ -952,10 +906,14 @@ function levelFraction(level) {
|
|
|
952
906
|
}
|
|
953
907
|
/**
|
|
954
908
|
* Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
|
|
955
|
-
* in `'auto'` mode
|
|
909
|
+
* in `'auto'` mode, where the normal variants take the authored normal entries
|
|
910
|
+
* as-is.
|
|
956
911
|
*
|
|
957
|
-
* Note `contrastLevel: 0` yields `0`, not `undefined
|
|
958
|
-
*
|
|
912
|
+
* Note `contrastLevel: 0` yields `0`, not `undefined`. The two select different
|
|
913
|
+
* branches, and an authored `0` is a slider position that `.export()` freezes
|
|
914
|
+
* where an absent level is not — but they are not an *output* distinction: at
|
|
915
|
+
* fraction 0 every interpolation returns its normal entry verbatim, so level 0
|
|
916
|
+
* reproduces `'auto'` bit for bit.
|
|
959
917
|
*/
|
|
960
918
|
function contrastFraction(config) {
|
|
961
919
|
const level = config.contrastLevel;
|
|
@@ -963,6 +921,20 @@ function contrastFraction(config) {
|
|
|
963
921
|
return levelFraction(level);
|
|
964
922
|
}
|
|
965
923
|
/**
|
|
924
|
+
* Whether the high-contrast variants are exact duplicates of the normal ones —
|
|
925
|
+
* true only at `contrastLevel: 100`, where the normal passes already resolve at
|
|
926
|
+
* full contrast.
|
|
927
|
+
*
|
|
928
|
+
* The level otherwise says nothing about high contrast: it positions the normal
|
|
929
|
+
* variants, while `lightContrast` / `darkContrast` stay the true high-contrast
|
|
930
|
+
* resolution at every level. This predicate marks the one level where that tier
|
|
931
|
+
* carries no new information, so callers can skip the two high-contrast passes,
|
|
932
|
+
* their drift checks, and the redundant output tier.
|
|
933
|
+
*/
|
|
934
|
+
function hcMirrorsNormal(config) {
|
|
935
|
+
return contrastFraction(config) === 1;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
966
938
|
* A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
|
|
967
939
|
*
|
|
968
940
|
* The endpoints return the authored entry *by identity* rather than through
|
|
@@ -1252,10 +1224,267 @@ function mapSaturationDark(s, mode, config) {
|
|
|
1252
1224
|
* endpoints (HC bypasses to full range, a manual `contrastLevel` widens
|
|
1253
1225
|
* toward it — see `activeWindow`).
|
|
1254
1226
|
*/
|
|
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)];
|
|
1227
|
+
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1228
|
+
if (mode === "static") return [0, 1];
|
|
1229
|
+
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1230
|
+
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/color-value.ts
|
|
1235
|
+
/**
|
|
1236
|
+
* Parsing and validation for `GlazeColorValue` — the literal color forms.
|
|
1237
|
+
*
|
|
1238
|
+
* Hex, the CSS color functions Glaze emits, and the four value-object shapes all
|
|
1239
|
+
* land here and come out as OKHSL. Split out of `color-token.ts` so both
|
|
1240
|
+
* consumers can reach it without an import cycle: standalone `glaze.color()`
|
|
1241
|
+
* tokens build on it, and the resolver needs it for a theme color's `from`. It
|
|
1242
|
+
* depends on nothing but the color math, which is what keeps that true.
|
|
1243
|
+
*/
|
|
1244
|
+
/**
|
|
1245
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1246
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1247
|
+
*
|
|
1248
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1249
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1250
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1251
|
+
* are out of scope.
|
|
1252
|
+
*/
|
|
1253
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1254
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1255
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1256
|
+
return parseFloat(raw);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Split the body of a CSS color function into its components and detect
|
|
1260
|
+
* whether an alpha channel was present.
|
|
1261
|
+
*
|
|
1262
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1263
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1264
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1265
|
+
*/
|
|
1266
|
+
function splitColorBody(body) {
|
|
1267
|
+
const slashIdx = body.indexOf("/");
|
|
1268
|
+
if (slashIdx !== -1) return {
|
|
1269
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1270
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1271
|
+
};
|
|
1272
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1273
|
+
if (components.length === 4) {
|
|
1274
|
+
components.pop();
|
|
1275
|
+
return {
|
|
1276
|
+
components,
|
|
1277
|
+
hadAlpha: true
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
components,
|
|
1282
|
+
hadAlpha: false
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function warnDroppedAlpha(input) {
|
|
1286
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1287
|
+
}
|
|
1288
|
+
function parseColorString(input) {
|
|
1289
|
+
if (input.startsWith("#")) {
|
|
1290
|
+
const parsed = parseHexAlpha(input);
|
|
1291
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1292
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1293
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1294
|
+
return {
|
|
1295
|
+
h,
|
|
1296
|
+
s,
|
|
1297
|
+
l
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
const m = input.match(COLOR_FN_RE);
|
|
1301
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1302
|
+
const fn = m[1].toLowerCase();
|
|
1303
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1304
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1305
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1306
|
+
switch (fn) {
|
|
1307
|
+
case "rgb":
|
|
1308
|
+
case "rgba": {
|
|
1309
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1310
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1311
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1312
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1313
|
+
]);
|
|
1314
|
+
return {
|
|
1315
|
+
h,
|
|
1316
|
+
s,
|
|
1317
|
+
l
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
case "hsl":
|
|
1321
|
+
case "hsla": {
|
|
1322
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1323
|
+
return {
|
|
1324
|
+
h: oh,
|
|
1325
|
+
s: os,
|
|
1326
|
+
l: ol
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
case "okhsl": return {
|
|
1330
|
+
h: parseFloat(components[0]),
|
|
1331
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1332
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1333
|
+
};
|
|
1334
|
+
case "okhst": return okhstToOkhsl({
|
|
1335
|
+
h: parseFloat(components[0]),
|
|
1336
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1337
|
+
t: parseNumberOrPercent(components[2], 1)
|
|
1338
|
+
});
|
|
1339
|
+
case "oklch": {
|
|
1340
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1341
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1342
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1343
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1344
|
+
L,
|
|
1345
|
+
C * Math.cos(hRad),
|
|
1346
|
+
C * Math.sin(hRad)
|
|
1347
|
+
]);
|
|
1348
|
+
return {
|
|
1349
|
+
h,
|
|
1350
|
+
s,
|
|
1351
|
+
l
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1359
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1360
|
+
*/
|
|
1361
|
+
function validateOkhslColor(value) {
|
|
1362
|
+
const { h, s, l } = value;
|
|
1363
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1364
|
+
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)?");
|
|
1365
|
+
}
|
|
1366
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1367
|
+
function validateRgbColor(value) {
|
|
1368
|
+
for (const key of [
|
|
1369
|
+
"r",
|
|
1370
|
+
"g",
|
|
1371
|
+
"b"
|
|
1372
|
+
]) {
|
|
1373
|
+
const n = value[key];
|
|
1374
|
+
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}).`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1378
|
+
function validateOklchColor(value) {
|
|
1379
|
+
const { l, c, h } = value;
|
|
1380
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1381
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1382
|
+
}
|
|
1383
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1384
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1385
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1386
|
+
l,
|
|
1387
|
+
c * Math.cos(hRad),
|
|
1388
|
+
c * Math.sin(hRad)
|
|
1389
|
+
]);
|
|
1390
|
+
return {
|
|
1391
|
+
h,
|
|
1392
|
+
s,
|
|
1393
|
+
l: outL
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
function isRgbColorObject(value) {
|
|
1397
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1398
|
+
}
|
|
1399
|
+
function isOklchColorObject(value) {
|
|
1400
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1401
|
+
}
|
|
1402
|
+
function isOkhstColorObject(value) {
|
|
1403
|
+
return "t" in value && "h" in value && "s" in value;
|
|
1404
|
+
}
|
|
1405
|
+
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
1406
|
+
function validateOkhstColor(value) {
|
|
1407
|
+
const { h, s, t } = value;
|
|
1408
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
1409
|
+
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)?");
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1413
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1414
|
+
* literal objects) go through one parser.
|
|
1415
|
+
*/
|
|
1416
|
+
function extractOkhslFromValue(value) {
|
|
1417
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1418
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1419
|
+
if (isRgbColorObject(value)) {
|
|
1420
|
+
validateRgbColor(value);
|
|
1421
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1422
|
+
value.r / 255,
|
|
1423
|
+
value.g / 255,
|
|
1424
|
+
value.b / 255
|
|
1425
|
+
]);
|
|
1426
|
+
return {
|
|
1427
|
+
h,
|
|
1428
|
+
s,
|
|
1429
|
+
l
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (isOklchColorObject(value)) {
|
|
1433
|
+
validateOklchColor(value);
|
|
1434
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1435
|
+
}
|
|
1436
|
+
if (isOkhstColorObject(value)) {
|
|
1437
|
+
validateOkhstColor(value);
|
|
1438
|
+
return okhstToOkhsl(value);
|
|
1439
|
+
}
|
|
1440
|
+
validateOkhslColor(value);
|
|
1441
|
+
return value;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
//#endregion
|
|
1445
|
+
//#region src/format-guard.ts
|
|
1446
|
+
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
1447
|
+
/**
|
|
1448
|
+
* Throw when a non-native Glaze color space is requested for an export that
|
|
1449
|
+
* emits raw CSS or non-Tasty token maps.
|
|
1450
|
+
*/
|
|
1451
|
+
function assertNativeFormat(format, method) {
|
|
1452
|
+
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}().`);
|
|
1453
|
+
}
|
|
1454
|
+
const SCHEME_FIELDS = [
|
|
1455
|
+
{
|
|
1456
|
+
field: "light",
|
|
1457
|
+
modes: () => true
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
field: "dark",
|
|
1461
|
+
modes: (m) => m.dark
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
field: "lightContrast",
|
|
1465
|
+
modes: (m) => m.highContrast
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
field: "darkContrast",
|
|
1469
|
+
modes: (m) => m.dark && m.highContrast
|
|
1470
|
+
}
|
|
1471
|
+
];
|
|
1472
|
+
/**
|
|
1473
|
+
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
1474
|
+
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
1475
|
+
* safe chroma (`computeSafeChromaOKLCH`).
|
|
1476
|
+
*/
|
|
1477
|
+
function assertAllPastel(resolved, modes) {
|
|
1478
|
+
const nonPastel = [];
|
|
1479
|
+
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
1480
|
+
if (!active(modes)) continue;
|
|
1481
|
+
if (color[field].pastel !== true) {
|
|
1482
|
+
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
1483
|
+
break;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (nonPastel.length === 0) return;
|
|
1487
|
+
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
1488
|
}
|
|
1260
1489
|
|
|
1261
1490
|
//#endregion
|
|
@@ -1896,6 +2125,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1896
2125
|
* its base / bg / fg / target dependencies.
|
|
1897
2126
|
*/
|
|
1898
2127
|
/**
|
|
2128
|
+
* Does this color sit at an absolute tone — either authored, or carried by a
|
|
2129
|
+
* `from` value? That is the test for "root" (placed on its own) as opposed to
|
|
2130
|
+
* "dependent" (placed relative to a base), and it is what lets a bare
|
|
2131
|
+
* `{ from: '#2F5BFF' }` stand as a complete color definition.
|
|
2132
|
+
*/
|
|
2133
|
+
function hasAbsoluteTone(def) {
|
|
2134
|
+
if (def.tone === void 0) return def.from !== void 0;
|
|
2135
|
+
return isAbsoluteTone(def.tone);
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
1899
2138
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1900
2139
|
*
|
|
1901
2140
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1934,11 +2173,16 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1934
2173
|
}
|
|
1935
2174
|
const regDef = def;
|
|
1936
2175
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
2176
|
+
if (regDef.from !== void 0) try {
|
|
2177
|
+
extractOkhslFromValue(regDef.from);
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
throw new Error(`glaze: color "${name}" has an invalid "from" value. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2180
|
+
}
|
|
1937
2181
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1938
2182
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1939
2183
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1940
2184
|
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 (!
|
|
2185
|
+
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
2186
|
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
2187
|
}
|
|
1944
2188
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2050,12 +2294,14 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
2050
2294
|
* Color resolution engine.
|
|
2051
2295
|
*
|
|
2052
2296
|
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
2053
|
-
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name
|
|
2054
|
-
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2297
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name. A manual
|
|
2298
|
+
* `contrastLevel` interpolates the authored HC pairs, tone window, and contrast
|
|
2299
|
+
* targets at that level for the two *normal* passes only; the high-contrast
|
|
2300
|
+
* passes bypass the level entirely, so the high-contrast slots are the same
|
|
2301
|
+
* values `'auto'` resolves. At level 100 the normal passes already are the
|
|
2302
|
+
* high-contrast ones, so the solver drops to two passes (light → dark) and
|
|
2303
|
+
* mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow,
|
|
2304
|
+
* and mix color defs.
|
|
2059
2305
|
*
|
|
2060
2306
|
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
2061
2307
|
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
@@ -2195,7 +2441,43 @@ function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config)
|
|
|
2195
2441
|
const shift = extremeLightTone - lightBase.t * 100;
|
|
2196
2442
|
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2197
2443
|
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2446
|
+
*
|
|
2447
|
+
* Deliberately not memoized. Keying a cache on the def object looks free — a
|
|
2448
|
+
* resolve reads this a handful of times per color — but defs are stored by
|
|
2449
|
+
* reference, so mutating one in place and re-setting it would invalidate the
|
|
2450
|
+
* theme's own cache while a def-keyed cache quietly served the old color. The
|
|
2451
|
+
* parse is a few microseconds against a resolve that is already cached; that is
|
|
2452
|
+
* not a trade worth a staleness bug.
|
|
2453
|
+
*/
|
|
2454
|
+
function fromSeed(def) {
|
|
2455
|
+
if (def.from === void 0) return void 0;
|
|
2456
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2457
|
+
return {
|
|
2458
|
+
hue: h,
|
|
2459
|
+
saturation: s,
|
|
2460
|
+
tone: toTone(l)
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
/**
|
|
2464
|
+
* The config this color resolves against.
|
|
2465
|
+
*
|
|
2466
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2467
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2468
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2469
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2470
|
+
* the color is expected to adapt.
|
|
2471
|
+
*/
|
|
2472
|
+
function configForColor(def, config) {
|
|
2473
|
+
if (def.from === void 0) return config;
|
|
2474
|
+
return {
|
|
2475
|
+
...config,
|
|
2476
|
+
lightTone: false
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2198
2479
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2480
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2199
2481
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2200
2482
|
}
|
|
2201
2483
|
/**
|
|
@@ -2214,16 +2496,20 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2214
2496
|
function resolveChannels(def, ctx, isDark) {
|
|
2215
2497
|
const mode = def.mode ?? "auto";
|
|
2216
2498
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2499
|
+
const seed = fromSeed(def);
|
|
2500
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2501
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2217
2502
|
if (!isDark || mode === "static") return {
|
|
2218
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2219
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2503
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2504
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2220
2505
|
};
|
|
2221
2506
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2222
2507
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2223
|
-
const
|
|
2224
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2508
|
+
const fromDark = fromSaturation !== void 0 && def.darkSaturation === void 0;
|
|
2509
|
+
const raw = fromDark ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2510
|
+
const explicitDark = fromDark ? false : def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2225
2511
|
return {
|
|
2226
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2512
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2227
2513
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2228
2514
|
};
|
|
2229
2515
|
}
|
|
@@ -2234,30 +2520,32 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2234
2520
|
const mode = def.mode ?? "auto";
|
|
2235
2521
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2236
2522
|
const pastel = effectivePastel;
|
|
2523
|
+
const config = configForColor(def, ctx.config);
|
|
2237
2524
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2238
2525
|
const baseTone = baseVariant.t * 100;
|
|
2239
2526
|
let preferredTone;
|
|
2240
2527
|
let isExtreme = false;
|
|
2241
|
-
const
|
|
2528
|
+
const seed = fromSeed(def);
|
|
2529
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2242
2530
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2243
2531
|
else {
|
|
2244
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2532
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2245
2533
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2246
2534
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2247
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2535
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2248
2536
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2249
2537
|
else {
|
|
2250
2538
|
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,
|
|
2539
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2540
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2253
2541
|
}
|
|
2254
2542
|
}
|
|
2255
2543
|
const rawContrast = def.contrast;
|
|
2256
2544
|
if (rawContrast !== void 0) {
|
|
2257
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2545
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2258
2546
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2259
2547
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2260
|
-
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast,
|
|
2548
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2261
2549
|
let initialDirection;
|
|
2262
2550
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2263
2551
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
@@ -2270,7 +2558,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2270
2558
|
flip,
|
|
2271
2559
|
pastel
|
|
2272
2560
|
};
|
|
2273
|
-
const level = contrastFraction(
|
|
2561
|
+
const level = isHighContrast ? void 0 : contrastFraction(config);
|
|
2274
2562
|
let preferInitial = false;
|
|
2275
2563
|
if (level !== void 0 && level > 0 && level < 1) {
|
|
2276
2564
|
initialDirection = findToneForContrast({
|
|
@@ -2296,11 +2584,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2296
2584
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2297
2585
|
const regDef = def;
|
|
2298
2586
|
const mode = regDef.mode ?? "auto";
|
|
2299
|
-
const isRoot =
|
|
2587
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2300
2588
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2301
2589
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2302
2590
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2303
|
-
const
|
|
2591
|
+
const config = configForColor(regDef, ctx.config);
|
|
2592
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2304
2593
|
return {
|
|
2305
2594
|
h: channels.hue,
|
|
2306
2595
|
s: channels.saturation,
|
|
@@ -2458,10 +2747,11 @@ function seedField(order, ctx, field, source) {
|
|
|
2458
2747
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
2459
2748
|
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
2460
2749
|
*
|
|
2461
|
-
* Under a manual `contrastLevel`
|
|
2462
|
-
*
|
|
2463
|
-
*
|
|
2464
|
-
*
|
|
2750
|
+
* Under a manual `contrastLevel` the spec is resolved at the level, so the check
|
|
2751
|
+
* measures each normal variant against the target it was actually solved for. At
|
|
2752
|
+
* level 100 the high-contrast slots are mirrors and are skipped: the normal
|
|
2753
|
+
* schemes already carry that check, and repeating it would emit a byte-identical
|
|
2754
|
+
* advisory under a different scheme label, which dedupes as a separate key.
|
|
2465
2755
|
*/
|
|
2466
2756
|
function verifyContrastDrift(order, defs, result, config) {
|
|
2467
2757
|
const roles = /* @__PURE__ */ new Map();
|
|
@@ -2496,9 +2786,9 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2496
2786
|
field: "darkContrast"
|
|
2497
2787
|
}
|
|
2498
2788
|
];
|
|
2499
|
-
const
|
|
2789
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2500
2790
|
for (const s of schemes) {
|
|
2501
|
-
if (
|
|
2791
|
+
if (mirrored && s.isHighContrast) continue;
|
|
2502
2792
|
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
|
|
2503
2793
|
const cVariant = color[s.field];
|
|
2504
2794
|
const bVariant = base[s.field];
|
|
@@ -2523,10 +2813,10 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2523
2813
|
roles: /* @__PURE__ */ new Map()
|
|
2524
2814
|
};
|
|
2525
2815
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
2526
|
-
const
|
|
2816
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2527
2817
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
2528
2818
|
let lightHCMap = lightMap;
|
|
2529
|
-
if (!
|
|
2819
|
+
if (!mirrored) {
|
|
2530
2820
|
seedField(order, ctx, "lightContrast", lightMap);
|
|
2531
2821
|
lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
2532
2822
|
}
|
|
@@ -2534,7 +2824,7 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2534
2824
|
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
2535
2825
|
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
2536
2826
|
let darkHCMap = darkMap;
|
|
2537
|
-
if (!
|
|
2827
|
+
if (!mirrored) {
|
|
2538
2828
|
seedField(order, ctx, "darkContrast", darkMap);
|
|
2539
2829
|
darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
2540
2830
|
}
|
|
@@ -2593,20 +2883,30 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2593
2883
|
value: String(variant.h)
|
|
2594
2884
|
}]
|
|
2595
2885
|
};
|
|
2596
|
-
if (regDef.hue === void 0 && regDef.darkHue === void 0) return {
|
|
2886
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2597
2887
|
hueVar: baseHueVar,
|
|
2598
2888
|
inline: false,
|
|
2599
2889
|
declarations: []
|
|
2600
2890
|
};
|
|
2601
2891
|
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2602
|
-
if (authored === void 0)
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2892
|
+
if (authored === void 0) {
|
|
2893
|
+
if (regDef.from !== void 0) return {
|
|
2894
|
+
hueVar: `var(${prop})`,
|
|
2895
|
+
inline: false,
|
|
2896
|
+
declarations: [{
|
|
2897
|
+
prop,
|
|
2898
|
+
value: String(variant.h)
|
|
2899
|
+
}]
|
|
2900
|
+
};
|
|
2901
|
+
return {
|
|
2902
|
+
hueVar: `var(${prop})`,
|
|
2903
|
+
inline: false,
|
|
2904
|
+
declarations: [{
|
|
2905
|
+
prop,
|
|
2906
|
+
value: baseHueVar
|
|
2907
|
+
}]
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2610
2910
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2611
2911
|
if (parsed.relative) {
|
|
2612
2912
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2758,23 +3058,28 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
2758
3058
|
/**
|
|
2759
3059
|
* Resolve the effective output modes for an export.
|
|
2760
3060
|
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
2765
|
-
*
|
|
2766
|
-
*
|
|
3061
|
+
* `modes.highContrast` and `contrastLevel` are independent: the level positions
|
|
3062
|
+
* the normal variants, the high-contrast tier stays the true high-contrast
|
|
3063
|
+
* resolution, and the two compose — a slider raises the baseline while a
|
|
3064
|
+
* `prefers-contrast: more` block still escalates on top of it.
|
|
3065
|
+
*
|
|
3066
|
+
* The one exception is a **global** level of 100, where the normal variants
|
|
3067
|
+
* already *are* the high-contrast ones. The tier would be an exact duplicate, so
|
|
3068
|
+
* it is dropped and a single light/dark set is emitted — including against an
|
|
3069
|
+
* explicit `highContrast: true` override, which has nothing different left to
|
|
3070
|
+
* ask for.
|
|
2767
3071
|
*
|
|
2768
3072
|
* A level set on a single theme or token does not change which modes are
|
|
2769
|
-
* emitted:
|
|
2770
|
-
*
|
|
2771
|
-
* the
|
|
3073
|
+
* emitted: `modes` is global-only, and a palette must not have one sibling
|
|
3074
|
+
* collapse the shared token structure. A per-instance level of 100 therefore
|
|
3075
|
+
* still reports into the tier, with values equal to its own normal ones.
|
|
2772
3076
|
*/
|
|
2773
3077
|
function resolveModes(override) {
|
|
2774
3078
|
const cfg = getConfig();
|
|
3079
|
+
const highContrast = override?.highContrast ?? cfg.modes.highContrast;
|
|
2775
3080
|
return {
|
|
2776
3081
|
dark: override?.dark ?? cfg.modes.dark,
|
|
2777
|
-
highContrast:
|
|
3082
|
+
highContrast: highContrast && !hcMirrorsNormal(cfg)
|
|
2778
3083
|
};
|
|
2779
3084
|
}
|
|
2780
3085
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|
|
@@ -3088,173 +3393,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3088
3393
|
};
|
|
3089
3394
|
}
|
|
3090
3395
|
/**
|
|
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
3396
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3259
3397
|
* Must be a finite number in `0..=1`.
|
|
3260
3398
|
*/
|
|
@@ -3304,38 +3442,6 @@ function validateStandaloneName(name) {
|
|
|
3304
3442
|
}
|
|
3305
3443
|
}
|
|
3306
3444
|
/**
|
|
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
3445
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3340
3446
|
*
|
|
3341
3447
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|