@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.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) {
|
|
@@ -1256,6 +1210,263 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
1256
1210
|
return [clamp(toTone(win.lo / 100) / 100, 0, 1), clamp(toTone(win.hi / 100) / 100, 0, 1)];
|
|
1257
1211
|
}
|
|
1258
1212
|
|
|
1213
|
+
//#endregion
|
|
1214
|
+
//#region src/color-value.ts
|
|
1215
|
+
/**
|
|
1216
|
+
* Parsing and validation for `GlazeColorValue` — the literal color forms.
|
|
1217
|
+
*
|
|
1218
|
+
* Hex, the CSS color functions Glaze emits, and the four value-object shapes all
|
|
1219
|
+
* land here and come out as OKHSL. Split out of `color-token.ts` so both
|
|
1220
|
+
* consumers can reach it without an import cycle: standalone `glaze.color()`
|
|
1221
|
+
* tokens build on it, and the resolver needs it for a theme color's `from`. It
|
|
1222
|
+
* depends on nothing but the color math, which is what keeps that true.
|
|
1223
|
+
*/
|
|
1224
|
+
/**
|
|
1225
|
+
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
1226
|
+
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
1227
|
+
*
|
|
1228
|
+
* Only bare numeric components are supported. Named colors (`red`),
|
|
1229
|
+
* relative-color syntax (`from <color> ...`), and angle units other
|
|
1230
|
+
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
1231
|
+
* are out of scope.
|
|
1232
|
+
*/
|
|
1233
|
+
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
1234
|
+
function parseNumberOrPercent(raw, percentScale) {
|
|
1235
|
+
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
1236
|
+
return parseFloat(raw);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Split the body of a CSS color function into its components and detect
|
|
1240
|
+
* whether an alpha channel was present.
|
|
1241
|
+
*
|
|
1242
|
+
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
1243
|
+
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
1244
|
+
* by the caller — standalone Glaze colors have no opacity field.
|
|
1245
|
+
*/
|
|
1246
|
+
function splitColorBody(body) {
|
|
1247
|
+
const slashIdx = body.indexOf("/");
|
|
1248
|
+
if (slashIdx !== -1) return {
|
|
1249
|
+
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
1250
|
+
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
1251
|
+
};
|
|
1252
|
+
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
1253
|
+
if (components.length === 4) {
|
|
1254
|
+
components.pop();
|
|
1255
|
+
return {
|
|
1256
|
+
components,
|
|
1257
|
+
hadAlpha: true
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
components,
|
|
1262
|
+
hadAlpha: false
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function warnDroppedAlpha(input) {
|
|
1266
|
+
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
1267
|
+
}
|
|
1268
|
+
function parseColorString(input) {
|
|
1269
|
+
if (input.startsWith("#")) {
|
|
1270
|
+
const parsed = parseHexAlpha(input);
|
|
1271
|
+
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
1272
|
+
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
1273
|
+
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
1274
|
+
return {
|
|
1275
|
+
h,
|
|
1276
|
+
s,
|
|
1277
|
+
l
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const m = input.match(COLOR_FN_RE);
|
|
1281
|
+
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
1282
|
+
const fn = m[1].toLowerCase();
|
|
1283
|
+
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
1284
|
+
if (hadAlpha) warnDroppedAlpha(input);
|
|
1285
|
+
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
1286
|
+
switch (fn) {
|
|
1287
|
+
case "rgb":
|
|
1288
|
+
case "rgba": {
|
|
1289
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1290
|
+
parseNumberOrPercent(components[0], 255) / 255,
|
|
1291
|
+
parseNumberOrPercent(components[1], 255) / 255,
|
|
1292
|
+
parseNumberOrPercent(components[2], 255) / 255
|
|
1293
|
+
]);
|
|
1294
|
+
return {
|
|
1295
|
+
h,
|
|
1296
|
+
s,
|
|
1297
|
+
l
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
case "hsl":
|
|
1301
|
+
case "hsla": {
|
|
1302
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
1303
|
+
return {
|
|
1304
|
+
h: oh,
|
|
1305
|
+
s: os,
|
|
1306
|
+
l: ol
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
case "okhsl": return {
|
|
1310
|
+
h: parseFloat(components[0]),
|
|
1311
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1312
|
+
l: parseNumberOrPercent(components[2], 1)
|
|
1313
|
+
};
|
|
1314
|
+
case "okhst": return okhstToOkhsl({
|
|
1315
|
+
h: parseFloat(components[0]),
|
|
1316
|
+
s: parseNumberOrPercent(components[1], 1),
|
|
1317
|
+
t: parseNumberOrPercent(components[2], 1)
|
|
1318
|
+
});
|
|
1319
|
+
case "oklch": {
|
|
1320
|
+
const L = parseNumberOrPercent(components[0], 1);
|
|
1321
|
+
const C = parseNumberOrPercent(components[1], .4);
|
|
1322
|
+
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
1323
|
+
const [h, s, l] = oklabToOkhsl([
|
|
1324
|
+
L,
|
|
1325
|
+
C * Math.cos(hRad),
|
|
1326
|
+
C * Math.sin(hRad)
|
|
1327
|
+
]);
|
|
1328
|
+
return {
|
|
1329
|
+
h,
|
|
1330
|
+
s,
|
|
1331
|
+
l
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
1339
|
+
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
1340
|
+
*/
|
|
1341
|
+
function validateOkhslColor(value) {
|
|
1342
|
+
const { h, s, l } = value;
|
|
1343
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
1344
|
+
if (s > 1.5 || l > 1.5) throw new Error("glaze.color: OkhslColor s/l must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
1345
|
+
}
|
|
1346
|
+
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
1347
|
+
function validateRgbColor(value) {
|
|
1348
|
+
for (const key of [
|
|
1349
|
+
"r",
|
|
1350
|
+
"g",
|
|
1351
|
+
"b"
|
|
1352
|
+
]) {
|
|
1353
|
+
const n = value[key];
|
|
1354
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) throw new Error(`glaze.color: RgbColor ${key} must be a finite number in 0–255 (got ${n}).`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
1358
|
+
function validateOklchColor(value) {
|
|
1359
|
+
const { l, c, h } = value;
|
|
1360
|
+
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
1361
|
+
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
1362
|
+
}
|
|
1363
|
+
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
1364
|
+
const hRad = hDeg * Math.PI / 180;
|
|
1365
|
+
const [h, s, outL] = oklabToOkhsl([
|
|
1366
|
+
l,
|
|
1367
|
+
c * Math.cos(hRad),
|
|
1368
|
+
c * Math.sin(hRad)
|
|
1369
|
+
]);
|
|
1370
|
+
return {
|
|
1371
|
+
h,
|
|
1372
|
+
s,
|
|
1373
|
+
l: outL
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function isRgbColorObject(value) {
|
|
1377
|
+
return "r" in value && "g" in value && "b" in value;
|
|
1378
|
+
}
|
|
1379
|
+
function isOklchColorObject(value) {
|
|
1380
|
+
return "c" in value && "l" in value && "h" in value;
|
|
1381
|
+
}
|
|
1382
|
+
function isOkhstColorObject(value) {
|
|
1383
|
+
return "t" in value && "h" in value && "s" in value;
|
|
1384
|
+
}
|
|
1385
|
+
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
1386
|
+
function validateOkhstColor(value) {
|
|
1387
|
+
const { h, s, t } = value;
|
|
1388
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
1389
|
+
if (s > 1.5 || t > 1.5) throw new Error("glaze.color: OkhstColor s/t must be in 0–1 range. Did you mean the structured form { hue, saturation, tone } (which uses 0–100)?");
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
1393
|
+
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
1394
|
+
* literal objects) go through one parser.
|
|
1395
|
+
*/
|
|
1396
|
+
function extractOkhslFromValue(value) {
|
|
1397
|
+
if (typeof value === "string") return parseColorString(value);
|
|
1398
|
+
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
1399
|
+
if (isRgbColorObject(value)) {
|
|
1400
|
+
validateRgbColor(value);
|
|
1401
|
+
const [h, s, l] = srgbToOkhsl([
|
|
1402
|
+
value.r / 255,
|
|
1403
|
+
value.g / 255,
|
|
1404
|
+
value.b / 255
|
|
1405
|
+
]);
|
|
1406
|
+
return {
|
|
1407
|
+
h,
|
|
1408
|
+
s,
|
|
1409
|
+
l
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
if (isOklchColorObject(value)) {
|
|
1413
|
+
validateOklchColor(value);
|
|
1414
|
+
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
1415
|
+
}
|
|
1416
|
+
if (isOkhstColorObject(value)) {
|
|
1417
|
+
validateOkhstColor(value);
|
|
1418
|
+
return okhstToOkhsl(value);
|
|
1419
|
+
}
|
|
1420
|
+
validateOkhslColor(value);
|
|
1421
|
+
return value;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
//#endregion
|
|
1425
|
+
//#region src/format-guard.ts
|
|
1426
|
+
const NON_NATIVE_FORMATS = new Set(["okhsl", "okhst"]);
|
|
1427
|
+
/**
|
|
1428
|
+
* Throw when a non-native Glaze color space is requested for an export that
|
|
1429
|
+
* emits raw CSS or non-Tasty token maps.
|
|
1430
|
+
*/
|
|
1431
|
+
function assertNativeFormat(format, method) {
|
|
1432
|
+
if (format !== void 0 && NON_NATIVE_FORMATS.has(format)) throw new Error(`glaze: ${format} output is only supported by tasty() (not a native CSS color space). Use tasty({ format: '${format}' }) or pick a native format (oklch|hsl|rgb) for ${method}().`);
|
|
1433
|
+
}
|
|
1434
|
+
const SCHEME_FIELDS = [
|
|
1435
|
+
{
|
|
1436
|
+
field: "light",
|
|
1437
|
+
modes: () => true
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
field: "dark",
|
|
1441
|
+
modes: (m) => m.dark
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
field: "lightContrast",
|
|
1445
|
+
modes: (m) => m.highContrast
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
field: "darkContrast",
|
|
1449
|
+
modes: (m) => m.dark && m.highContrast
|
|
1450
|
+
}
|
|
1451
|
+
];
|
|
1452
|
+
/**
|
|
1453
|
+
* Throw when `splitHue` is enabled but any exported color is not pastel.
|
|
1454
|
+
* Hue rotation is only clip-free when chroma is bounded by the hue-independent
|
|
1455
|
+
* safe chroma (`computeSafeChromaOKLCH`).
|
|
1456
|
+
*/
|
|
1457
|
+
function assertAllPastel(resolved, modes) {
|
|
1458
|
+
const nonPastel = [];
|
|
1459
|
+
for (const [name, color] of resolved) for (const { field, modes: active } of SCHEME_FIELDS) {
|
|
1460
|
+
if (!active(modes)) continue;
|
|
1461
|
+
if (color[field].pastel !== true) {
|
|
1462
|
+
if (!nonPastel.includes(name)) nonPastel.push(name);
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (nonPastel.length === 0) return;
|
|
1467
|
+
throw new Error(`glaze: splitHue requires every color to be pastel (hue rotation is only clip-free when chroma is bounded by the hue-independent safe chroma). Non-pastel: ${nonPastel.join(", ")}. Set pastel: true (per-theme, per-token, or per-color) or drop splitHue.`);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1259
1470
|
//#endregion
|
|
1260
1471
|
//#region src/contrast-solver.ts
|
|
1261
1472
|
/**
|
|
@@ -1894,6 +2105,16 @@ function computeShadow(bg, fg, intensity, tuning) {
|
|
|
1894
2105
|
* its base / bg / fg / target dependencies.
|
|
1895
2106
|
*/
|
|
1896
2107
|
/**
|
|
2108
|
+
* Does this color sit at an absolute tone — either authored, or carried by a
|
|
2109
|
+
* `from` value? That is the test for "root" (placed on its own) as opposed to
|
|
2110
|
+
* "dependent" (placed relative to a base), and it is what lets a bare
|
|
2111
|
+
* `{ from: '#2F5BFF' }` stand as a complete color definition.
|
|
2112
|
+
*/
|
|
2113
|
+
function hasAbsoluteTone(def) {
|
|
2114
|
+
if (def.tone === void 0) return def.from !== void 0;
|
|
2115
|
+
return isAbsoluteTone(def.tone);
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
1897
2118
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
1898
2119
|
*
|
|
1899
2120
|
* A WCAG ratio and an APCA Lc are different scales, so `[4.5, { apca: 75 }]`
|
|
@@ -1932,11 +2153,16 @@ function validateColorDefs(defs, externalBases) {
|
|
|
1932
2153
|
}
|
|
1933
2154
|
const regDef = def;
|
|
1934
2155
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
2156
|
+
if (regDef.from !== void 0) try {
|
|
2157
|
+
extractOkhslFromValue(regDef.from);
|
|
2158
|
+
} catch (error) {
|
|
2159
|
+
throw new Error(`glaze: color "${name}" has an invalid "from" value. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2160
|
+
}
|
|
1935
2161
|
if (regDef.contrast !== void 0 && !regDef.base) throw new Error(`glaze: color "${name}" has "contrast" without "base".`);
|
|
1936
2162
|
if (regDef.tone !== void 0 && !isAbsoluteTone(regDef.tone) && !regDef.base) throw new Error(`glaze: color "${name}" has relative "tone" without "base".`);
|
|
1937
2163
|
if (regDef.base && !allNames.has(regDef.base)) throw new Error(`glaze: color "${name}" references non-existent base "${regDef.base}".`);
|
|
1938
2164
|
if (regDef.base && localNames.has(regDef.base) && isShadowDef(defs[regDef.base])) throw new Error(`glaze: color "${name}" base "${regDef.base}" references a shadow color.`);
|
|
1939
|
-
if (!
|
|
2165
|
+
if (!hasAbsoluteTone(regDef) && regDef.base === void 0) throw new Error(`glaze: color "${name}" must have either absolute "tone" (root), "from" (a literal color), or "base" (dependent).`);
|
|
1940
2166
|
if (regDef.contrast !== void 0 && regDef.opacity !== void 0) console.warn(`glaze: color "${name}" has both "contrast" and "opacity". Opacity makes perceived tone unpredictable.`);
|
|
1941
2167
|
}
|
|
1942
2168
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -2186,11 +2412,50 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
2186
2412
|
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2187
2413
|
* intended, since the author asked for the extreme.
|
|
2188
2414
|
*/
|
|
2189
|
-
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved,
|
|
2190
|
-
const
|
|
2191
|
-
|
|
2415
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, config) {
|
|
2416
|
+
const extremeLightTone = mapToneForScheme(authorTone, mode, false, isHighContrast, config);
|
|
2417
|
+
const lightBase = getSchemeVariant(baseResolved, false, isHighContrast);
|
|
2418
|
+
const darkBase = getSchemeVariant(baseResolved, true, isHighContrast);
|
|
2419
|
+
const shift = extremeLightTone - lightBase.t * 100;
|
|
2420
|
+
return clamp(darkBase.t * 100 + (mode === "auto" ? -shift : shift), 0, 100);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Hue, absolute saturation (0–1) and tone (0–100) read off a color def's `from`.
|
|
2424
|
+
*
|
|
2425
|
+
* Deliberately not memoized. Keying a cache on the def object looks free — a
|
|
2426
|
+
* resolve reads this a handful of times per color — but defs are stored by
|
|
2427
|
+
* reference, so mutating one in place and re-setting it would invalidate the
|
|
2428
|
+
* theme's own cache while a def-keyed cache quietly served the old color. The
|
|
2429
|
+
* parse is a few microseconds against a resolve that is already cached; that is
|
|
2430
|
+
* not a trade worth a staleness bug.
|
|
2431
|
+
*/
|
|
2432
|
+
function fromSeed(def) {
|
|
2433
|
+
if (def.from === void 0) return void 0;
|
|
2434
|
+
const { h, s, l } = extractOkhslFromValue(def.from);
|
|
2435
|
+
return {
|
|
2436
|
+
hue: h,
|
|
2437
|
+
saturation: s,
|
|
2438
|
+
tone: toTone(l)
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* The config this color resolves against.
|
|
2443
|
+
*
|
|
2444
|
+
* A `from` color carries a literal value, so its light variant has to survive
|
|
2445
|
+
* the light tone window intact — hence a local `lightTone: false`, the same
|
|
2446
|
+
* default the value-shorthand form of `glaze.color()` applies. Only the light
|
|
2447
|
+
* window is dropped: dark and high contrast keep theirs, because that is where
|
|
2448
|
+
* the color is expected to adapt.
|
|
2449
|
+
*/
|
|
2450
|
+
function configForColor(def, config) {
|
|
2451
|
+
if (def.from === void 0) return config;
|
|
2452
|
+
return {
|
|
2453
|
+
...config,
|
|
2454
|
+
lightTone: false
|
|
2455
|
+
};
|
|
2192
2456
|
}
|
|
2193
2457
|
function resolveRootColor(def, isHighContrast, config) {
|
|
2458
|
+
if (def.tone === void 0) return clamp(fromSeed(def).tone, 0, 100);
|
|
2194
2459
|
return clamp(passTone(def.tone, isHighContrast, config).value, 0, 100);
|
|
2195
2460
|
}
|
|
2196
2461
|
/**
|
|
@@ -2209,16 +2474,20 @@ function resolveRootColor(def, isHighContrast, config) {
|
|
|
2209
2474
|
function resolveChannels(def, ctx, isDark) {
|
|
2210
2475
|
const mode = def.mode ?? "auto";
|
|
2211
2476
|
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2477
|
+
const seed = fromSeed(def);
|
|
2478
|
+
const fromHue = seed !== void 0 && def.hue === void 0 ? seed.hue : void 0;
|
|
2479
|
+
const fromSaturation = seed !== void 0 && def.saturation === void 0 ? seed.saturation : void 0;
|
|
2212
2480
|
if (!isDark || mode === "static") return {
|
|
2213
|
-
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2214
|
-
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2481
|
+
hue: fromHue ?? resolveEffectiveHue(ctx.hue, def.hue),
|
|
2482
|
+
saturation: fromSaturation ?? clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2215
2483
|
};
|
|
2216
2484
|
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2217
2485
|
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2218
|
-
const
|
|
2219
|
-
const raw = darkFactor * darkSeedSaturation / 100;
|
|
2486
|
+
const fromDark = fromSaturation !== void 0 && def.darkSaturation === void 0;
|
|
2487
|
+
const raw = fromDark ? fromSaturation : darkFactor * darkSeedSaturation / 100;
|
|
2488
|
+
const explicitDark = fromDark ? false : def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2220
2489
|
return {
|
|
2221
|
-
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2490
|
+
hue: fromHue !== void 0 && def.darkHue === void 0 ? fromHue : resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2222
2491
|
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2223
2492
|
};
|
|
2224
2493
|
}
|
|
@@ -2229,40 +2498,39 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels,
|
|
|
2229
2498
|
const mode = def.mode ?? "auto";
|
|
2230
2499
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2231
2500
|
const pastel = effectivePastel;
|
|
2501
|
+
const config = configForColor(def, ctx.config);
|
|
2232
2502
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2233
2503
|
const baseTone = baseVariant.t * 100;
|
|
2234
2504
|
let preferredTone;
|
|
2235
2505
|
let isExtreme = false;
|
|
2236
|
-
const
|
|
2506
|
+
const seed = fromSeed(def);
|
|
2507
|
+
const rawTone = def.tone ?? (seed !== void 0 ? seed.tone : void 0);
|
|
2237
2508
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2238
2509
|
else {
|
|
2239
|
-
const parsed = passTone(rawTone, isHighContrast,
|
|
2510
|
+
const parsed = passTone(rawTone, isHighContrast, config);
|
|
2240
2511
|
if (parsed.kind === "relative") if (isDark && mode === "auto") {
|
|
2241
2512
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2242
|
-
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast,
|
|
2513
|
+
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, config);
|
|
2243
2514
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2244
|
-
else
|
|
2245
|
-
isExtreme = true;
|
|
2246
|
-
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2247
|
-
} else {
|
|
2515
|
+
else {
|
|
2248
2516
|
isExtreme = parsed.kind === "extreme";
|
|
2249
|
-
preferredTone =
|
|
2517
|
+
if (isExtreme && isDark && mode !== "static") preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, config);
|
|
2518
|
+
else preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, config);
|
|
2250
2519
|
}
|
|
2251
2520
|
}
|
|
2252
2521
|
const rawContrast = def.contrast;
|
|
2253
2522
|
if (rawContrast !== void 0) {
|
|
2254
|
-
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast,
|
|
2523
|
+
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, config, polarity);
|
|
2255
2524
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2256
2525
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2257
|
-
const
|
|
2526
|
+
const preferredRange = isExtreme ? [0, 1] : schemeToneRange(isDark, mode, isHighContrast, config);
|
|
2258
2527
|
let initialDirection;
|
|
2259
2528
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2260
2529
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2261
|
-
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2262
2530
|
const solve = {
|
|
2263
2531
|
hue: channels.hue,
|
|
2264
2532
|
saturation: channels.saturation,
|
|
2265
|
-
preferredTone:
|
|
2533
|
+
preferredTone: clamp(preferredTone / 100, preferredRange[0], preferredRange[1]),
|
|
2266
2534
|
baseLinearRgb,
|
|
2267
2535
|
toneRange: [0, 1],
|
|
2268
2536
|
flip,
|
|
@@ -2294,11 +2562,12 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2294
2562
|
if (isMixDef(def)) return resolveMixForScheme(name, def, ctx, isDark, isHighContrast);
|
|
2295
2563
|
const regDef = def;
|
|
2296
2564
|
const mode = regDef.mode ?? "auto";
|
|
2297
|
-
const isRoot =
|
|
2565
|
+
const isRoot = hasAbsoluteTone(regDef) && !regDef.base;
|
|
2298
2566
|
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2299
2567
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2300
2568
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2301
|
-
const
|
|
2569
|
+
const config = configForColor(regDef, ctx.config);
|
|
2570
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast, config), mode, isDark, isHighContrast, config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2302
2571
|
return {
|
|
2303
2572
|
h: channels.hue,
|
|
2304
2573
|
s: channels.saturation,
|
|
@@ -2559,7 +2828,9 @@ function resolveAllColors(seed, defs, config, externalBases) {
|
|
|
2559
2828
|
*
|
|
2560
2829
|
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2561
2830
|
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2562
|
-
* set is emitted only when it actually differs — see `
|
|
2831
|
+
* set is emitted only when it actually differs — see `buildHueDeclarations`.
|
|
2832
|
+
* A color that authors a hue in *either* scheme gets its own var in both, so
|
|
2833
|
+
* the shared `var()` reference stays valid.
|
|
2563
2834
|
*/
|
|
2564
2835
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2565
2836
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2581,23 +2852,38 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2581
2852
|
const regDef = def;
|
|
2582
2853
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2583
2854
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2584
|
-
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2585
|
-
|
|
2855
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) return {
|
|
2856
|
+
hueVar: `var(${prop})`,
|
|
2857
|
+
inline: false,
|
|
2858
|
+
declarations: [{
|
|
2859
|
+
prop,
|
|
2860
|
+
value: String(variant.h)
|
|
2861
|
+
}]
|
|
2862
|
+
};
|
|
2863
|
+
if (regDef.hue === void 0 && regDef.darkHue === void 0 && regDef.from === void 0) return {
|
|
2864
|
+
hueVar: baseHueVar,
|
|
2865
|
+
inline: false,
|
|
2866
|
+
declarations: []
|
|
2867
|
+
};
|
|
2868
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2869
|
+
if (authored === void 0) {
|
|
2870
|
+
if (regDef.from !== void 0) return {
|
|
2871
|
+
hueVar: `var(${prop})`,
|
|
2872
|
+
inline: false,
|
|
2873
|
+
declarations: [{
|
|
2874
|
+
prop,
|
|
2875
|
+
value: String(variant.h)
|
|
2876
|
+
}]
|
|
2877
|
+
};
|
|
2586
2878
|
return {
|
|
2587
2879
|
hueVar: `var(${prop})`,
|
|
2588
2880
|
inline: false,
|
|
2589
2881
|
declarations: [{
|
|
2590
2882
|
prop,
|
|
2591
|
-
value:
|
|
2883
|
+
value: baseHueVar
|
|
2592
2884
|
}]
|
|
2593
2885
|
};
|
|
2594
2886
|
}
|
|
2595
|
-
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2596
|
-
if (authored === void 0) return {
|
|
2597
|
-
hueVar: baseHueVar,
|
|
2598
|
-
inline: false,
|
|
2599
|
-
declarations: []
|
|
2600
|
-
};
|
|
2601
2887
|
const parsed = parseRelativeOrAbsolute(authored);
|
|
2602
2888
|
if (parsed.relative) {
|
|
2603
2889
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
@@ -2644,7 +2930,7 @@ function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
|
2644
2930
|
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2645
2931
|
}
|
|
2646
2932
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2647
|
-
function collectHueDeclarations(resolved, ctx, scheme
|
|
2933
|
+
function collectHueDeclarations(resolved, ctx, scheme) {
|
|
2648
2934
|
if (ctx.emitDeclarations === false) return [];
|
|
2649
2935
|
const seen = /* @__PURE__ */ new Set();
|
|
2650
2936
|
const out = [];
|
|
@@ -2665,21 +2951,24 @@ function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
|
2665
2951
|
return out;
|
|
2666
2952
|
}
|
|
2667
2953
|
/**
|
|
2668
|
-
*
|
|
2669
|
-
* light
|
|
2954
|
+
* Hue declarations for both schemes. `dark` is empty when it resolves to the
|
|
2955
|
+
* same hues as light — the common case, with no `darkHue` anywhere.
|
|
2670
2956
|
*
|
|
2671
|
-
* When they differ, the **whole** set is returned rather than only the
|
|
2672
|
-
* whose text changed. A relative declaration reads
|
|
2957
|
+
* When they differ, the **whole** dark set is returned rather than only the
|
|
2958
|
+
* entries whose text changed. A relative declaration reads
|
|
2673
2959
|
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2674
2960
|
* moves with the seed, and custom properties substitute `var()` at
|
|
2675
2961
|
* computed-value time on the declaring element — so re-declaring
|
|
2676
2962
|
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2677
2963
|
* the light-substituted value.
|
|
2678
2964
|
*/
|
|
2679
|
-
function
|
|
2680
|
-
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2965
|
+
function buildHueDeclarations(resolved, ctx) {
|
|
2681
2966
|
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2682
|
-
|
|
2967
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2968
|
+
return {
|
|
2969
|
+
light,
|
|
2970
|
+
dark: dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark
|
|
2971
|
+
};
|
|
2683
2972
|
}
|
|
2684
2973
|
function buildHuePlans(resolved, ctx) {
|
|
2685
2974
|
const plans = /* @__PURE__ */ new Map();
|
|
@@ -2769,8 +3058,9 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2769
3058
|
const tokens = {};
|
|
2770
3059
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2771
3060
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2772
|
-
|
|
2773
|
-
|
|
3061
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3062
|
+
for (const decl of hueDecls.light) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
3063
|
+
if (modes.dark) for (const decl of hueDecls.dark) {
|
|
2774
3064
|
const key = `$${decl.prop.slice(2)}`;
|
|
2775
3065
|
tokens[key] = {
|
|
2776
3066
|
...tokens[key],
|
|
@@ -2827,8 +3117,9 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2827
3117
|
};
|
|
2828
3118
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2829
3119
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2830
|
-
|
|
2831
|
-
for (const decl of
|
|
3120
|
+
const hueDecls = buildHueDeclarations(resolved, channelCtx);
|
|
3121
|
+
for (const decl of hueDecls.light) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
3122
|
+
for (const decl of hueDecls.dark) {
|
|
2832
3123
|
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2833
3124
|
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2834
3125
|
}
|
|
@@ -3074,173 +3365,6 @@ function sparseValueFormLocal(userOverride) {
|
|
|
3074
3365
|
};
|
|
3075
3366
|
}
|
|
3076
3367
|
/**
|
|
3077
|
-
* Matches the CSS color functions Glaze itself emits (`rgb()`, `hsl()`,
|
|
3078
|
-
* `okhsl()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
3079
|
-
*
|
|
3080
|
-
* Only bare numeric components are supported. Named colors (`red`),
|
|
3081
|
-
* relative-color syntax (`from <color> ...`), and angle units other
|
|
3082
|
-
* than bare degrees (`deg` is the only suffix tolerated by `parseFloat`)
|
|
3083
|
-
* are out of scope.
|
|
3084
|
-
*/
|
|
3085
|
-
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
3086
|
-
function parseNumberOrPercent(raw, percentScale) {
|
|
3087
|
-
if (raw.endsWith("%")) return parseFloat(raw) / 100 * percentScale;
|
|
3088
|
-
return parseFloat(raw);
|
|
3089
|
-
}
|
|
3090
|
-
/**
|
|
3091
|
-
* Split the body of a CSS color function into its components and detect
|
|
3092
|
-
* whether an alpha channel was present.
|
|
3093
|
-
*
|
|
3094
|
-
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
3095
|
-
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
3096
|
-
* by the caller — standalone Glaze colors have no opacity field.
|
|
3097
|
-
*/
|
|
3098
|
-
function splitColorBody(body) {
|
|
3099
|
-
const slashIdx = body.indexOf("/");
|
|
3100
|
-
if (slashIdx !== -1) return {
|
|
3101
|
-
components: body.slice(0, slashIdx).trim().split(/[\s,]+/).filter(Boolean),
|
|
3102
|
-
hadAlpha: body.slice(slashIdx + 1).trim().length > 0
|
|
3103
|
-
};
|
|
3104
|
-
const components = body.split(/[\s,]+/).filter(Boolean);
|
|
3105
|
-
if (components.length === 4) {
|
|
3106
|
-
components.pop();
|
|
3107
|
-
return {
|
|
3108
|
-
components,
|
|
3109
|
-
hadAlpha: true
|
|
3110
|
-
};
|
|
3111
|
-
}
|
|
3112
|
-
return {
|
|
3113
|
-
components,
|
|
3114
|
-
hadAlpha: false
|
|
3115
|
-
};
|
|
3116
|
-
}
|
|
3117
|
-
function warnDroppedAlpha(input) {
|
|
3118
|
-
console.warn(`glaze: alpha component dropped from "${input}" (standalone color has no opacity field).`);
|
|
3119
|
-
}
|
|
3120
|
-
function parseColorString(input) {
|
|
3121
|
-
if (input.startsWith("#")) {
|
|
3122
|
-
const parsed = parseHexAlpha(input);
|
|
3123
|
-
if (!parsed) throw new Error(`glaze: invalid hex color "${input}".`);
|
|
3124
|
-
if (parsed.alpha !== void 0) warnDroppedAlpha(input);
|
|
3125
|
-
const [h, s, l] = srgbToOkhsl(parsed.rgb);
|
|
3126
|
-
return {
|
|
3127
|
-
h,
|
|
3128
|
-
s,
|
|
3129
|
-
l
|
|
3130
|
-
};
|
|
3131
|
-
}
|
|
3132
|
-
const m = input.match(COLOR_FN_RE);
|
|
3133
|
-
if (!m) throw new Error(`glaze: unsupported color string "${input}".`);
|
|
3134
|
-
const fn = m[1].toLowerCase();
|
|
3135
|
-
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
3136
|
-
if (hadAlpha) warnDroppedAlpha(input);
|
|
3137
|
-
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
3138
|
-
switch (fn) {
|
|
3139
|
-
case "rgb":
|
|
3140
|
-
case "rgba": {
|
|
3141
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3142
|
-
parseNumberOrPercent(components[0], 255) / 255,
|
|
3143
|
-
parseNumberOrPercent(components[1], 255) / 255,
|
|
3144
|
-
parseNumberOrPercent(components[2], 255) / 255
|
|
3145
|
-
]);
|
|
3146
|
-
return {
|
|
3147
|
-
h,
|
|
3148
|
-
s,
|
|
3149
|
-
l
|
|
3150
|
-
};
|
|
3151
|
-
}
|
|
3152
|
-
case "hsl":
|
|
3153
|
-
case "hsla": {
|
|
3154
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseFloat(components[0]), parseNumberOrPercent(components[1], 1), parseNumberOrPercent(components[2], 1)));
|
|
3155
|
-
return {
|
|
3156
|
-
h: oh,
|
|
3157
|
-
s: os,
|
|
3158
|
-
l: ol
|
|
3159
|
-
};
|
|
3160
|
-
}
|
|
3161
|
-
case "okhsl": return {
|
|
3162
|
-
h: parseFloat(components[0]),
|
|
3163
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3164
|
-
l: parseNumberOrPercent(components[2], 1)
|
|
3165
|
-
};
|
|
3166
|
-
case "okhst": return okhstToOkhsl({
|
|
3167
|
-
h: parseFloat(components[0]),
|
|
3168
|
-
s: parseNumberOrPercent(components[1], 1),
|
|
3169
|
-
t: parseNumberOrPercent(components[2], 1)
|
|
3170
|
-
});
|
|
3171
|
-
case "oklch": {
|
|
3172
|
-
const L = parseNumberOrPercent(components[0], 1);
|
|
3173
|
-
const C = parseNumberOrPercent(components[1], .4);
|
|
3174
|
-
const hRad = parseFloat(components[2]) * Math.PI / 180;
|
|
3175
|
-
const [h, s, l] = oklabToOkhsl([
|
|
3176
|
-
L,
|
|
3177
|
-
C * Math.cos(hRad),
|
|
3178
|
-
C * Math.sin(hRad)
|
|
3179
|
-
]);
|
|
3180
|
-
return {
|
|
3181
|
-
h,
|
|
3182
|
-
s,
|
|
3183
|
-
l
|
|
3184
|
-
};
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
throw new Error(`glaze: unsupported color function "${fn}".`);
|
|
3188
|
-
}
|
|
3189
|
-
/**
|
|
3190
|
-
* Validate a user-supplied `OkhslColor`. Catches the common 0-100 vs 0-1
|
|
3191
|
-
* confusion (the structured form uses 0-100, OKHSL objects use 0-1).
|
|
3192
|
-
*/
|
|
3193
|
-
function validateOkhslColor(value) {
|
|
3194
|
-
const { h, s, l } = value;
|
|
3195
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3196
|
-
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)?");
|
|
3197
|
-
}
|
|
3198
|
-
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
3199
|
-
function validateRgbColor(value) {
|
|
3200
|
-
for (const key of [
|
|
3201
|
-
"r",
|
|
3202
|
-
"g",
|
|
3203
|
-
"b"
|
|
3204
|
-
]) {
|
|
3205
|
-
const n = value[key];
|
|
3206
|
-
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}).`);
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
/** Validate a user-supplied `{ l, c, h }` OKLCh object. */
|
|
3210
|
-
function validateOklchColor(value) {
|
|
3211
|
-
const { l, c, h } = value;
|
|
3212
|
-
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3213
|
-
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
3214
|
-
}
|
|
3215
|
-
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
3216
|
-
const hRad = hDeg * Math.PI / 180;
|
|
3217
|
-
const [h, s, outL] = oklabToOkhsl([
|
|
3218
|
-
l,
|
|
3219
|
-
c * Math.cos(hRad),
|
|
3220
|
-
c * Math.sin(hRad)
|
|
3221
|
-
]);
|
|
3222
|
-
return {
|
|
3223
|
-
h,
|
|
3224
|
-
s,
|
|
3225
|
-
l: outL
|
|
3226
|
-
};
|
|
3227
|
-
}
|
|
3228
|
-
function isRgbColorObject(value) {
|
|
3229
|
-
return "r" in value && "g" in value && "b" in value;
|
|
3230
|
-
}
|
|
3231
|
-
function isOklchColorObject(value) {
|
|
3232
|
-
return "c" in value && "l" in value && "h" in value;
|
|
3233
|
-
}
|
|
3234
|
-
function isOkhstColorObject(value) {
|
|
3235
|
-
return "t" in value && "h" in value && "s" in value;
|
|
3236
|
-
}
|
|
3237
|
-
/** Validate a user-supplied `{ h, s, t }` OKHST object (s/t in 0–1). */
|
|
3238
|
-
function validateOkhstColor(value) {
|
|
3239
|
-
const { h, s, t } = value;
|
|
3240
|
-
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3241
|
-
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)?");
|
|
3242
|
-
}
|
|
3243
|
-
/**
|
|
3244
3368
|
* Validate a user-supplied `opacity` override on `glaze.color()`.
|
|
3245
3369
|
* Must be a finite number in `0..=1`.
|
|
3246
3370
|
*/
|
|
@@ -3290,38 +3414,6 @@ function validateStandaloneName(name) {
|
|
|
3290
3414
|
}
|
|
3291
3415
|
}
|
|
3292
3416
|
/**
|
|
3293
|
-
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
3294
|
-
* `glaze.shadow()` so all shadow inputs (hex, color functions, OKHSL,
|
|
3295
|
-
* literal objects) go through one parser.
|
|
3296
|
-
*/
|
|
3297
|
-
function extractOkhslFromValue(value) {
|
|
3298
|
-
if (typeof value === "string") return parseColorString(value);
|
|
3299
|
-
if (Array.isArray(value)) throw new Error("glaze.color: RGB tuple [r, g, b] is no longer supported — use { r, g, b } instead.");
|
|
3300
|
-
if (isRgbColorObject(value)) {
|
|
3301
|
-
validateRgbColor(value);
|
|
3302
|
-
const [h, s, l] = srgbToOkhsl([
|
|
3303
|
-
value.r / 255,
|
|
3304
|
-
value.g / 255,
|
|
3305
|
-
value.b / 255
|
|
3306
|
-
]);
|
|
3307
|
-
return {
|
|
3308
|
-
h,
|
|
3309
|
-
s,
|
|
3310
|
-
l
|
|
3311
|
-
};
|
|
3312
|
-
}
|
|
3313
|
-
if (isOklchColorObject(value)) {
|
|
3314
|
-
validateOklchColor(value);
|
|
3315
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3316
|
-
}
|
|
3317
|
-
if (isOkhstColorObject(value)) {
|
|
3318
|
-
validateOkhstColor(value);
|
|
3319
|
-
return okhstToOkhsl(value);
|
|
3320
|
-
}
|
|
3321
|
-
validateOkhslColor(value);
|
|
3322
|
-
return value;
|
|
3323
|
-
}
|
|
3324
|
-
/**
|
|
3325
3417
|
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3326
3418
|
*
|
|
3327
3419
|
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|