@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.mjs
CHANGED
|
@@ -879,52 +879,6 @@ function isPaletteExport(data) {
|
|
|
879
879
|
return hasPaletteShape(data);
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
-
//#endregion
|
|
883
|
-
//#region src/format-guard.ts
|
|
884
|
-
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
885
|
-
/**
|
|
886
|
-
* Throw when a non-native Glaze color space is requested for an export that
|
|
887
|
-
* emits raw CSS or non-Tasty token maps.
|
|
888
|
-
*/
|
|
889
|
-
function assertNativeFormat(format, method) {
|
|
890
|
-
if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
|
|
891
|
-
}
|
|
892
|
-
const SCHEME_FIELDS = [
|
|
893
|
-
{
|
|
894
|
-
field: "light",
|
|
895
|
-
modes: () => true
|
|
896
|
-
},
|
|
897
|
-
{
|
|
898
|
-
field: "dark",
|
|
899
|
-
modes: (m) => m.dark
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
field: "lightContrast",
|
|
903
|
-
modes: (m) => m.highContrast
|
|
904
|
-
},
|
|
905
|
-
{
|
|
906
|
-
field: "darkContrast",
|
|
907
|
-
modes: (m) => m.dark && m.highContrast
|
|
908
|
-
}
|
|
909
|
-
];
|
|
910
|
-
/**
|
|
911
|
-
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
912
|
-
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
913
|
-
* safe chroma (`computeSafeChromaOKLCH`).
|
|
914
|
-
*/
|
|
915
|
-
function assertAllPastel(resolved, modes) {
|
|
916
|
-
const nonPastel = [];
|
|
917
|
-
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
918
|
-
if (!active(modes)) continue;
|
|
919
|
-
if (color[field].pastel !== true) {
|
|
920
|
-
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
if (nonPastel.length === 0) return;
|
|
925
|
-
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
882
|
//#endregion
|
|
929
883
|
//#region src/hc-pair.ts
|
|
930
884
|
function pairNormal(p) {
|
|
@@ -950,10 +904,14 @@ function levelFraction(level) {
|
|
|
950
904
|
}
|
|
951
905
|
/**
|
|
952
906
|
* Manual-contrast blend fraction (0–1) for a resolved config, or `undefined`
|
|
953
|
-
* in `'auto'` mode
|
|
907
|
+
* in `'auto'` mode, where the normal variants take the authored normal entries
|
|
908
|
+
* as-is.
|
|
954
909
|
*
|
|
955
|
-
* Note `contrastLevel: 0` yields `0`, not `undefined
|
|
956
|
-
*
|
|
910
|
+
* Note `contrastLevel: 0` yields `0`, not `undefined`. The two select different
|
|
911
|
+
* branches, and an authored `0` is a slider position that `.export()` freezes
|
|
912
|
+
* where an absent level is not — but they are not an *output* distinction: at
|
|
913
|
+
* fraction 0 every interpolation returns its normal entry verbatim, so level 0
|
|
914
|
+
* reproduces `'auto'` bit for bit.
|
|
957
915
|
*/
|
|
958
916
|
function contrastFraction(config) {
|
|
959
917
|
const level = config.contrastLevel;
|
|
@@ -961,6 +919,20 @@ function contrastFraction(config) {
|
|
|
961
919
|
return levelFraction(level);
|
|
962
920
|
}
|
|
963
921
|
/**
|
|
922
|
+
* Whether the high-contrast variants are exact duplicates of the normal ones —
|
|
923
|
+
* true only at `contrastLevel: 100`, where the normal passes already resolve at
|
|
924
|
+
* full contrast.
|
|
925
|
+
*
|
|
926
|
+
* The level otherwise says nothing about high contrast: it positions the normal
|
|
927
|
+
* variants, while `lightContrast` / `darkContrast` stay the true high-contrast
|
|
928
|
+
* resolution at every level. This predicate marks the one level where that tier
|
|
929
|
+
* carries no new information, so callers can skip the two high-contrast passes,
|
|
930
|
+
* their drift checks, and the redundant output tier.
|
|
931
|
+
*/
|
|
932
|
+
function hcMirrorsNormal(config) {
|
|
933
|
+
return contrastFraction(config) === 1;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
964
936
|
* A numeric HC pair at a blend fraction (shadow `intensity`, mix `value`).
|
|
965
937
|
*
|
|
966
938
|
* The endpoints return the authored entry *by identity* rather than through
|
|
@@ -1250,10 +1222,267 @@ function mapSaturationDark(s, mode, config) {
|
|
|
1250
1222
|
* endpoints (HC bypasses to full range, a manual `contrastLevel` widens
|
|
1251
1223
|
* toward it — see `activeWindow`).
|
|
1252
1224
|
*/
|
|
1253
|
-
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1254
|
-
if (mode === "static") return [0, 1];
|
|
1255
|
-
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1256
|
-
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1225
|
+
function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
1226
|
+
if (mode === "static") return [0, 1];
|
|
1227
|
+
const win = activeWindow(isHighContrast, isDark ? "dark" : "light", config);
|
|
1228
|
+
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
//#endregion
|
|
1232
|
+
//#region src/color-value.ts
|
|
1233
|
+
/**
|
|
1234
|
+
* Parsing and validation for `GlazeColorValue` — the literal color forms.
|
|
1235
|
+
*
|
|
1236
|
+
* Hex, the CSS color functions Glaze emits, and the four value-object shapes all
|
|
1237
|
+
* land here and come out as OKHSL. Split out of `color-token.ts` so both
|
|
1238
|
+
* consumers can reach it without an import cycle: standalone `glaze.color()`
|
|
1239
|
+
* tokens build on it, and the resolver needs it for a theme color's `from`. It
|
|
1240
|
+
* depends on nothing but the color math, which is what keeps that true.
|
|
1241
|
+
*/
|
|
1242
|
+
/**
|
|
1243
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1244
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1245
|
+
*
|
|
1246
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1247
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1248
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1249
|
+
* are out of scope.
|
|
1250
|
+
*/
|
|
1251
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1252
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1253
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1254
|
+
return parseFloat(raw);
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Split the body of a CSS color function into its components and detect
|
|
1258
|
+
* whether an alpha channel was present.
|
|
1259
|
+
*
|
|
1260
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1261
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1262
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1263
|
+
*/
|
|
1264
|
+
function splitColorBody(body) {
|
|
1265
|
+
const slashIdx = body.indexOf("/");
|
|
1266
|
+
if (slashIdx !== -1) return {
|
|
1267
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1268
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1269
|
+
};
|
|
1270
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1271
|
+
if (components.length === 4) {
|
|
1272
|
+
components.pop();
|
|
1273
|
+
return {
|
|
1274
|
+
components,
|
|
1275
|
+
hadAlpha: true
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
return {
|
|
1279
|
+
components,
|
|
1280
|
+
hadAlpha: false
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function warnDroppedAlpha(input) {
|
|
1284
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1285
|
+
}
|
|
1286
|
+
function parseColorString(input) {
|
|
1287
|
+
if (input.startsWith("#")) {
|
|
1288
|
+
const parsed = parseHexAlpha(input);
|
|
1289
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1290
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1291
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1292
|
+
return {
|
|
1293
|
+
h,
|
|
1294
|
+
s,
|
|
1295
|
+
l
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
const m = input.match(COLOR_FN_RE);
|
|
1299
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1300
|
+
const fn = m[1].toLowerCase();
|
|
1301
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1302
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1303
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1304
|
+
switch (fn) {
|
|
1305
|
+
case "rgb":
|
|
1306
|
+
case "rgba": {
|
|
1307
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1308
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1309
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1310
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1311
|
+
]);
|
|
1312
|
+
return {
|
|
1313
|
+
h,
|
|
1314
|
+
s,
|
|
1315
|
+
l
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
case "hsl":
|
|
1319
|
+
case "hsla": {
|
|
1320
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1321
|
+
return {
|
|
1322
|
+
h: oh,
|
|
1323
|
+
s: os,
|
|
1324
|
+
l: ol
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
case "okhsl": return {
|
|
1328
|
+
h: parseFloat(components[0]),
|
|
1329
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1330
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1331
|
+
};
|
|
1332
|
+
case "okhst": return okhstToOkhsl({
|
|
1333
|
+
h: parseFloat(components[0]),
|
|
1334
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1335
|
+
t: parseNumberOrPercent(components[2], 1)
|
|
1336
|
+
});
|
|
1337
|
+
case "oklch": {
|
|
1338
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1339
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1340
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1341
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1342
|
+
L,
|
|
1343
|
+
C * Math.cos(hRad),
|
|
1344
|
+
C * Math.sin(hRad)
|
|
1345
|
+
]);
|
|
1346
|
+
return {
|
|
1347
|
+
h,
|
|
1348
|
+
s,
|
|
1349
|
+
l
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1357
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1358
|
+
*/
|
|
1359
|
+
function validateOkhslColor(value) {
|
|
1360
|
+
const { h, s, l } = value;
|
|
1361
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1362
|
+
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)?");
|
|
1363
|
+
}
|
|
1364
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1365
|
+
function validateRgbColor(value) {
|
|
1366
|
+
for (const key of [
|
|
1367
|
+
"r",
|
|
1368
|
+
"g",
|
|
1369
|
+
"b"
|
|
1370
|
+
]) {
|
|
1371
|
+
const n = value[key];
|
|
1372
|
+
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}).`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1376
|
+
function validateOklchColor(value) {
|
|
1377
|
+
const { l, c, h } = value;
|
|
1378
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1379
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1380
|
+
}
|
|
1381
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1382
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1383
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1384
|
+
l,
|
|
1385
|
+
c * Math.cos(hRad),
|
|
1386
|
+
c * Math.sin(hRad)
|
|
1387
|
+
]);
|
|
1388
|
+
return {
|
|
1389
|
+
h,
|
|
1390
|
+
s,
|
|
1391
|
+
l: outL
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function isRgbColorObject(value) {
|
|
1395
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1396
|
+
}
|
|
1397
|
+
function isOklchColorObject(value) {
|
|
1398
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1399
|
+
}
|
|
1400
|
+
function isOkhstColorObject(value) {
|
|
1401
|
+
return "t" in value && "h" in value && "s" in value;
|
|
1402
|
+
}
|
|
1403
|
+
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
1404
|
+
function validateOkhstColor(value) {
|
|
1405
|
+
const { h, s, t } = value;
|
|
1406
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
1407
|
+
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)?");
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1411
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1412
|
+
* literal objects) go through one parser.
|
|
1413
|
+
*/
|
|
1414
|
+
function extractOkhslFromValue(value) {
|
|
1415
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1416
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1417
|
+
if (isRgbColorObject(value)) {
|
|
1418
|
+
validateRgbColor(value);
|
|
1419
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1420
|
+
value.r / 255,
|
|
1421
|
+
value.g / 255,
|
|
1422
|
+
value.b / 255
|
|
1423
|
+
]);
|
|
1424
|
+
return {
|
|
1425
|
+
h,
|
|
1426
|
+
s,
|
|
1427
|
+
l
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
if (isOklchColorObject(value)) {
|
|
1431
|
+
validateOklchColor(value);
|
|
1432
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1433
|
+
}
|
|
1434
|
+
if (isOkhstColorObject(value)) {
|
|
1435
|
+
validateOkhstColor(value);
|
|
1436
|
+
return okhstToOkhsl(value);
|
|
1437
|
+
}
|
|
1438
|
+
validateOkhslColor(value);
|
|
1439
|
+
return value;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/format-guard.ts
|
|
1444
|
+
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
1445
|
+
/**
|
|
1446
|
+
* Throw when a non-native Glaze color space is requested for an export that
|
|
1447
|
+
* emits raw CSS or non-Tasty token maps.
|
|
1448
|
+
*/
|
|
1449
|
+
function assertNativeFormat(format, method) {
|
|
1450
|
+
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}().`);
|
|
1451
|
+
}
|
|
1452
|
+
const SCHEME_FIELDS = [
|
|
1453
|
+
{
|
|
1454
|
+
field: "light",
|
|
1455
|
+
modes: () => true
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
field: "dark",
|
|
1459
|
+
modes: (m) => m.dark
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
field: "lightContrast",
|
|
1463
|
+
modes: (m) => m.highContrast
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
field: "darkContrast",
|
|
1467
|
+
modes: (m) => m.dark && m.highContrast
|
|
1468
|
+
}
|
|
1469
|
+
];
|
|
1470
|
+
/**
|
|
1471
|
+
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
1472
|
+
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
1473
|
+
* safe chroma (`computeSafeChromaOKLCH`).
|
|
1474
|
+
*/
|
|
1475
|
+
function assertAllPastel(resolved, modes) {
|
|
1476
|
+
const nonPastel = [];
|
|
1477
|
+
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
1478
|
+
if (!active(modes)) continue;
|
|
1479
|
+
if (color[field].pastel !== true) {
|
|
1480
|
+
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
1481
|
+
break;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (nonPastel.length === 0) return;
|
|
1485
|
+
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
|
|
1257
1486
|
}
|
|
1258
1487
|
|
|
1259
1488
|
//#endregion
|
|
@@ -1894,6 +2123,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1894
2123
|
* its base / bg / fg / target dependencies.
|
|
1895
2124
|
*/
|
|
1896
2125
|
/**
|
|
2126
|
+
* Does this color sit at an absolute tone — either authored, or carried by a
|
|
2127
|
+
* `from` value? That is the test for "root" (placed on its own) as opposed to
|
|
2128
|
+
* "dependent" (placed relative to a base), and it is what lets a bare
|
|
2129
|
+
* `{ from: '#2F5BFF' }` stand as a complete color definition.
|
|
2130
|
+
*/
|
|
2131
|
+
function hasAbsoluteTone(def) {
|
|
2132
|
+
if (def.tone === void 0) return def.from !== void 0;
|
|
2133
|
+
return isAbsoluteTone(def.tone);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
1897
2136
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1898
2137
|
*
|
|
1899
2138
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1932,11 +2171,16 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1932
2171
|
}
|
|
1933
2172
|
const regDef = def;
|
|
1934
2173
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
2174
|
+
if (regDef.from !== void 0) try {
|
|
2175
|
+
extractOkhslFromValue(regDef.from);
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
throw new Error(`glaze: color "${name}" has an invalid "from" value. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2178
|
+
}
|
|
1935
2179
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1936
2180
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1937
2181
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1938
2182
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1939
|
-
if (!
|
|
2183
|
+
if (!hasAbsoluteTone(regDef) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`);
|
|
1940
2184
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1941
2185
|
}
|
|
1942
2186
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2048,12 +2292,14 @@ function warnContrastDrift(name, isDark, isHighContrast, contrast, yColor, yBase
|
|
|
2048
2292
|
* Color resolution engine.
|
|
2049
2293
|
*
|
|
2050
2294
|
* Runs the four-pass solver (light → light-HC → dark → dark-HC) that
|
|
2051
|
-
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
*
|
|
2055
|
-
*
|
|
2056
|
-
*
|
|
2295
|
+
* turns a `ColorMap` into a fully resolved `ResolvedColor` per name. A manual
|
|
2296
|
+
* `contrastLevel` interpolates the authored HC pairs, tone window, and contrast
|
|
2297
|
+
* targets at that level for the two *normal* passes only; the high-contrast
|
|
2298
|
+
* passes bypass the level entirely, so the high-contrast slots are the same
|
|
2299
|
+
* values `'auto'` resolves. At level 100 the normal passes already are the
|
|
2300
|
+
* high-contrast ones, so the solver drops to two passes (light → dark) and
|
|
2301
|
+
* mirrors the slots. Owns the per-scheme resolve helpers for regular, shadow,
|
|
2302
|
+
* and mix color defs.
|
|
2057
2303
|
*
|
|
2058
2304
|
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
2059
2305
|
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
@@ -2193,7 +2439,43 @@ function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config)
|
|
|
2193
2439
|
const shift = extremeLightTone - lightBase.t * 100;
|
|
2194
2440
|
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2195
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2444
|
+
*
|
|
2445
|
+
* Deliberately not memoized. Keying a cache on the def object looks free — a
|
|
2446
|
+
* resolve reads this a handful of times per color — but defs are stored by
|
|
2447
|
+
* reference, so mutating one in place and re-setting it would invalidate the
|
|
2448
|
+
* theme's own cache while a def-keyed cache quietly served the old color. The
|
|
2449
|
+
* parse is a few microseconds against a resolve that is already cached; that is
|
|
2450
|
+
* not a trade worth a staleness bug.
|
|
2451
|
+
*/
|
|
2452
|
+
function fromSeed(def) {
|
|
2453
|
+
if (def.from === void 0) return void 0;
|
|
2454
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2455
|
+
return {
|
|
2456
|
+
hue: h,
|
|
2457
|
+
saturation: s,
|
|
2458
|
+
tone: toTone(l)
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* The config this color resolves against.
|
|
2463
|
+
*
|
|
2464
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2465
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2466
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2467
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2468
|
+
* the color is expected to adapt.
|
|
2469
|
+
*/
|
|
2470
|
+
function configForColor(def, config) {
|
|
2471
|
+
if (def.from === void 0) return config;
|
|
2472
|
+
return {
|
|
2473
|
+
...config,
|
|
2474
|
+
lightTone: false
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2196
2477
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2478
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2197
2479
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2198
2480
|
}
|
|
2199
2481
|
/**
|
|
@@ -2212,16 +2494,20 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2212
2494
|
function resolveChannels(def, ctx, isDark) {
|
|
2213
2495
|
const mode = def.mode ?? "auto";
|
|
2214
2496
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2497
|
+
const seed = fromSeed(def);
|
|
2498
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2499
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2215
2500
|
if (!isDark || mode === "static") return {
|
|
2216
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2217
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2501
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2502
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2218
2503
|
};
|
|
2219
2504
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2220
2505
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2221
|
-
const
|
|
2222
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2506
|
+
const fromDark = fromSaturation !== void 0 && def.darkSaturation === void 0;
|
|
2507
|
+
const raw = fromDark ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2508
|
+
const explicitDark = fromDark ? false : def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2223
2509
|
return {
|
|
2224
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2510
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2225
2511
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2226
2512
|
};
|
|
2227
2513
|
}
|
|
@@ -2232,30 +2518,32 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2232
2518
|
const mode = def.mode ?? "auto";
|
|
2233
2519
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2234
2520
|
const pastel = effectivePastel;
|
|
2521
|
+
const config = configForColor(def, ctx.config);
|
|
2235
2522
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2236
2523
|
const baseTone = baseVariant.t * 100;
|
|
2237
2524
|
let preferredTone;
|
|
2238
2525
|
let isExtreme = false;
|
|
2239
|
-
const
|
|
2526
|
+
const seed = fromSeed(def);
|
|
2527
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2240
2528
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2241
2529
|
else {
|
|
2242
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2530
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2243
2531
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2244
2532
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2245
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2533
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2246
2534
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2247
2535
|
else {
|
|
2248
2536
|
isExtreme = parsed.kind === "extreme";
|
|
2249
|
-
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved,
|
|
2250
|
-
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast,
|
|
2537
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2538
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2251
2539
|
}
|
|
2252
2540
|
}
|
|
2253
2541
|
const rawContrast = def.contrast;
|
|
2254
2542
|
if (rawContrast !== void 0) {
|
|
2255
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2543
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2256
2544
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2257
2545
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2258
|
-
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast,
|
|
2546
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2259
2547
|
let initialDirection;
|
|
2260
2548
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2261
2549
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
@@ -2268,7 +2556,7 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2268
2556
|
flip,
|
|
2269
2557
|
pastel
|
|
2270
2558
|
};
|
|
2271
|
-
const level = contrastFraction(
|
|
2559
|
+
const level = isHighContrast ? void 0 : contrastFraction(config);
|
|
2272
2560
|
let preferInitial = false;
|
|
2273
2561
|
if (level !== void 0 && level > 0 && level < 1) {
|
|
2274
2562
|
initialDirection = findToneForContrast({
|
|
@@ -2294,11 +2582,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2294
2582
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2295
2583
|
const regDef = def;
|
|
2296
2584
|
const mode = regDef.mode ?? "auto";
|
|
2297
|
-
const isRoot =
|
|
2585
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2298
2586
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2299
2587
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2300
2588
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2301
|
-
const
|
|
2589
|
+
const config = configForColor(regDef, ctx.config);
|
|
2590
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2302
2591
|
return {
|
|
2303
2592
|
h: channels.hue,
|
|
2304
2593
|
s: channels.saturation,
|
|
@@ -2456,10 +2745,11 @@ function seedField(order, ctx, field, source) {
|
|
|
2456
2745
|
* resolved with a `base` + `contrast` may land slightly under the contrast
|
|
2457
2746
|
* its tone implies because chromatic luminance drifts from the gray tone.
|
|
2458
2747
|
*
|
|
2459
|
-
* Under a manual `contrastLevel`
|
|
2460
|
-
*
|
|
2461
|
-
*
|
|
2462
|
-
*
|
|
2748
|
+
* Under a manual `contrastLevel` the spec is resolved at the level, so the check
|
|
2749
|
+
* measures each normal variant against the target it was actually solved for. At
|
|
2750
|
+
* level 100 the high-contrast slots are mirrors and are skipped: the normal
|
|
2751
|
+
* schemes already carry that check, and repeating it would emit a byte-identical
|
|
2752
|
+
* advisory under a different scheme label, which dedupes as a separate key.
|
|
2463
2753
|
*/
|
|
2464
2754
|
function verifyContrastDrift(order, defs, result, config) {
|
|
2465
2755
|
const roles = /* @__PURE__ */ new Map();
|
|
@@ -2494,9 +2784,9 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2494
2784
|
field: "darkContrast"
|
|
2495
2785
|
}
|
|
2496
2786
|
];
|
|
2497
|
-
const
|
|
2787
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2498
2788
|
for (const s of schemes) {
|
|
2499
|
-
if (
|
|
2789
|
+
if (mirrored && s.isHighContrast) continue;
|
|
2500
2790
|
const spec = resolveContrastSpec(regDef.contrast, s.isHighContrast, config, polarity);
|
|
2501
2791
|
const cVariant = color[s.field];
|
|
2502
2792
|
const bVariant = base[s.field];
|
|
@@ -2521,10 +2811,10 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2521
2811
|
roles: /* @__PURE__ */ new Map()
|
|
2522
2812
|
};
|
|
2523
2813
|
if (externalBases) for (const [name, color] of externalBases) ctx.resolved.set(name, color);
|
|
2524
|
-
const
|
|
2814
|
+
const mirrored = hcMirrorsNormal(config);
|
|
2525
2815
|
const lightMap = runPass(order, defs, ctx, false, false, "light");
|
|
2526
2816
|
let lightHCMap = lightMap;
|
|
2527
|
-
if (!
|
|
2817
|
+
if (!mirrored) {
|
|
2528
2818
|
seedField(order, ctx, "lightContrast", lightMap);
|
|
2529
2819
|
lightHCMap = runPass(order, defs, ctx, false, true, "lightContrast");
|
|
2530
2820
|
}
|
|
@@ -2532,7 +2822,7 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2532
2822
|
seedField(order, ctx, "darkContrast", lightHCMap);
|
|
2533
2823
|
const darkMap = runPass(order, defs, ctx, true, false, "dark");
|
|
2534
2824
|
let darkHCMap = darkMap;
|
|
2535
|
-
if (!
|
|
2825
|
+
if (!mirrored) {
|
|
2536
2826
|
seedField(order, ctx, "darkContrast", darkMap);
|
|
2537
2827
|
darkHCMap = runPass(order, defs, ctx, true, true, "darkContrast");
|
|
2538
2828
|
}
|
|
@@ -2591,20 +2881,30 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2591
2881
|
value: String(variant.h)
|
|
2592
2882
|
}]
|
|
2593
2883
|
};
|
|
2594
|
-
if (regDef.hue === void 0 && regDef.darkHue === void 0) return {
|
|
2884
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2595
2885
|
hueVar: baseHueVar,
|
|
2596
2886
|
inline: false,
|
|
2597
2887
|
declarations: []
|
|
2598
2888
|
};
|
|
2599
2889
|
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2600
|
-
if (authored === void 0)
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2890
|
+
if (authored === void 0) {
|
|
2891
|
+
if (regDef.from !== void 0) return {
|
|
2892
|
+
hueVar: `var(${prop})`,
|
|
2893
|
+
inline: false,
|
|
2894
|
+
declarations: [{
|
|
2895
|
+
prop,
|
|
2896
|
+
value: String(variant.h)
|
|
2897
|
+
}]
|
|
2898
|
+
};
|
|
2899
|
+
return {
|
|
2900
|
+
hueVar: `var(${prop})`,
|
|
2901
|
+
inline: false,
|
|
2902
|
+
declarations: [{
|
|
2903
|
+
prop,
|
|
2904
|
+
value: baseHueVar
|
|
2905
|
+
}]
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2608
2908
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2609
2909
|
if (parsed.relative) {
|
|
2610
2910
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2756,23 +3056,28 @@ function formatColorValue(v, format, pastel, huePlan) {
|
|
|
2756
3056
|
/**
|
|
2757
3057
|
* Resolve the effective output modes for an export.
|
|
2758
3058
|
*
|
|
2759
|
-
*
|
|
2760
|
-
*
|
|
2761
|
-
*
|
|
2762
|
-
*
|
|
2763
|
-
*
|
|
2764
|
-
*
|
|
3059
|
+
* `modes.highContrast` and `contrastLevel` are independent: the level positions
|
|
3060
|
+
* the normal variants, the high-contrast tier stays the true high-contrast
|
|
3061
|
+
* resolution, and the two compose — a slider raises the baseline while a
|
|
3062
|
+
* `prefers-contrast: more` block still escalates on top of it.
|
|
3063
|
+
*
|
|
3064
|
+
* The one exception is a **global** level of 100, where the normal variants
|
|
3065
|
+
* already *are* the high-contrast ones. The tier would be an exact duplicate, so
|
|
3066
|
+
* it is dropped and a single light/dark set is emitted — including against an
|
|
3067
|
+
* explicit `highContrast: true` override, which has nothing different left to
|
|
3068
|
+
* ask for.
|
|
2765
3069
|
*
|
|
2766
3070
|
* A level set on a single theme or token does not change which modes are
|
|
2767
|
-
* emitted:
|
|
2768
|
-
*
|
|
2769
|
-
* the
|
|
3071
|
+
* emitted: `modes` is global-only, and a palette must not have one sibling
|
|
3072
|
+
* collapse the shared token structure. A per-instance level of 100 therefore
|
|
3073
|
+
* still reports into the tier, with values equal to its own normal ones.
|
|
2770
3074
|
*/
|
|
2771
3075
|
function resolveModes(override) {
|
|
2772
3076
|
const cfg = getConfig();
|
|
3077
|
+
const highContrast = override?.highContrast ?? cfg.modes.highContrast;
|
|
2773
3078
|
return {
|
|
2774
3079
|
dark: override?.dark ?? cfg.modes.dark,
|
|
2775
|
-
highContrast:
|
|
3080
|
+
highContrast: highContrast && !hcMirrorsNormal(cfg)
|
|
2776
3081
|
};
|
|
2777
3082
|
}
|
|
2778
3083
|
function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel = false, channelCtx) {
|
|
@@ -3086,173 +3391,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3086
3391
|
};
|
|
3087
3392
|
}
|
|
3088
3393
|
/**
|
|
3089
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
3090
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
3091
|
-
*
|
|
3092
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
3093
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
3094
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
3095
|
-
* are out of scope.
|
|
3096
|
-
*/
|
|
3097
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
3098
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
3099
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
3100
|
-
return parseFloat(raw);
|
|
3101
|
-
}
|
|
3102
|
-
/**
|
|
3103
|
-
* Split the body of a CSS color function into its components and detect
|
|
3104
|
-
* whether an alpha channel was present.
|
|
3105
|
-
*
|
|
3106
|
-
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
3107
|
-
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
3108
|
-
* by the caller — standalone Glaze colors have no opacity field.
|
|
3109
|
-
*/
|
|
3110
|
-
function splitColorBody(body) {
|
|
3111
|
-
const slashIdx = body.indexOf("/");
|
|
3112
|
-
if (slashIdx !== -1) return {
|
|
3113
|
-
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
3114
|
-
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
3115
|
-
};
|
|
3116
|
-
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
3117
|
-
if (components.length === 4) {
|
|
3118
|
-
components.pop();
|
|
3119
|
-
return {
|
|
3120
|
-
components,
|
|
3121
|
-
hadAlpha: true
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
return {
|
|
3125
|
-
components,
|
|
3126
|
-
hadAlpha: false
|
|
3127
|
-
};
|
|
3128
|
-
}
|
|
3129
|
-
function warnDroppedAlpha(input) {
|
|
3130
|
-
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
3131
|
-
}
|
|
3132
|
-
function parseColorString(input) {
|
|
3133
|
-
if (input.startsWith("#")) {
|
|
3134
|
-
const parsed = parseHexAlpha(input);
|
|
3135
|
-
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
3136
|
-
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
3137
|
-
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
3138
|
-
return {
|
|
3139
|
-
h,
|
|
3140
|
-
s,
|
|
3141
|
-
l
|
|
3142
|
-
};
|
|
3143
|
-
}
|
|
3144
|
-
const m = input.match(COLOR_FN_RE);
|
|
3145
|
-
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
3146
|
-
const fn = m[1].toLowerCase();
|
|
3147
|
-
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
3148
|
-
if (hadAlpha) warnDroppedAlpha(input);
|
|
3149
|
-
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
3150
|
-
switch (fn) {
|
|
3151
|
-
case "rgb":
|
|
3152
|
-
case "rgba": {
|
|
3153
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3154
|
-
parseNumberOrPercent(components[0], 255) / 255,
|
|
3155
|
-
parseNumberOrPercent(components[1], 255) / 255,
|
|
3156
|
-
parseNumberOrPercent(components[2], 255) / 255
|
|
3157
|
-
]);
|
|
3158
|
-
return {
|
|
3159
|
-
h,
|
|
3160
|
-
s,
|
|
3161
|
-
l
|
|
3162
|
-
};
|
|
3163
|
-
}
|
|
3164
|
-
case "hsl":
|
|
3165
|
-
case "hsla": {
|
|
3166
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
3167
|
-
return {
|
|
3168
|
-
h: oh,
|
|
3169
|
-
s: os,
|
|
3170
|
-
l: ol
|
|
3171
|
-
};
|
|
3172
|
-
}
|
|
3173
|
-
case "okhsl": return {
|
|
3174
|
-
h: parseFloat(components[0]),
|
|
3175
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3176
|
-
l: parseNumberOrPercent(components[2], 1)
|
|
3177
|
-
};
|
|
3178
|
-
case "okhst": return okhstToOkhsl({
|
|
3179
|
-
h: parseFloat(components[0]),
|
|
3180
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3181
|
-
t: parseNumberOrPercent(components[2], 1)
|
|
3182
|
-
});
|
|
3183
|
-
case "oklch": {
|
|
3184
|
-
const L = parseNumberOrPercent(components[0], 1);
|
|
3185
|
-
const C = parseNumberOrPercent(components[1], .4);
|
|
3186
|
-
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
3187
|
-
const [h, s, l] = oklabToOkhsl([
|
|
3188
|
-
L,
|
|
3189
|
-
C * Math.cos(hRad),
|
|
3190
|
-
C * Math.sin(hRad)
|
|
3191
|
-
]);
|
|
3192
|
-
return {
|
|
3193
|
-
h,
|
|
3194
|
-
s,
|
|
3195
|
-
l
|
|
3196
|
-
};
|
|
3197
|
-
}
|
|
3198
|
-
}
|
|
3199
|
-
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
3200
|
-
}
|
|
3201
|
-
/**
|
|
3202
|
-
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
3203
|
-
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
3204
|
-
*/
|
|
3205
|
-
function validateOkhslColor(value) {
|
|
3206
|
-
const { h, s, l } = value;
|
|
3207
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3208
|
-
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
3209
|
-
}
|
|
3210
|
-
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
3211
|
-
function validateRgbColor(value) {
|
|
3212
|
-
for (const key of [
|
|
3213
|
-
"r",
|
|
3214
|
-
"g",
|
|
3215
|
-
"b"
|
|
3216
|
-
]) {
|
|
3217
|
-
const n = value[key];
|
|
3218
|
-
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
3222
|
-
function validateOklchColor(value) {
|
|
3223
|
-
const { l, c, h } = value;
|
|
3224
|
-
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3225
|
-
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
3226
|
-
}
|
|
3227
|
-
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
3228
|
-
const hRad = hDeg * Math.PI / 180;
|
|
3229
|
-
const [h, s, outL] = oklabToOkhsl([
|
|
3230
|
-
l,
|
|
3231
|
-
c * Math.cos(hRad),
|
|
3232
|
-
c * Math.sin(hRad)
|
|
3233
|
-
]);
|
|
3234
|
-
return {
|
|
3235
|
-
h,
|
|
3236
|
-
s,
|
|
3237
|
-
l: outL
|
|
3238
|
-
};
|
|
3239
|
-
}
|
|
3240
|
-
function isRgbColorObject(value) {
|
|
3241
|
-
return "r" in value && "g" in value && "b" in value;
|
|
3242
|
-
}
|
|
3243
|
-
function isOklchColorObject(value) {
|
|
3244
|
-
return "c" in value && "l" in value && "h" in value;
|
|
3245
|
-
}
|
|
3246
|
-
function isOkhstColorObject(value) {
|
|
3247
|
-
return "t" in value && "h" in value && "s" in value;
|
|
3248
|
-
}
|
|
3249
|
-
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
3250
|
-
function validateOkhstColor(value) {
|
|
3251
|
-
const { h, s, t } = value;
|
|
3252
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3253
|
-
if (s > 1.5 || t > 1.5) throw new Error("glaze.color: OkhstColor s/t must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
3254
|
-
}
|
|
3255
|
-
/**
|
|
3256
3394
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3257
3395
|
* Must be a finite number in `0..=1`.
|
|
3258
3396
|
*/
|
|
@@ -3302,38 +3440,6 @@ function validateStandaloneName(name) {
|
|
|
3302
3440
|
}
|
|
3303
3441
|
}
|
|
3304
3442
|
/**
|
|
3305
|
-
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
3306
|
-
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
3307
|
-
* literal objects) go through one parser.
|
|
3308
|
-
*/
|
|
3309
|
-
function extractOkhslFromValue(value) {
|
|
3310
|
-
if (typeof value === "string") return parseColorString(value);
|
|
3311
|
-
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
3312
|
-
if (isRgbColorObject(value)) {
|
|
3313
|
-
validateRgbColor(value);
|
|
3314
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3315
|
-
value.r / 255,
|
|
3316
|
-
value.g / 255,
|
|
3317
|
-
value.b / 255
|
|
3318
|
-
]);
|
|
3319
|
-
return {
|
|
3320
|
-
h,
|
|
3321
|
-
s,
|
|
3322
|
-
l
|
|
3323
|
-
};
|
|
3324
|
-
}
|
|
3325
|
-
if (isOklchColorObject(value)) {
|
|
3326
|
-
validateOklchColor(value);
|
|
3327
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3328
|
-
}
|
|
3329
|
-
if (isOkhstColorObject(value)) {
|
|
3330
|
-
validateOkhstColor(value);
|
|
3331
|
-
return okhstToOkhsl(value);
|
|
3332
|
-
}
|
|
3333
|
-
validateOkhslColor(value);
|
|
3334
|
-
return value;
|
|
3335
|
-
}
|
|
3336
|
-
/**
|
|
3337
3443
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3338
3444
|
*
|
|
3339
3445
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|