@tenphi/glaze 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +382 -290
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.mts +31 -1
- package/dist/index.mjs +382 -290
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +59 -3
- 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) {
|
|
@@ -1258,6 +1212,263 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
1258
1212
|
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1259
1213
|
}
|
|
1260
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
|
+
|
|
1261
1472
|
//#endregion
|
|
1262
1473
|
//#region src/contrast-solver.ts
|
|
1263
1474
|
/**
|
|
@@ -1896,6 +2107,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1896
2107
|
* its base / bg / fg / target dependencies.
|
|
1897
2108
|
*/
|
|
1898
2109
|
/**
|
|
2110
|
+
* Does this color sit at an absolute tone — either authored, or carried by a
|
|
2111
|
+
* `from` value? That is the test for "root" (placed on its own) as opposed to
|
|
2112
|
+
* "dependent" (placed relative to a base), and it is what lets a bare
|
|
2113
|
+
* `{ from: '#2F5BFF' }` stand as a complete color definition.
|
|
2114
|
+
*/
|
|
2115
|
+
function hasAbsoluteTone(def) {
|
|
2116
|
+
if (def.tone === void 0) return def.from !== void 0;
|
|
2117
|
+
return isAbsoluteTone(def.tone);
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
1899
2120
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1900
2121
|
*
|
|
1901
2122
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1934,11 +2155,16 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1934
2155
|
}
|
|
1935
2156
|
const regDef = def;
|
|
1936
2157
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
2158
|
+
if (regDef.from !== void 0) try {
|
|
2159
|
+
extractOkhslFromValue(regDef.from);
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
throw new Error(`glaze: color "${name}" has an invalid "from" value. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2162
|
+
}
|
|
1937
2163
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1938
2164
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1939
2165
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1940
2166
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1941
|
-
if (!
|
|
2167
|
+
if (!hasAbsoluteTone(regDef) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`);
|
|
1942
2168
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1943
2169
|
}
|
|
1944
2170
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2188,11 +2414,50 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
2188
2414
|
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2189
2415
|
* intended, since the author asked for the extreme.
|
|
2190
2416
|
*/
|
|
2191
|
-
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved,
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2417
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config) {
|
|
2418
|
+
const extremeLightTone = mapToneForScheme(authorTone, mode, false, isHighContrast, config);
|
|
2419
|
+
const lightBase = getSchemeVariant(baseResolved, false, isHighContrast);
|
|
2420
|
+
const darkBase = getSchemeVariant(baseResolved, true, isHighContrast);
|
|
2421
|
+
const shift = extremeLightTone - lightBase.t * 100;
|
|
2422
|
+
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2426
|
+
*
|
|
2427
|
+
* Deliberately not memoized. Keying a cache on the def object looks free — a
|
|
2428
|
+
* resolve reads this a handful of times per color — but defs are stored by
|
|
2429
|
+
* reference, so mutating one in place and re-setting it would invalidate the
|
|
2430
|
+
* theme's own cache while a def-keyed cache quietly served the old color. The
|
|
2431
|
+
* parse is a few microseconds against a resolve that is already cached; that is
|
|
2432
|
+
* not a trade worth a staleness bug.
|
|
2433
|
+
*/
|
|
2434
|
+
function fromSeed(def) {
|
|
2435
|
+
if (def.from === void 0) return void 0;
|
|
2436
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2437
|
+
return {
|
|
2438
|
+
hue: h,
|
|
2439
|
+
saturation: s,
|
|
2440
|
+
tone: toTone(l)
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* The config this color resolves against.
|
|
2445
|
+
*
|
|
2446
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2447
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2448
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2449
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2450
|
+
* the color is expected to adapt.
|
|
2451
|
+
*/
|
|
2452
|
+
function configForColor(def, config) {
|
|
2453
|
+
if (def.from === void 0) return config;
|
|
2454
|
+
return {
|
|
2455
|
+
...config,
|
|
2456
|
+
lightTone: false
|
|
2457
|
+
};
|
|
2194
2458
|
}
|
|
2195
2459
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2460
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2196
2461
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2197
2462
|
}
|
|
2198
2463
|
/**
|
|
@@ -2211,16 +2476,20 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2211
2476
|
function resolveChannels(def, ctx, isDark) {
|
|
2212
2477
|
const mode = def.mode ?? "auto";
|
|
2213
2478
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2479
|
+
const seed = fromSeed(def);
|
|
2480
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2481
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2214
2482
|
if (!isDark || mode === "static") return {
|
|
2215
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2216
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2483
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2484
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2217
2485
|
};
|
|
2218
2486
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2219
2487
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2220
|
-
const
|
|
2221
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2488
|
+
const fromDark = fromSaturation !== void 0 && def.darkSaturation === void 0;
|
|
2489
|
+
const raw = fromDark ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2490
|
+
const explicitDark = fromDark ? false : def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2222
2491
|
return {
|
|
2223
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2492
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2224
2493
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2225
2494
|
};
|
|
2226
2495
|
}
|
|
@@ -2231,40 +2500,39 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2231
2500
|
const mode = def.mode ?? "auto";
|
|
2232
2501
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2233
2502
|
const pastel = effectivePastel;
|
|
2503
|
+
const config = configForColor(def, ctx.config);
|
|
2234
2504
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2235
2505
|
const baseTone = baseVariant.t * 100;
|
|
2236
2506
|
let preferredTone;
|
|
2237
2507
|
let isExtreme = false;
|
|
2238
|
-
const
|
|
2508
|
+
const seed = fromSeed(def);
|
|
2509
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2239
2510
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2240
2511
|
else {
|
|
2241
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2512
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2242
2513
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2243
2514
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2244
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2515
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2245
2516
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2246
|
-
else
|
|
2247
|
-
isExtreme = true;
|
|
2248
|
-
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2249
|
-
} else {
|
|
2517
|
+
else {
|
|
2250
2518
|
isExtreme = parsed.kind === "extreme";
|
|
2251
|
-
preferredTone =
|
|
2519
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2520
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2252
2521
|
}
|
|
2253
2522
|
}
|
|
2254
2523
|
const rawContrast = def.contrast;
|
|
2255
2524
|
if (rawContrast !== void 0) {
|
|
2256
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2525
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2257
2526
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2258
2527
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2259
|
-
const
|
|
2528
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2260
2529
|
let initialDirection;
|
|
2261
2530
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2262
2531
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2263
|
-
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2264
2532
|
const solve = {
|
|
2265
2533
|
hue: channels.hue,
|
|
2266
2534
|
saturation: channels.saturation,
|
|
2267
|
-
preferredTone:
|
|
2535
|
+
preferredTone: clamp(preferredTone / 100, preferredRange[0], preferredRange[1]),
|
|
2268
2536
|
baseLinearRgb,
|
|
2269
2537
|
toneRange: [0, 1],
|
|
2270
2538
|
flip,
|
|
@@ -2296,11 +2564,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2296
2564
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2297
2565
|
const regDef = def;
|
|
2298
2566
|
const mode = regDef.mode ?? "auto";
|
|
2299
|
-
const isRoot =
|
|
2567
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2300
2568
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2301
2569
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2302
2570
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2303
|
-
const
|
|
2571
|
+
const config = configForColor(regDef, ctx.config);
|
|
2572
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2304
2573
|
return {
|
|
2305
2574
|
h: channels.hue,
|
|
2306
2575
|
s: channels.saturation,
|
|
@@ -2561,7 +2830,9 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2561
2830
|
*
|
|
2562
2831
|
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2563
2832
|
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2564
|
-
* set is emitted only when it actually differs — see `
|
|
2833
|
+
* set is emitted only when it actually differs — see `buildHueDeclarations`.
|
|
2834
|
+
* A color that authors a hue in *either* scheme gets its own var in both, so
|
|
2835
|
+
* the shared `var()` reference stays valid.
|
|
2565
2836
|
*/
|
|
2566
2837
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2567
2838
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2583,23 +2854,38 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2583
2854
|
const regDef = def;
|
|
2584
2855
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2585
2856
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2586
|
-
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2587
|
-
|
|
2857
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) return {
|
|
2858
|
+
hueVar: `var(${prop})`,
|
|
2859
|
+
inline: false,
|
|
2860
|
+
declarations: [{
|
|
2861
|
+
prop,
|
|
2862
|
+
value: String(variant.h)
|
|
2863
|
+
}]
|
|
2864
|
+
};
|
|
2865
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2866
|
+
hueVar: baseHueVar,
|
|
2867
|
+
inline: false,
|
|
2868
|
+
declarations: []
|
|
2869
|
+
};
|
|
2870
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2871
|
+
if (authored === void 0) {
|
|
2872
|
+
if (regDef.from !== void 0) return {
|
|
2873
|
+
hueVar: `var(${prop})`,
|
|
2874
|
+
inline: false,
|
|
2875
|
+
declarations: [{
|
|
2876
|
+
prop,
|
|
2877
|
+
value: String(variant.h)
|
|
2878
|
+
}]
|
|
2879
|
+
};
|
|
2588
2880
|
return {
|
|
2589
2881
|
hueVar: `var(${prop})`,
|
|
2590
2882
|
inline: false,
|
|
2591
2883
|
declarations: [{
|
|
2592
2884
|
prop,
|
|
2593
|
-
value:
|
|
2885
|
+
value: baseHueVar
|
|
2594
2886
|
}]
|
|
2595
2887
|
};
|
|
2596
2888
|
}
|
|
2597
|
-
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2598
|
-
if (authored === void 0) return {
|
|
2599
|
-
hueVar: baseHueVar,
|
|
2600
|
-
inline: false,
|
|
2601
|
-
declarations: []
|
|
2602
|
-
};
|
|
2603
2889
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2604
2890
|
if (parsed.relative) {
|
|
2605
2891
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2646,7 +2932,7 @@ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
|
2646
2932
|
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2647
2933
|
}
|
|
2648
2934
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2649
|
-
function collectHueDeclarations(resolved, ctx, scheme
|
|
2935
|
+
function collectHueDeclarations(resolved, ctx, scheme) {
|
|
2650
2936
|
if (ctx.emitDeclarations === false) return [];
|
|
2651
2937
|
const seen = /* @__PURE__ */ new Set();
|
|
2652
2938
|
const out = [];
|
|
@@ -2667,21 +2953,24 @@ function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
|
2667
2953
|
return out;
|
|
2668
2954
|
}
|
|
2669
2955
|
/**
|
|
2670
|
-
*
|
|
2671
|
-
* light
|
|
2956
|
+
* Hue declarations for both schemes. `dark` is empty when it resolves to the
|
|
2957
|
+
* same hues as light — the common case, with no `darkHue` anywhere.
|
|
2672
2958
|
*
|
|
2673
|
-
* When they differ, the **whole** set is returned rather than only the
|
|
2674
|
-
* whose text changed. A relative declaration reads
|
|
2959
|
+
* When they differ, the **whole** dark set is returned rather than only the
|
|
2960
|
+
* entries whose text changed. A relative declaration reads
|
|
2675
2961
|
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2676
2962
|
* moves with the seed, and custom properties substitute `var()` at
|
|
2677
2963
|
* computed-value time on the declaring element — so re-declaring
|
|
2678
2964
|
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2679
2965
|
* the light-substituted value.
|
|
2680
2966
|
*/
|
|
2681
|
-
function
|
|
2682
|
-
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2967
|
+
function buildHueDeclarations(resolved, ctx) {
|
|
2683
2968
|
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2684
|
-
|
|
2969
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2970
|
+
return {
|
|
2971
|
+
light,
|
|
2972
|
+
dark: dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark
|
|
2973
|
+
};
|
|
2685
2974
|
}
|
|
2686
2975
|
function buildHuePlans(resolved, ctx) {
|
|
2687
2976
|
const plans = /* @__PURE__ */ new Map();
|
|
@@ -2771,8 +3060,9 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2771
3060
|
const tokens = {};
|
|
2772
3061
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2773
3062
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2774
|
-
|
|
2775
|
-
|
|
3063
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3064
|
+
for (const decl of hueDecls.light) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
3065
|
+
if (modes.dark) for (const decl of hueDecls.dark) {
|
|
2776
3066
|
const key = `$${decl.prop.slice(2)}`;
|
|
2777
3067
|
tokens[key] = {
|
|
2778
3068
|
...tokens[key],
|
|
@@ -2829,8 +3119,9 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2829
3119
|
};
|
|
2830
3120
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2831
3121
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2832
|
-
|
|
2833
|
-
for (const decl of
|
|
3122
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3123
|
+
for (const decl of hueDecls.light) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
3124
|
+
for (const decl of hueDecls.dark) {
|
|
2834
3125
|
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2835
3126
|
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2836
3127
|
}
|
|
@@ -3076,173 +3367,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3076
3367
|
};
|
|
3077
3368
|
}
|
|
3078
3369
|
/**
|
|
3079
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
3080
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
3081
|
-
*
|
|
3082
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
3083
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
3084
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
3085
|
-
* are out of scope.
|
|
3086
|
-
*/
|
|
3087
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
3088
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
3089
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
3090
|
-
return parseFloat(raw);
|
|
3091
|
-
}
|
|
3092
|
-
/**
|
|
3093
|
-
* Split the body of a CSS color function into its components and detect
|
|
3094
|
-
* whether an alpha channel was present.
|
|
3095
|
-
*
|
|
3096
|
-
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
3097
|
-
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
3098
|
-
* by the caller — standalone Glaze colors have no opacity field.
|
|
3099
|
-
*/
|
|
3100
|
-
function splitColorBody(body) {
|
|
3101
|
-
const slashIdx = body.indexOf("/");
|
|
3102
|
-
if (slashIdx !== -1) return {
|
|
3103
|
-
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
3104
|
-
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
3105
|
-
};
|
|
3106
|
-
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
3107
|
-
if (components.length === 4) {
|
|
3108
|
-
components.pop();
|
|
3109
|
-
return {
|
|
3110
|
-
components,
|
|
3111
|
-
hadAlpha: true
|
|
3112
|
-
};
|
|
3113
|
-
}
|
|
3114
|
-
return {
|
|
3115
|
-
components,
|
|
3116
|
-
hadAlpha: false
|
|
3117
|
-
};
|
|
3118
|
-
}
|
|
3119
|
-
function warnDroppedAlpha(input) {
|
|
3120
|
-
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
3121
|
-
}
|
|
3122
|
-
function parseColorString(input) {
|
|
3123
|
-
if (input.startsWith("#")) {
|
|
3124
|
-
const parsed = parseHexAlpha(input);
|
|
3125
|
-
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
3126
|
-
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
3127
|
-
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
3128
|
-
return {
|
|
3129
|
-
h,
|
|
3130
|
-
s,
|
|
3131
|
-
l
|
|
3132
|
-
};
|
|
3133
|
-
}
|
|
3134
|
-
const m = input.match(COLOR_FN_RE);
|
|
3135
|
-
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
3136
|
-
const fn = m[1].toLowerCase();
|
|
3137
|
-
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
3138
|
-
if (hadAlpha) warnDroppedAlpha(input);
|
|
3139
|
-
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
3140
|
-
switch (fn) {
|
|
3141
|
-
case "rgb":
|
|
3142
|
-
case "rgba": {
|
|
3143
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3144
|
-
parseNumberOrPercent(components[0], 255) / 255,
|
|
3145
|
-
parseNumberOrPercent(components[1], 255) / 255,
|
|
3146
|
-
parseNumberOrPercent(components[2], 255) / 255
|
|
3147
|
-
]);
|
|
3148
|
-
return {
|
|
3149
|
-
h,
|
|
3150
|
-
s,
|
|
3151
|
-
l
|
|
3152
|
-
};
|
|
3153
|
-
}
|
|
3154
|
-
case "hsl":
|
|
3155
|
-
case "hsla": {
|
|
3156
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
3157
|
-
return {
|
|
3158
|
-
h: oh,
|
|
3159
|
-
s: os,
|
|
3160
|
-
l: ol
|
|
3161
|
-
};
|
|
3162
|
-
}
|
|
3163
|
-
case "okhsl": return {
|
|
3164
|
-
h: parseFloat(components[0]),
|
|
3165
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3166
|
-
l: parseNumberOrPercent(components[2], 1)
|
|
3167
|
-
};
|
|
3168
|
-
case "okhst": return okhstToOkhsl({
|
|
3169
|
-
h: parseFloat(components[0]),
|
|
3170
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3171
|
-
t: parseNumberOrPercent(components[2], 1)
|
|
3172
|
-
});
|
|
3173
|
-
case "oklch": {
|
|
3174
|
-
const L = parseNumberOrPercent(components[0], 1);
|
|
3175
|
-
const C = parseNumberOrPercent(components[1], .4);
|
|
3176
|
-
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
3177
|
-
const [h, s, l] = oklabToOkhsl([
|
|
3178
|
-
L,
|
|
3179
|
-
C * Math.cos(hRad),
|
|
3180
|
-
C * Math.sin(hRad)
|
|
3181
|
-
]);
|
|
3182
|
-
return {
|
|
3183
|
-
h,
|
|
3184
|
-
s,
|
|
3185
|
-
l
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
3190
|
-
}
|
|
3191
|
-
/**
|
|
3192
|
-
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
3193
|
-
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
3194
|
-
*/
|
|
3195
|
-
function validateOkhslColor(value) {
|
|
3196
|
-
const { h, s, l } = value;
|
|
3197
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3198
|
-
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)?");
|
|
3199
|
-
}
|
|
3200
|
-
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
3201
|
-
function validateRgbColor(value) {
|
|
3202
|
-
for (const key of [
|
|
3203
|
-
"r",
|
|
3204
|
-
"g",
|
|
3205
|
-
"b"
|
|
3206
|
-
]) {
|
|
3207
|
-
const n = value[key];
|
|
3208
|
-
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}).`);
|
|
3209
|
-
}
|
|
3210
|
-
}
|
|
3211
|
-
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
3212
|
-
function validateOklchColor(value) {
|
|
3213
|
-
const { l, c, h } = value;
|
|
3214
|
-
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3215
|
-
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
3216
|
-
}
|
|
3217
|
-
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
3218
|
-
const hRad = hDeg * Math.PI / 180;
|
|
3219
|
-
const [h, s, outL] = oklabToOkhsl([
|
|
3220
|
-
l,
|
|
3221
|
-
c * Math.cos(hRad),
|
|
3222
|
-
c * Math.sin(hRad)
|
|
3223
|
-
]);
|
|
3224
|
-
return {
|
|
3225
|
-
h,
|
|
3226
|
-
s,
|
|
3227
|
-
l: outL
|
|
3228
|
-
};
|
|
3229
|
-
}
|
|
3230
|
-
function isRgbColorObject(value) {
|
|
3231
|
-
return "r" in value && "g" in value && "b" in value;
|
|
3232
|
-
}
|
|
3233
|
-
function isOklchColorObject(value) {
|
|
3234
|
-
return "c" in value && "l" in value && "h" in value;
|
|
3235
|
-
}
|
|
3236
|
-
function isOkhstColorObject(value) {
|
|
3237
|
-
return "t" in value && "h" in value && "s" in value;
|
|
3238
|
-
}
|
|
3239
|
-
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
3240
|
-
function validateOkhstColor(value) {
|
|
3241
|
-
const { h, s, t } = value;
|
|
3242
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3243
|
-
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)?");
|
|
3244
|
-
}
|
|
3245
|
-
/**
|
|
3246
3370
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3247
3371
|
* Must be a finite number in `0..=1`.
|
|
3248
3372
|
*/
|
|
@@ -3292,38 +3416,6 @@ function validateStandaloneName(name) {
|
|
|
3292
3416
|
}
|
|
3293
3417
|
}
|
|
3294
3418
|
/**
|
|
3295
|
-
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
3296
|
-
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
3297
|
-
* literal objects) go through one parser.
|
|
3298
|
-
*/
|
|
3299
|
-
function extractOkhslFromValue(value) {
|
|
3300
|
-
if (typeof value === "string") return parseColorString(value);
|
|
3301
|
-
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
3302
|
-
if (isRgbColorObject(value)) {
|
|
3303
|
-
validateRgbColor(value);
|
|
3304
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3305
|
-
value.r / 255,
|
|
3306
|
-
value.g / 255,
|
|
3307
|
-
value.b / 255
|
|
3308
|
-
]);
|
|
3309
|
-
return {
|
|
3310
|
-
h,
|
|
3311
|
-
s,
|
|
3312
|
-
l
|
|
3313
|
-
};
|
|
3314
|
-
}
|
|
3315
|
-
if (isOklchColorObject(value)) {
|
|
3316
|
-
validateOklchColor(value);
|
|
3317
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3318
|
-
}
|
|
3319
|
-
if (isOkhstColorObject(value)) {
|
|
3320
|
-
validateOkhstColor(value);
|
|
3321
|
-
return okhstToOkhsl(value);
|
|
3322
|
-
}
|
|
3323
|
-
validateOkhslColor(value);
|
|
3324
|
-
return value;
|
|
3325
|
-
}
|
|
3326
|
-
/**
|
|
3327
3419
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3328
3420
|
*
|
|
3329
3421
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|