@tenphi/glaze 0.0.0-snapshot.0fcd6cf → 0.0.0-snapshot.12fd135
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/AGENTS.md +29 -28
- package/README.md +2 -2
- package/dist/index.cjs +604 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -34
- package/dist/index.d.mts +98 -34
- package/dist/index.mjs +602 -181
- package/dist/index.mjs.map +1 -1
- package/docs/exporting.mdx +2 -218
- package/docs/hdr.mdx +523 -0
- package/docs/methodology.mdx +2 -3
- package/docs/migration.md +53 -16
- package/docs/mixes.mdx +48 -5
- package/docs/okhst.mdx +2 -5
- package/docs/reference/color-definitions.md +13 -15
- package/docs/reference/color-tokens.md +11 -7
- package/docs/reference/configuration.md +28 -11
- package/docs/reference/contrast.md +4 -4
- package/docs/reference/index.md +7 -5
- package/docs/reference/output.md +31 -151
- package/docs/reference/palette.md +4 -4
- package/docs/reference/theme.md +10 -10
- package/docs/reference/utilities.md +40 -23
- package/docs/schemes.mdx +4 -5
- package/docs/shadows-and-glows.mdx +7 -225
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -97,6 +97,12 @@ const K1 = .206;
|
|
|
97
97
|
const K2 = .03;
|
|
98
98
|
const K3 = (1 + K1) / (1 + K2);
|
|
99
99
|
const EPSILON = 1e-10;
|
|
100
|
+
/**
|
|
101
|
+
* OKLab chroma under which an sRGB input counts as gray. An exact sRGB gray
|
|
102
|
+
* leaves float residue in OKLab a / b (at most 3.7e-8, at white); the faintest
|
|
103
|
+
* deliberate tint, one channel 0.01 of 255 off gray, has 1.06e-5.
|
|
104
|
+
*/
|
|
105
|
+
const GRAY_CHROMA = 1e-6;
|
|
100
106
|
const constrainAngle = (angle) => (angle % 360 + 360) % 360;
|
|
101
107
|
/**
|
|
102
108
|
* OKHSL toe function: maps OKLab lightness L to perceptual lightness l.
|
|
@@ -490,13 +496,21 @@ const oklabToOkhsl = (lab, pastel = false) => {
|
|
|
490
496
|
/**
|
|
491
497
|
* Convert gamma-encoded sRGB (0–1 per channel) to OKHSL.
|
|
492
498
|
* Returns [h, s, l] where h: 0–360, s: 0–1, l: 0–1.
|
|
499
|
+
*
|
|
500
|
+
* A gray reads with saturation 0 (its computed hue is kept). Its OKLab
|
|
501
|
+
* residue would otherwise come out as a saturation that grows toward white
|
|
502
|
+
* (`#fefefe` reads 1.6e-5, `rgb(254.999 254.999 254.999)` 1.6e-2), which a
|
|
503
|
+
* color carries into every scheme and an OKHSL mix reads as a hue.
|
|
493
504
|
*/
|
|
494
505
|
function srgbToOkhsl(rgb, pastel = false) {
|
|
495
|
-
|
|
506
|
+
const oklab = linearSrgbToOklab([
|
|
496
507
|
sRGBGammaToLinear(rgb[0]),
|
|
497
508
|
sRGBGammaToLinear(rgb[1]),
|
|
498
509
|
sRGBGammaToLinear(rgb[2])
|
|
499
|
-
])
|
|
510
|
+
]);
|
|
511
|
+
const okhsl = oklabToOkhsl(oklab, pastel);
|
|
512
|
+
if (Math.hypot(oklab[1], oklab[2]) < GRAY_CHROMA) okhsl[1] = 0;
|
|
513
|
+
return okhsl;
|
|
500
514
|
}
|
|
501
515
|
/**
|
|
502
516
|
* Convert CSS HSL (sRGB-based) to gamma-encoded sRGB [r, g, b] in 0–1 range.
|
|
@@ -670,13 +684,26 @@ function formatOklch(h, s, l, pastel = false) {
|
|
|
670
684
|
* Format linear sRGB channels as a CSS `color(srgb-linear R G B)` string with
|
|
671
685
|
* 4 decimals. Unlike the other writers it takes linear light, not OKHSL, and
|
|
672
686
|
* does not clamp: a channel may exceed 1, which is how an HDR glow value
|
|
673
|
-
*
|
|
674
|
-
* above it
|
|
687
|
+
* written with `glaze.formatHdr(v, 'srgb-linear')` goes past SDR white — `1`
|
|
688
|
+
* is SDR white, `2` one stop above it (the default extended form is
|
|
689
|
+
* `formatSrgb`). Opaque only; HDR values carry no alpha.
|
|
675
690
|
*/
|
|
676
691
|
function formatSrgbLinear(rgb) {
|
|
677
692
|
return `color(srgb-linear ${fmt$1(rgb[0], 4)} ${fmt$1(rgb[1], 4)} ${fmt$1(rgb[2], 4)})`;
|
|
678
693
|
}
|
|
679
694
|
/**
|
|
695
|
+
* Format extended sRGB channels — gamma-encoded, with the transfer function
|
|
696
|
+
* continued past 1 (`sRGBLinearToGamma` does) — as a CSS `color(srgb R G B)`
|
|
697
|
+
* string with 4 decimals. Like `formatSrgbLinear` it takes the channels of
|
|
698
|
+
* its own space and does not clamp, so an HDR glow value
|
|
699
|
+
* (`glaze.formatHdr()`) can pass SDR white: `1` is SDR white, about `1.3533`
|
|
700
|
+
* one stop above it. CSS reads it as the same color as the matching
|
|
701
|
+
* `color(srgb-linear …)`. Opaque only; HDR values carry no alpha.
|
|
702
|
+
*/
|
|
703
|
+
function formatSrgb(rgb) {
|
|
704
|
+
return `color(srgb ${fmt$1(rgb[0], 4)} ${fmt$1(rgb[1], 4)} ${fmt$1(rgb[2], 4)})`;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
680
707
|
* Convert gamma-encoded sRGB channels (0–1) to a 6-digit lowercase hex
|
|
681
708
|
* string (`#rrggbb`). Channels are clamped to [0,1] and rounded to 8-bit.
|
|
682
709
|
* Alpha is not encoded here — DTCG carries it as a separate `alpha` field.
|
|
@@ -716,18 +743,26 @@ function clamp(v, min, max) {
|
|
|
716
743
|
function lerp(a, b, f) {
|
|
717
744
|
return a + (b - a) * f;
|
|
718
745
|
}
|
|
746
|
+
/** Brackets `undefined` or a non-finite number while JSON writes it. */
|
|
747
|
+
const UNWRITABLE_MARK = "";
|
|
748
|
+
const UNWRITABLE_RE = /"\uE000([^"]*)\uE000"/g;
|
|
719
749
|
/**
|
|
720
750
|
* A rejected input as a validation error shows it. Numbers and `undefined`
|
|
721
751
|
* via `String` (`JSON.stringify(NaN)` is "null"), a bigint with its `n`
|
|
722
752
|
* (JSON can't write one), anything else via JSON so a string shows its quotes
|
|
723
|
-
* and an array its shape
|
|
724
|
-
*
|
|
753
|
+
* and an array its shape, with `undefined` or a non-finite number inside
|
|
754
|
+
* written as it prints (`[10,NaN]`, `{"wcag":undefined}`; JSON would write
|
|
755
|
+
* `null` or drop the key) — falling back to `String` for what JSON can't
|
|
756
|
+
* write (a symbol, a function, a cycle).
|
|
725
757
|
*/
|
|
726
758
|
function showValue(value) {
|
|
727
759
|
if (typeof value === "number" || value === void 0) return String(value);
|
|
728
760
|
if (typeof value === "bigint") return `${value}n`;
|
|
729
761
|
try {
|
|
730
|
-
|
|
762
|
+
const plain = JSON.stringify(value);
|
|
763
|
+
if (plain === void 0) return String(value);
|
|
764
|
+
if (plain.includes(UNWRITABLE_MARK)) return plain;
|
|
765
|
+
return JSON.stringify(value, (_key, v) => v === void 0 || typeof v === "number" && !Number.isFinite(v) ? `${UNWRITABLE_MARK}${v}${UNWRITABLE_MARK}` : v)?.replace(UNWRITABLE_RE, "$1") ?? String(value);
|
|
731
766
|
} catch {
|
|
732
767
|
return String(value);
|
|
733
768
|
}
|
|
@@ -1178,8 +1213,8 @@ function schemeToneRange(isDark, mode, isHighContrast, config) {
|
|
|
1178
1213
|
* surface at all gets none and keeps the SDR shortfall warning. Since the
|
|
1179
1214
|
* value lands the whole step, only a full-reach glow meets SDR at the boundary
|
|
1180
1215
|
* (`exposure` → 0); one with `reach` below 1 jumps from its pigment to the
|
|
1181
|
-
* exact step. `hdrToLinearSrgb`
|
|
1182
|
-
* sRGB for the writers.
|
|
1216
|
+
* exact step. `hdrToLinearSrgb` / `hdrToSrgb` turn a resolved HDR value back
|
|
1217
|
+
* into linear or extended sRGB channels for the writers (both public).
|
|
1183
1218
|
*/
|
|
1184
1219
|
/** Most stops of HDR headroom a glow may use; larger values clamp to it. */
|
|
1185
1220
|
const MAX_HEADROOM = 2;
|
|
@@ -1209,11 +1244,13 @@ const DEFAULT_LIGHTING_TUNING = {
|
|
|
1209
1244
|
* warned about one above the maximum.
|
|
1210
1245
|
*/
|
|
1211
1246
|
function resolveLightingTuning(perColor, globalTuning) {
|
|
1247
|
+
const field = (key) => perColor?.[key] ?? globalTuning?.[key] ?? DEFAULT_LIGHTING_TUNING[key];
|
|
1212
1248
|
const headroom = perColor?.headroom ?? globalTuning?.headroom ?? 0;
|
|
1213
1249
|
return {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1250
|
+
objectInfluence: field("objectInfluence"),
|
|
1251
|
+
tint: field("tint"),
|
|
1252
|
+
reach: field("reach"),
|
|
1253
|
+
hueBleed: field("hueBleed"),
|
|
1217
1254
|
headroom: clamp(headroom, 0, MAX_HEADROOM)
|
|
1218
1255
|
};
|
|
1219
1256
|
}
|
|
@@ -1223,11 +1260,18 @@ function circularLerp(a, b, t) {
|
|
|
1223
1260
|
else if (diff < -180) diff += 360;
|
|
1224
1261
|
return ((a + diff * t) % 360 + 360) % 360;
|
|
1225
1262
|
}
|
|
1226
|
-
|
|
1263
|
+
/**
|
|
1264
|
+
* Saturation at or below which a color has no hue of its own. A gray given as
|
|
1265
|
+
* a value parses with saturation 0; a near-gray under this has a hue too
|
|
1266
|
+
* faint to give a tint.
|
|
1267
|
+
*/
|
|
1268
|
+
const HUE_EPSILON = 1e-4;
|
|
1227
1269
|
const BISECT_ITERATIONS = 40;
|
|
1228
1270
|
const GOLDEN = (Math.sqrt(5) - 1) / 2;
|
|
1229
1271
|
/** Float noise a target tone may sit above 100 without counting as HDR. */
|
|
1230
1272
|
const HDR_TRIGGER_EPS = 1e-9;
|
|
1273
|
+
/** Float noise a pigment may move the surface by and still count as not. */
|
|
1274
|
+
const NO_LIFT_EPS = 1e-9;
|
|
1231
1275
|
/** Float noise a surface tone may sit below 100 and still count as white. */
|
|
1232
1276
|
const WHITE_SURFACE_EPS = 1e-6;
|
|
1233
1277
|
/**
|
|
@@ -1245,16 +1289,30 @@ function objectFactor(kind, bgTone, fgTone, objectInfluence) {
|
|
|
1245
1289
|
* The pigment: pure black (shadow) / pure white (glow) by default. `reach`
|
|
1246
1290
|
* below 1 parks it part of the way from the surface's tone to that extreme so
|
|
1247
1291
|
* a `tint` — saturation in the surface's hue, optionally pulled toward the
|
|
1248
|
-
* object's hue by `hueBleed` — can show.
|
|
1292
|
+
* object's hue by `hueBleed` — can show. A neutral surface has no hue to give:
|
|
1293
|
+
* the tint takes the object's hue when `hueBleed` is above 0, and the pigment
|
|
1294
|
+
* stays neutral otherwise.
|
|
1249
1295
|
*/
|
|
1250
1296
|
function pigmentFor(kind, bg, fg, bgTone, tuning) {
|
|
1251
1297
|
const reach = clamp(tuning.reach, 0, 1);
|
|
1252
1298
|
const tone = kind === "shadow" ? bgTone * (1 - reach) : bgTone + (100 - bgTone) * reach;
|
|
1253
1299
|
const bleed = clamp(tuning.hueBleed, 0, 1);
|
|
1300
|
+
const l = clamp(fromTone(tone), 0, 1);
|
|
1301
|
+
const tint = clamp(tuning.tint, 0, 1);
|
|
1302
|
+
const objectHue = fg !== void 0 && fg.s > HUE_EPSILON && bleed > 0 ? fg.h : void 0;
|
|
1303
|
+
if (bg.s <= HUE_EPSILON) return objectHue === void 0 ? {
|
|
1304
|
+
h: bg.h,
|
|
1305
|
+
s: 0,
|
|
1306
|
+
l
|
|
1307
|
+
} : {
|
|
1308
|
+
h: objectHue,
|
|
1309
|
+
s: tint,
|
|
1310
|
+
l
|
|
1311
|
+
};
|
|
1254
1312
|
return {
|
|
1255
|
-
h:
|
|
1256
|
-
s:
|
|
1257
|
-
l
|
|
1313
|
+
h: objectHue === void 0 ? bg.h : circularLerp(bg.h, objectHue, bleed),
|
|
1314
|
+
s: tint,
|
|
1315
|
+
l
|
|
1258
1316
|
};
|
|
1259
1317
|
}
|
|
1260
1318
|
/**
|
|
@@ -1311,7 +1369,7 @@ function solveOpacity(kind, gain, step) {
|
|
|
1311
1369
|
if (gain(1) < step) {
|
|
1312
1370
|
hi = kind === "shadow" ? strongestShadowAlpha(gain) : 1;
|
|
1313
1371
|
const reached = gain(hi);
|
|
1314
|
-
if (reached < step) return reached >
|
|
1372
|
+
if (reached < step) return reached > NO_LIFT_EPS ? {
|
|
1315
1373
|
alpha: hi,
|
|
1316
1374
|
reached
|
|
1317
1375
|
} : {
|
|
@@ -1422,6 +1480,21 @@ function hdrToLinearSrgb(hdr) {
|
|
|
1422
1480
|
];
|
|
1423
1481
|
}
|
|
1424
1482
|
/**
|
|
1483
|
+
* Extended sRGB of a resolved HDR value: `hdrToLinearSrgb` through the sRGB
|
|
1484
|
+
* transfer function, continued past 1 the way CSS and the platform
|
|
1485
|
+
* extended-sRGB spaces continue it, unclamped. These are the channels of
|
|
1486
|
+
* `color(srgb R G B)` (`glaze.formatHdr()`), and what a WebGPU `'srgb'`
|
|
1487
|
+
* canvas, `CGColorSpace.extendedSRGB`, and Android's `EXTENDED_SRGB` take.
|
|
1488
|
+
*/
|
|
1489
|
+
function hdrToSrgb(hdr) {
|
|
1490
|
+
const [r, g, b] = hdrToLinearSrgb(hdr);
|
|
1491
|
+
return [
|
|
1492
|
+
sRGBLinearToGamma(r),
|
|
1493
|
+
sRGBLinearToGamma(g),
|
|
1494
|
+
sRGBLinearToGamma(b)
|
|
1495
|
+
];
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1425
1498
|
* Opacity of a shadow pigment's darkest composite. Full opacity for pure
|
|
1426
1499
|
* black; a tinted pigment whose channels pull in different directions can
|
|
1427
1500
|
* bottom out earlier, so a golden-section search over the unimodal gain finds
|
|
@@ -1521,6 +1594,18 @@ const WCAG_HC_PROMOTION = {
|
|
|
1521
1594
|
AA: "AAA",
|
|
1522
1595
|
"AA-large": "AAA-large"
|
|
1523
1596
|
};
|
|
1597
|
+
/** An `{ apca }` spec: its `apca` is set (a key holding `undefined` is not). */
|
|
1598
|
+
function isApcaSpec(spec) {
|
|
1599
|
+
return spec.apca !== void 0;
|
|
1600
|
+
}
|
|
1601
|
+
/** Whether `value` names a WCAG preset (internal; see validation.ts). */
|
|
1602
|
+
function isContrastPreset(value) {
|
|
1603
|
+
return typeof value === "string" && Object.hasOwn(CONTRAST_PRESETS, value);
|
|
1604
|
+
}
|
|
1605
|
+
/** Whether `value` names an APCA preset (internal; see validation.ts). */
|
|
1606
|
+
function isApcaPreset(value) {
|
|
1607
|
+
return typeof value === "string" && Object.hasOwn(APCA_PRESETS, value);
|
|
1608
|
+
}
|
|
1524
1609
|
function resolveMinContrast(value) {
|
|
1525
1610
|
if (typeof value === "number") return Math.max(1, value);
|
|
1526
1611
|
return CONTRAST_PRESETS[value];
|
|
@@ -1546,7 +1631,7 @@ function pickPair(p, isHighContrast) {
|
|
|
1546
1631
|
* presets are `'wcag'`; only an `{ apca }` object is `'apca'`.
|
|
1547
1632
|
*/
|
|
1548
1633
|
function contrastMetricOf(spec) {
|
|
1549
|
-
return typeof spec === "object" &&
|
|
1634
|
+
return typeof spec === "object" && isApcaSpec(spec) ? "apca" : "wcag";
|
|
1550
1635
|
}
|
|
1551
1636
|
/**
|
|
1552
1637
|
* Resolve a `ContrastSpec` (already selected from any outer HC pair) for a
|
|
@@ -1569,12 +1654,12 @@ function resolveContrastForMode(spec, isHighContrast, polarity, outerExplicitHC)
|
|
|
1569
1654
|
metric: "wcag",
|
|
1570
1655
|
target: resolveWcagTarget(spec, isHighContrast, !!outerExplicitHC)
|
|
1571
1656
|
};
|
|
1572
|
-
if (
|
|
1657
|
+
if (isApcaSpec(spec)) {
|
|
1573
1658
|
const baseTarget = resolveApcaTarget(pickPair(spec.apca, isHighContrast));
|
|
1574
1659
|
const innerExplicitHC = Array.isArray(spec.apca);
|
|
1575
1660
|
return {
|
|
1576
1661
|
metric: "apca",
|
|
1577
|
-
target: isHighContrast && !outerExplicitHC && !innerExplicitHC ? Math.min(baseTarget + APCA_HC_ENHANCEMENT, APCA_MAX_LC) : baseTarget,
|
|
1662
|
+
target: isHighContrast && !outerExplicitHC && !innerExplicitHC ? Math.max(baseTarget, Math.min(baseTarget + APCA_HC_ENHANCEMENT, APCA_MAX_LC)) : baseTarget,
|
|
1578
1663
|
polarity: polarity ?? "fg"
|
|
1579
1664
|
};
|
|
1580
1665
|
}
|
|
@@ -1602,6 +1687,7 @@ function resolveContrastForMode(spec, isHighContrast, polarity, outerExplicitHC)
|
|
|
1602
1687
|
* for direct callers.
|
|
1603
1688
|
*/
|
|
1604
1689
|
function resolveContrastForLevel(spec, level, polarity) {
|
|
1690
|
+
if (!Number.isFinite(level)) throw new Error(`glaze: resolveContrastForLevel needs a finite level (got ${level}).`);
|
|
1605
1691
|
const outerExplicitHC = Array.isArray(spec);
|
|
1606
1692
|
const f = levelFraction(level);
|
|
1607
1693
|
const normal = resolveContrastForMode(pairNormal(spec), false, polarity, outerExplicitHC);
|
|
@@ -1947,10 +2033,11 @@ function dedupe(key) {
|
|
|
1947
2033
|
}
|
|
1948
2034
|
/**
|
|
1949
2035
|
* Whether `actual` is short of the floor by more than the slack. The solver
|
|
1950
|
-
* aims slightly past the target, so a smaller shortfall is rounding noise.
|
|
2036
|
+
* aims slightly past the target, so a smaller shortfall is rounding noise. A
|
|
2037
|
+
* non-finite target or measurement counts as a miss, so it is reported.
|
|
1951
2038
|
*/
|
|
1952
2039
|
function missesFloor(contrast, actual) {
|
|
1953
|
-
return actual
|
|
2040
|
+
return !(actual >= (contrast.metric === "apca" ? contrast.target - CONTRAST_WARN_SLACK_APCA : contrast.target * CONTRAST_WARN_SLACK_WCAG));
|
|
1954
2041
|
}
|
|
1955
2042
|
function contrastKey(kind, name, scheme, contrast, actual) {
|
|
1956
2043
|
return `${kind}|${name}|${scheme}|${contrast.metric}|${contrast.target.toFixed(2)}|${actual.toFixed(2)}`;
|
|
@@ -1966,13 +2053,16 @@ function warnContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
|
|
|
1966
2053
|
* Warn when a mix's ratio / opacity search could not reach its contrast floor
|
|
1967
2054
|
* against the mix's `base`. An unmet mix means even the pure target falls
|
|
1968
2055
|
* short, so the fix is the target color or the floor, not a tone window.
|
|
1969
|
-
* `actual` is the contrast of the value the mix settled at.
|
|
2056
|
+
* `actual` is the contrast of the value the mix settled at. `pastelCapped`
|
|
2057
|
+
* says the pastel cap is why: the mix's pastel gamut paints the target with
|
|
2058
|
+
* less saturation, and the target painted in full would meet the floor, so
|
|
2059
|
+
* the hint offers `pastel: false`.
|
|
1970
2060
|
*/
|
|
1971
|
-
function warnMixContrastUnmet(name, isDark, isHighContrast, contrast, actual) {
|
|
2061
|
+
function warnMixContrastUnmet(name, isDark, isHighContrast, contrast, actual, pastelCapped = false) {
|
|
1972
2062
|
if (!missesFloor(contrast, actual)) return;
|
|
1973
2063
|
const scheme = schemeLabel(isDark, isHighContrast);
|
|
1974
2064
|
if (dedupe(contrastKey("unmet-mix", name, scheme, contrast, actual))) return;
|
|
1975
|
-
console.warn(`glaze: mix "${name}" cannot meet ${metricLabel(contrast)} against its base in ${scheme} scheme (got ${actual.toFixed(2)}). Even the pure target falls short; try lowering the contrast target or mixing toward a color further from the base
|
|
2065
|
+
console.warn(`glaze: mix "${name}" cannot meet ${metricLabel(contrast)} against its base in ${scheme} scheme (got ${actual.toFixed(2)}). ` + (pastelCapped ? "Even the pure target falls short once the mix's pastel gamut caps its saturation; try `pastel: false` on the mix, lowering the contrast target, or mixing toward a color further from the base." : "Even the pure target falls short; try lowering the contrast target or mixing toward a color further from the base."));
|
|
1976
2066
|
}
|
|
1977
2067
|
/**
|
|
1978
2068
|
* Verification (the drift check in docs/okhst.mdx): a chromatic swatch
|
|
@@ -2126,6 +2216,13 @@ function defaultConfig() {
|
|
|
2126
2216
|
};
|
|
2127
2217
|
}
|
|
2128
2218
|
/**
|
|
2219
|
+
* `value`, or `fallback` when it is `undefined`. Only `undefined` means
|
|
2220
|
+
* "keep": a `null` is passed on so the field's validator rejects it.
|
|
2221
|
+
*/
|
|
2222
|
+
function keepIfUndefined(value, fallback) {
|
|
2223
|
+
return value !== void 0 ? value : fallback;
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2129
2226
|
* Validate an authored `contrastLevel`. Numbers clamp to 0–100 so a slider
|
|
2130
2227
|
* can't produce an out-of-range level; a non-finite number throws rather than
|
|
2131
2228
|
* silently reading as `'auto'`, which would turn a `NaN` slider value into an
|
|
@@ -2183,15 +2280,26 @@ function normalizeHeadroom(value, where, deferred) {
|
|
|
2183
2280
|
return value;
|
|
2184
2281
|
}
|
|
2185
2282
|
/**
|
|
2186
|
-
* Validate and copy a lighting tuning: a detached copy (or `undefined`) with
|
|
2187
|
-
*
|
|
2188
|
-
*
|
|
2189
|
-
* `
|
|
2190
|
-
*
|
|
2283
|
+
* Validate and copy a lighting tuning: a detached copy (or `undefined`) with
|
|
2284
|
+
* `objectInfluence` / `tint` / `reach` / `hueBleed` checked to be finite
|
|
2285
|
+
* numbers (the 0–1 fields clamp at use) and a glow's `headroom` checked and
|
|
2286
|
+
* clamped by `normalizeHeadroom`. A shadow has no headroom — it darkens toward
|
|
2287
|
+
* black, and nothing goes below black — so a `headroom` on shadow tuning
|
|
2288
|
+
* throws. Only `undefined` counts as unset. `deferred` queues the clamp
|
|
2289
|
+
* warning (see `normalizeHeadroom`).
|
|
2191
2290
|
*/
|
|
2192
2291
|
function normalizeLightingTuning(kind, tuning, where, deferred) {
|
|
2193
2292
|
if (tuning === void 0) return void 0;
|
|
2194
2293
|
const out = { ...tuning };
|
|
2294
|
+
for (const field of [
|
|
2295
|
+
"objectInfluence",
|
|
2296
|
+
"tint",
|
|
2297
|
+
"reach",
|
|
2298
|
+
"hueBleed"
|
|
2299
|
+
]) {
|
|
2300
|
+
const value = out[field];
|
|
2301
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value))) throw new Error(`glaze: ${where}.${field} must be a finite number (got ${showValue(value)}).`);
|
|
2302
|
+
}
|
|
2195
2303
|
const headroom = tuning?.headroom;
|
|
2196
2304
|
if (headroom === void 0) return out;
|
|
2197
2305
|
if (kind === "shadow") throw new Error(`glaze: ${where}.headroom is not supported — a shadow darkens toward black, and nothing goes below black. Headroom is for glows (glowTuning or a glow's tuning).`);
|
|
@@ -2274,7 +2382,8 @@ const OVERRIDE_FIELD_COPY = {
|
|
|
2274
2382
|
* through unchanged.
|
|
2275
2383
|
*/
|
|
2276
2384
|
function copyConfigOverride(override) {
|
|
2277
|
-
if (
|
|
2385
|
+
if (override === null) return void 0;
|
|
2386
|
+
if (typeof override !== "object") return override;
|
|
2278
2387
|
const source = override;
|
|
2279
2388
|
const out = {};
|
|
2280
2389
|
for (const [key, copy] of Object.entries(OVERRIDE_FIELD_COPY)) if (key in source) out[key] = copy(source[key]);
|
|
@@ -2317,7 +2426,7 @@ function configure(config) {
|
|
|
2317
2426
|
const next = {
|
|
2318
2427
|
lightTone: normalizeToneWindowInput(config.lightTone !== void 0 ? config.lightTone : globalConfig.lightTone, "lightTone"),
|
|
2319
2428
|
darkTone: normalizeToneWindowInput(config.darkTone !== void 0 ? config.darkTone : globalConfig.darkTone, "darkTone"),
|
|
2320
|
-
darkDesaturation: normalizeDarkDesaturation(config.darkDesaturation
|
|
2429
|
+
darkDesaturation: normalizeDarkDesaturation(keepIfUndefined(config.darkDesaturation, globalConfig.darkDesaturation)),
|
|
2321
2430
|
states: {
|
|
2322
2431
|
dark: config.states?.dark ?? globalConfig.states.dark,
|
|
2323
2432
|
highContrast: config.states?.highContrast ?? globalConfig.states.highContrast,
|
|
@@ -2333,7 +2442,7 @@ function configure(config) {
|
|
|
2333
2442
|
autoFlip: config.autoFlip ?? globalConfig.autoFlip,
|
|
2334
2443
|
pastel: false,
|
|
2335
2444
|
inferRole: config.inferRole ?? globalConfig.inferRole,
|
|
2336
|
-
contrastLevel: normalizeContrastLevel(config.contrastLevel
|
|
2445
|
+
contrastLevel: normalizeContrastLevel(keepIfUndefined(config.contrastLevel, globalConfig.contrastLevel))
|
|
2337
2446
|
};
|
|
2338
2447
|
flushWarnings(deferred);
|
|
2339
2448
|
configVersion++;
|
|
@@ -2363,7 +2472,7 @@ function mergeConfig(base, override = {}) {
|
|
|
2363
2472
|
const merged = {
|
|
2364
2473
|
lightTone: normalizeToneWindowInput(override.lightTone !== void 0 ? override.lightTone : base.lightTone, "lightTone"),
|
|
2365
2474
|
darkTone: normalizeToneWindowInput(override.darkTone !== void 0 ? override.darkTone : base.darkTone, "darkTone"),
|
|
2366
|
-
darkDesaturation: normalizeDarkDesaturation(override.darkDesaturation
|
|
2475
|
+
darkDesaturation: normalizeDarkDesaturation(keepIfUndefined(override.darkDesaturation, base.darkDesaturation)),
|
|
2367
2476
|
states: { ...base.states },
|
|
2368
2477
|
modes: { ...base.modes },
|
|
2369
2478
|
shadowTuning: normalizeLightingTuning("shadow", override.shadowTuning ?? base.shadowTuning, "shadowTuning"),
|
|
@@ -2371,7 +2480,7 @@ function mergeConfig(base, override = {}) {
|
|
|
2371
2480
|
autoFlip: override.autoFlip ?? base.autoFlip,
|
|
2372
2481
|
pastel: override.pastel ?? false,
|
|
2373
2482
|
inferRole: override.inferRole ?? base.inferRole,
|
|
2374
|
-
contrastLevel: normalizeContrastLevel(override.contrastLevel
|
|
2483
|
+
contrastLevel: normalizeContrastLevel(keepIfUndefined(override.contrastLevel, base.contrastLevel))
|
|
2375
2484
|
};
|
|
2376
2485
|
flushWarnings(deferred);
|
|
2377
2486
|
return merged;
|
|
@@ -2438,7 +2547,7 @@ function assertExportVersion(data, factory) {
|
|
|
2438
2547
|
if (data.version > GLAZE_EXPORT_VERSION) throw new Error(`${factory}: unsupported export version ${data.version} (this library supports version ${GLAZE_EXPORT_VERSION}). Upgrade @tenphi/glaze to load this snapshot.`);
|
|
2439
2548
|
}
|
|
2440
2549
|
/**
|
|
2441
|
-
* Reject a version-1 (Glaze 2) theme snapshot that carries shadow colors or a
|
|
2550
|
+
* Reject a version-1 (Glaze 1.1–2) theme snapshot that carries shadow colors or a
|
|
2442
2551
|
* `shadowTuning`. Glaze 3 changed what shadow `intensity`, `fg` and tuning
|
|
2443
2552
|
* mean, so restoring one would silently re-render every shadow differently.
|
|
2444
2553
|
* Version-1 snapshots without shadows load as before. `fallbackVersion` is the
|
|
@@ -2451,7 +2560,7 @@ function assertNoLegacyShadows(data, factory, fallbackVersion) {
|
|
|
2451
2560
|
const hasTuning = isPlainObject(data.config) && data.config.shadowTuning !== void 0;
|
|
2452
2561
|
if (shadows.length === 0 && !hasTuning) return;
|
|
2453
2562
|
const what = shadows.length > 0 ? `shadow colors (${shadows.map((n) => JSON.stringify(n)).join(", ")})` : "a \"shadowTuning\" config";
|
|
2454
|
-
throw new Error(`${factory}: this snapshot is export version 1 (Glaze 2) and contains ${what}. Glaze 3 changed what shadow "intensity", "fg" and tuning mean, so it would restore with different shadows. Re-author them as described in docs/migration.md ("3.0 — shadows and glows") and export again.`);
|
|
2563
|
+
throw new Error(`${factory}: this snapshot is export version 1 (Glaze 1.1–2) and contains ${what}. Glaze 3 changed what shadow "intensity", "fg" and tuning mean, so it would restore with different shadows. Re-author them as described in docs/migration.md ("3.0 — shadows and glows") and export again.`);
|
|
2455
2564
|
}
|
|
2456
2565
|
/**
|
|
2457
2566
|
* When `kind` is present, it must match the factory. Missing `kind` is
|
|
@@ -2552,7 +2661,8 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2552
2661
|
const regDef = def;
|
|
2553
2662
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2554
2663
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2555
|
-
|
|
2664
|
+
const isStatic = (regDef.mode ?? "auto") === "static";
|
|
2665
|
+
if (isStatic && schemeSeedHue(ctx, "dark") !== ctx.seedHue) return {
|
|
2556
2666
|
hueVar: `var(${prop})`,
|
|
2557
2667
|
inline: false,
|
|
2558
2668
|
declarations: [{
|
|
@@ -2560,12 +2670,13 @@ function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
|
2560
2670
|
value: hueValue(variant.h)
|
|
2561
2671
|
}]
|
|
2562
2672
|
};
|
|
2563
|
-
|
|
2673
|
+
const darkHue = isStatic ? void 0 : regDef.darkHue;
|
|
2674
|
+
if (regDef.hue === void 0 && darkHue === void 0 && regDef.from === void 0) return {
|
|
2564
2675
|
hueVar: baseHueVar,
|
|
2565
2676
|
inline: false,
|
|
2566
2677
|
declarations: []
|
|
2567
2678
|
};
|
|
2568
|
-
const authored = scheme === "dark" ?
|
|
2679
|
+
const authored = scheme === "dark" ? darkHue ?? regDef.hue : regDef.hue;
|
|
2569
2680
|
if (authored === void 0) {
|
|
2570
2681
|
if (regDef.from !== void 0) return {
|
|
2571
2682
|
hueVar: `var(${prop})`,
|
|
@@ -2785,23 +2896,49 @@ function claimValueProps(resolved, prefix, suffix, owners, labels) {
|
|
|
2785
2896
|
* Matches the color functions Glaze itself emits (`rgb()`, `hsl()`, `okhsl()`,
|
|
2786
2897
|
* `okhst()`, `oklch()`) plus their legacy alpha aliases (`rgba()`, `hsla()`).
|
|
2787
2898
|
*
|
|
2788
|
-
* Only numeric components are supported
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
2791
|
-
* (
|
|
2899
|
+
* Only numeric components are supported, plus the CSS keyword `none` (a missing
|
|
2900
|
+
* component, read as 0 as CSS renders it); named colors (`red`) and
|
|
2901
|
+
* relative-color syntax (`from <color> ...`) are out of scope. A hue takes a
|
|
2902
|
+
* bare number (degrees) or `deg` / `turn` / `rad` / `grad`.
|
|
2792
2903
|
*/
|
|
2793
2904
|
const COLOR_FN_RE = /^(rgba?|hsla?|okhsl|okhst|oklch)\(\s*([^)]*)\s*\)$/i;
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2905
|
+
/** One numeric component: a CSS number with an optional unit. */
|
|
2906
|
+
const COMPONENT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([a-z%]*)$/i;
|
|
2907
|
+
/** Degrees per unit of each CSS angle unit. */
|
|
2908
|
+
const ANGLE_UNITS = {
|
|
2909
|
+
"": 1,
|
|
2910
|
+
deg: 1,
|
|
2911
|
+
turn: 360,
|
|
2912
|
+
rad: 180 / Math.PI,
|
|
2913
|
+
grad: .9
|
|
2914
|
+
};
|
|
2915
|
+
function readComponent(raw, input, units) {
|
|
2916
|
+
if (raw.toLowerCase() === "none") return 0;
|
|
2917
|
+
const m = COMPONENT_RE.exec(raw);
|
|
2918
|
+
const scale = m ? units[m[2].toLowerCase()] : void 0;
|
|
2919
|
+
const value = m ? parseFloat(m[1]) * (scale ?? NaN) : NaN;
|
|
2920
|
+
if (!Number.isFinite(value)) throw new Error(`glaze: invalid component "${raw}" in "${input}".`);
|
|
2921
|
+
return value;
|
|
2922
|
+
}
|
|
2923
|
+
/** A number, or a percentage of `percentScale`. */
|
|
2924
|
+
function parseNumberOrPercent(raw, percentScale, input) {
|
|
2925
|
+
return readComponent(raw, input, {
|
|
2926
|
+
"": 1,
|
|
2927
|
+
"%": percentScale / 100
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
/** A hue in degrees. */
|
|
2931
|
+
function parseHue(raw, input) {
|
|
2932
|
+
return readComponent(raw, input, ANGLE_UNITS);
|
|
2797
2933
|
}
|
|
2934
|
+
const clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
2798
2935
|
/**
|
|
2799
2936
|
* Split the body of a CSS color function into its components and detect
|
|
2800
2937
|
* whether an alpha channel was present.
|
|
2801
2938
|
*
|
|
2802
2939
|
* Handles both modern slash syntax (`R G B / A` or `R, G, B / A`) and
|
|
2803
2940
|
* legacy comma syntax (`R, G, B, A`). The alpha value itself is discarded
|
|
2804
|
-
* by the caller
|
|
2941
|
+
* by the caller, with a warning: translucency is a color's `opacity` field.
|
|
2805
2942
|
*/
|
|
2806
2943
|
function splitColorBody(body) {
|
|
2807
2944
|
const slashIdx = body.indexOf("/");
|
|
@@ -2822,8 +2959,13 @@ function splitColorBody(body) {
|
|
|
2822
2959
|
hadAlpha: false
|
|
2823
2960
|
};
|
|
2824
2961
|
}
|
|
2962
|
+
/** Inputs already warned about, so a re-parsed `from` warns once. */
|
|
2963
|
+
const warnedAlpha = /* @__PURE__ */ new Set();
|
|
2825
2964
|
function warnDroppedAlpha(input) {
|
|
2826
|
-
|
|
2965
|
+
if (warnedAlpha.has(input)) return;
|
|
2966
|
+
if (warnedAlpha.size >= 256) warnedAlpha.clear();
|
|
2967
|
+
warnedAlpha.add(input);
|
|
2968
|
+
console.warn(`glaze: alpha component dropped from "${input}"; use the color's \`opacity\` (or a transparent mix) for translucency.`);
|
|
2827
2969
|
}
|
|
2828
2970
|
function parseColorString(input) {
|
|
2829
2971
|
if (input.startsWith("#")) {
|
|
@@ -2843,13 +2985,14 @@ function parseColorString(input) {
|
|
|
2843
2985
|
const { components, hadAlpha } = splitColorBody(m[2].trim());
|
|
2844
2986
|
if (hadAlpha) warnDroppedAlpha(input);
|
|
2845
2987
|
if (components.length !== 3) throw new Error(`glaze: expected 3 components in "${input}".`);
|
|
2988
|
+
const [c0, c1, c2] = components;
|
|
2846
2989
|
switch (fn) {
|
|
2847
2990
|
case "rgb":
|
|
2848
2991
|
case "rgba": {
|
|
2849
2992
|
const [h, s, l] = srgbToOkhsl([
|
|
2850
|
-
parseNumberOrPercent(
|
|
2851
|
-
parseNumberOrPercent(
|
|
2852
|
-
parseNumberOrPercent(
|
|
2993
|
+
clamp01(parseNumberOrPercent(c0, 255, input) / 255),
|
|
2994
|
+
clamp01(parseNumberOrPercent(c1, 255, input) / 255),
|
|
2995
|
+
clamp01(parseNumberOrPercent(c2, 255, input) / 255)
|
|
2853
2996
|
]);
|
|
2854
2997
|
return {
|
|
2855
2998
|
h,
|
|
@@ -2859,7 +3002,7 @@ function parseColorString(input) {
|
|
|
2859
3002
|
}
|
|
2860
3003
|
case "hsl":
|
|
2861
3004
|
case "hsla": {
|
|
2862
|
-
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(
|
|
3005
|
+
const [oh, os, ol] = srgbToOkhsl(hslToSrgb(parseHue(c0, input), clamp01(parseNumberOrPercent(c1, 1, input)), clamp01(parseNumberOrPercent(c2, 1, input))));
|
|
2863
3006
|
return {
|
|
2864
3007
|
h: oh,
|
|
2865
3008
|
s: os,
|
|
@@ -2867,19 +3010,19 @@ function parseColorString(input) {
|
|
|
2867
3010
|
};
|
|
2868
3011
|
}
|
|
2869
3012
|
case "okhsl": return {
|
|
2870
|
-
h:
|
|
2871
|
-
s: parseNumberOrPercent(
|
|
2872
|
-
l: parseNumberOrPercent(
|
|
3013
|
+
h: parseHue(c0, input),
|
|
3014
|
+
s: clamp01(parseNumberOrPercent(c1, 1, input)),
|
|
3015
|
+
l: clamp01(parseNumberOrPercent(c2, 1, input))
|
|
2873
3016
|
};
|
|
2874
3017
|
case "okhst": return okhstToOkhsl({
|
|
2875
|
-
h:
|
|
2876
|
-
s: parseNumberOrPercent(
|
|
2877
|
-
t: parseNumberOrPercent(
|
|
3018
|
+
h: parseHue(c0, input),
|
|
3019
|
+
s: clamp01(parseNumberOrPercent(c1, 1, input)),
|
|
3020
|
+
t: clamp01(parseNumberOrPercent(c2, 1, input))
|
|
2878
3021
|
});
|
|
2879
3022
|
case "oklch": {
|
|
2880
|
-
const L = parseNumberOrPercent(
|
|
2881
|
-
const C = parseNumberOrPercent(
|
|
2882
|
-
const hRad =
|
|
3023
|
+
const L = clamp01(parseNumberOrPercent(c0, 1, input));
|
|
3024
|
+
const C = Math.max(0, parseNumberOrPercent(c1, .4, input));
|
|
3025
|
+
const hRad = parseHue(c2, input) * Math.PI / 180;
|
|
2883
3026
|
const [h, s, l] = oklabToOkhsl([
|
|
2884
3027
|
L,
|
|
2885
3028
|
C * Math.cos(hRad),
|
|
@@ -2901,8 +3044,16 @@ function parseColorString(input) {
|
|
|
2901
3044
|
function validateOkhslColor(value) {
|
|
2902
3045
|
const { h, s, l } = value;
|
|
2903
3046
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) throw new Error("glaze.color: OkhslColor h/s/l must be finite numbers.");
|
|
3047
|
+
assertNonNegative("OkhslColor", {
|
|
3048
|
+
s,
|
|
3049
|
+
l
|
|
3050
|
+
});
|
|
2904
3051
|
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)?");
|
|
2905
3052
|
}
|
|
3053
|
+
/** Reject a negative 0–1 component, naming it: no color has one. */
|
|
3054
|
+
function assertNonNegative(kind, fields) {
|
|
3055
|
+
for (const [key, n] of Object.entries(fields)) if (n < 0) throw new Error(`glaze.color: ${kind} ${key} must be 0 or more (got ${n}).`);
|
|
3056
|
+
}
|
|
2906
3057
|
/** Validate a user-supplied `{ r, g, b }` object in 0–255. */
|
|
2907
3058
|
function validateRgbColor(value) {
|
|
2908
3059
|
for (const key of [
|
|
@@ -2918,6 +3069,10 @@ function validateRgbColor(value) {
|
|
|
2918
3069
|
function validateOklchColor(value) {
|
|
2919
3070
|
const { l, c, h } = value;
|
|
2920
3071
|
if (!Number.isFinite(l) || !Number.isFinite(c) || !Number.isFinite(h)) throw new Error("glaze.color: OklchColor l/c/h must be finite numbers.");
|
|
3072
|
+
assertNonNegative("OklchColor", {
|
|
3073
|
+
l,
|
|
3074
|
+
c
|
|
3075
|
+
});
|
|
2921
3076
|
if (l > 1.5 || c > 1.5) throw new Error("glaze.color: OklchColor l/c must be in 0–1 range (matching oklch() strings).");
|
|
2922
3077
|
}
|
|
2923
3078
|
function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
@@ -2933,6 +3088,36 @@ function oklchComponentsToOkhsl(l, c, hDeg) {
|
|
|
2933
3088
|
l: outL
|
|
2934
3089
|
};
|
|
2935
3090
|
}
|
|
3091
|
+
/**
|
|
3092
|
+
* A detached copy of a color value. A value object is read by property access
|
|
3093
|
+
* once its shape is known, so a getter, an inherited field or a proxy copies
|
|
3094
|
+
* like a plain field (a spread would drop the first two). Strings, and shapes
|
|
3095
|
+
* the parser rejects anyway, pass through.
|
|
3096
|
+
*/
|
|
3097
|
+
function copyColorValue(value) {
|
|
3098
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
|
|
3099
|
+
const v = value;
|
|
3100
|
+
if (isRgbColorObject(value)) return {
|
|
3101
|
+
r: v.r,
|
|
3102
|
+
g: v.g,
|
|
3103
|
+
b: v.b
|
|
3104
|
+
};
|
|
3105
|
+
if (isOklchColorObject(value)) return {
|
|
3106
|
+
l: v.l,
|
|
3107
|
+
c: v.c,
|
|
3108
|
+
h: v.h
|
|
3109
|
+
};
|
|
3110
|
+
if (isOkhstColorObject(value)) return {
|
|
3111
|
+
h: v.h,
|
|
3112
|
+
s: v.s,
|
|
3113
|
+
t: v.t
|
|
3114
|
+
};
|
|
3115
|
+
return {
|
|
3116
|
+
h: v.h,
|
|
3117
|
+
s: v.s,
|
|
3118
|
+
l: v.l
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
2936
3121
|
function isRgbColorObject(value) {
|
|
2937
3122
|
return "r" in value && "g" in value && "b" in value;
|
|
2938
3123
|
}
|
|
@@ -2946,12 +3131,18 @@ function isOkhstColorObject(value) {
|
|
|
2946
3131
|
function validateOkhstColor(value) {
|
|
2947
3132
|
const { h, s, t } = value;
|
|
2948
3133
|
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(t)) throw new Error("glaze.color: OkhstColor h/s/t must be finite numbers.");
|
|
3134
|
+
assertNonNegative("OkhstColor", {
|
|
3135
|
+
s,
|
|
3136
|
+
t
|
|
3137
|
+
});
|
|
2949
3138
|
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)?");
|
|
2950
3139
|
}
|
|
2951
3140
|
/**
|
|
2952
3141
|
* Extract an OKHSL color from any `GlazeColorValue` form. Also used by
|
|
2953
3142
|
* `glaze.shadow()` / `glaze.glow()` so all their inputs (hex, color
|
|
2954
|
-
* functions, OKHSL, literal objects) go through one parser.
|
|
3143
|
+
* functions, OKHSL, literal objects) go through one parser. A value object's
|
|
3144
|
+
* 0–1 field that passed its check above 1 (up to 1.5) clamps to 1, as the
|
|
3145
|
+
* same value in a color function does.
|
|
2955
3146
|
*/
|
|
2956
3147
|
function extractOkhslFromValue(value) {
|
|
2957
3148
|
if (typeof value === "string") return parseColorString(value);
|
|
@@ -2971,14 +3162,22 @@ function extractOkhslFromValue(value) {
|
|
|
2971
3162
|
}
|
|
2972
3163
|
if (isOklchColorObject(value)) {
|
|
2973
3164
|
validateOklchColor(value);
|
|
2974
|
-
return oklchComponentsToOkhsl(value.l, value.c, value.h);
|
|
3165
|
+
return oklchComponentsToOkhsl(clamp01(value.l), value.c, value.h);
|
|
2975
3166
|
}
|
|
2976
3167
|
if (isOkhstColorObject(value)) {
|
|
2977
3168
|
validateOkhstColor(value);
|
|
2978
|
-
return okhstToOkhsl(
|
|
3169
|
+
return okhstToOkhsl({
|
|
3170
|
+
h: value.h,
|
|
3171
|
+
s: clamp01(value.s),
|
|
3172
|
+
t: clamp01(value.t)
|
|
3173
|
+
});
|
|
2979
3174
|
}
|
|
2980
3175
|
validateOkhslColor(value);
|
|
2981
|
-
return
|
|
3176
|
+
return {
|
|
3177
|
+
h: value.h,
|
|
3178
|
+
s: clamp01(value.s),
|
|
3179
|
+
l: clamp01(value.l)
|
|
3180
|
+
};
|
|
2982
3181
|
}
|
|
2983
3182
|
|
|
2984
3183
|
//#endregion
|
|
@@ -3142,7 +3341,10 @@ function oppositeRole(role) {
|
|
|
3142
3341
|
*
|
|
3143
3342
|
* `validateColorDefs` rejects bad references (missing / shadow- or
|
|
3144
3343
|
* glow-referencing / base/contrast/tone mismatches — a relative tone in either
|
|
3145
|
-
* entry of an HC pair needs a base)
|
|
3344
|
+
* entry of an HC pair needs a base), a malformed `contrast` spec (a misspelled
|
|
3345
|
+
* preset, a non-finite number, a bad shape), a numeric field the resolver can't
|
|
3346
|
+
* place (non-finite, a malformed `'+N'`, an opacity outside 0–1), and bad
|
|
3347
|
+
* per-color lighting tuning (a
|
|
3146
3348
|
* `headroom` on a shadow; a glow `headroom` that is not a finite number ≥ 0,
|
|
3147
3349
|
* with one above the maximum warned about — once every def has passed — and
|
|
3148
3350
|
* clamped at resolve), and detects cycles before the resolver runs. `topoSort` orders defs so each color is
|
|
@@ -3168,6 +3370,56 @@ function hasRelativeTone(tone) {
|
|
|
3168
3370
|
if (tone === void 0) return false;
|
|
3169
3371
|
return !isAbsoluteTone(pairNormal(tone)) || !isAbsoluteTone(pairHC(tone));
|
|
3170
3372
|
}
|
|
3373
|
+
const isWcagValue = (value) => typeof value === "number" && Number.isFinite(value) || isContrastPreset(value);
|
|
3374
|
+
const isApcaValue = (value) => typeof value === "number" && Number.isFinite(value) || isApcaPreset(value);
|
|
3375
|
+
/**
|
|
3376
|
+
* A value, or a `[normal, highContrast]` pair of two, that passes `isValue`.
|
|
3377
|
+
* Both entries are read by index: `every` would skip a hole (`[, 7]`).
|
|
3378
|
+
*/
|
|
3379
|
+
function isPairOf(value, isValue) {
|
|
3380
|
+
return Array.isArray(value) ? value.length === 2 && isValue(value[0]) && isValue(value[1]) : isValue(value);
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* A WCAG value, or an object with exactly one of `wcag` / `apca` set. A key
|
|
3384
|
+
* holding `undefined` is unset, as everywhere else; other keys are ignored.
|
|
3385
|
+
*/
|
|
3386
|
+
function isContrastSpec(spec) {
|
|
3387
|
+
if (isWcagValue(spec)) return true;
|
|
3388
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec)) return false;
|
|
3389
|
+
const { wcag, apca } = spec;
|
|
3390
|
+
if (wcag === void 0 === (apca === void 0)) return false;
|
|
3391
|
+
return wcag !== void 0 ? isPairOf(wcag, isWcagValue) : isPairOf(apca, isApcaValue);
|
|
3392
|
+
}
|
|
3393
|
+
const SIGNED_NUMBER_RE = /^[+-](?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
3394
|
+
const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
3395
|
+
/**
|
|
3396
|
+
* A relative `'+N'` / `'-N'` string with a finite `N`. Whitespace around it
|
|
3397
|
+
* is allowed, as the resolver's `parseFloat` reads past it. Also checks a
|
|
3398
|
+
* token's structured `darkHue`.
|
|
3399
|
+
*/
|
|
3400
|
+
const isRelativeValue = (value) => typeof value === "string" && SIGNED_NUMBER_RE.test(value.trim()) && Number.isFinite(parseFloat(value));
|
|
3401
|
+
const isToneValue = (value) => isFiniteNumber(value) || value === "max" || value === "min" || isRelativeValue(value);
|
|
3402
|
+
const isHueValue = (value) => isFiniteNumber(value) || isRelativeValue(value);
|
|
3403
|
+
const isOpacity = (value) => isFiniteNumber(value) && value >= 0 && value <= 1;
|
|
3404
|
+
/**
|
|
3405
|
+
* Reject a numeric field the resolver can't place: `NaN`, `Infinity`, a
|
|
3406
|
+
* relative string that isn't `'+N'` / `'-N'` (`'+-5'`, `'+abc'`), an opacity
|
|
3407
|
+
* outside 0–1. Each would otherwise reach the output as `NaN`.
|
|
3408
|
+
*/
|
|
3409
|
+
function assertField(name, field, value, isValid) {
|
|
3410
|
+
if (value === void 0 || isValid(value)) return;
|
|
3411
|
+
throw new Error(`glaze: color "${name}" has an invalid "${field}" ${showValue(value)}.`);
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* Reject a malformed `contrast`: a misspelled preset, a non-finite number, an
|
|
3415
|
+
* object without exactly one of `wcag` / `apca`, or a pair without exactly two
|
|
3416
|
+
* entries. The solver would read each as no target at all and pin the color
|
|
3417
|
+
* to the scheme's extreme without a word.
|
|
3418
|
+
*/
|
|
3419
|
+
function assertValidContrast(name, contrast) {
|
|
3420
|
+
if (contrast === void 0 || isPairOf(contrast, isContrastSpec)) return;
|
|
3421
|
+
throw new Error(`glaze: color "${name}" has an invalid "contrast" ${showValue(contrast)}. Use a WCAG ratio or 'AA' / 'AAA' / 'AA-large' / 'AAA-large', { wcag: … }, { apca: … }, or a [normal, highContrast] pair of these.`);
|
|
3422
|
+
}
|
|
3171
3423
|
/**
|
|
3172
3424
|
* Reject a `contrast` pair whose two entries measure in different metrics.
|
|
3173
3425
|
*
|
|
@@ -3201,11 +3453,15 @@ function validateColorDefs(defs, externalBases) {
|
|
|
3201
3453
|
const kind = def.type;
|
|
3202
3454
|
for (const field of ["bg", "fg"]) {
|
|
3203
3455
|
const ref = def[field];
|
|
3204
|
-
if (ref === void 0)
|
|
3456
|
+
if (ref === void 0) {
|
|
3457
|
+
if (field === "fg") continue;
|
|
3458
|
+
throw new Error(`glaze: ${kind} "${name}" is missing "bg".`);
|
|
3459
|
+
}
|
|
3205
3460
|
if (!allNames.has(ref)) throw new Error(`glaze: ${kind} "${name}" references non-existent ${field} "${ref}".`);
|
|
3206
3461
|
const refKind = overlayKindOf(defs, ref);
|
|
3207
3462
|
if (refKind !== void 0) throw new Error(`glaze: ${kind} "${name}" ${field} "${ref}" references a ${refKind} color.`);
|
|
3208
3463
|
}
|
|
3464
|
+
assertField(name, "intensity", def.intensity, (v) => isPairOf(v, isFiniteNumber));
|
|
3209
3465
|
normalizeLightingTuning(kind, def.tuning, `${kind} "${name}" tuning`, deferred);
|
|
3210
3466
|
continue;
|
|
3211
3467
|
}
|
|
@@ -3217,10 +3473,19 @@ function validateColorDefs(defs, externalBases) {
|
|
|
3217
3473
|
const refKind = overlayKindOf(defs, ref);
|
|
3218
3474
|
if (refKind !== void 0) throw new Error(`glaze: mix "${name}" ${field} "${ref}" references a ${refKind} color.`);
|
|
3219
3475
|
}
|
|
3476
|
+
assertField(name, "value", def.value, (v) => isPairOf(v, isFiniteNumber));
|
|
3477
|
+
assertValidContrast(name, def.contrast);
|
|
3220
3478
|
assertConsistentContrastMetric(name, def.contrast);
|
|
3221
3479
|
continue;
|
|
3222
3480
|
}
|
|
3223
3481
|
const regDef = def;
|
|
3482
|
+
assertField(name, "tone", regDef.tone, (v) => isPairOf(v, isToneValue));
|
|
3483
|
+
assertField(name, "hue", regDef.hue, isHueValue);
|
|
3484
|
+
assertField(name, "darkHue", regDef.darkHue, isHueValue);
|
|
3485
|
+
assertField(name, "saturation", regDef.saturation, isFiniteNumber);
|
|
3486
|
+
assertField(name, "darkSaturation", regDef.darkSaturation, isFiniteNumber);
|
|
3487
|
+
assertField(name, "opacity", regDef.opacity, isOpacity);
|
|
3488
|
+
assertValidContrast(name, regDef.contrast);
|
|
3224
3489
|
assertConsistentContrastMetric(name, regDef.contrast);
|
|
3225
3490
|
if (regDef.from !== void 0) try {
|
|
3226
3491
|
extractOkhslFromValue(regDef.from);
|
|
@@ -3320,16 +3585,43 @@ function topoSort(defs) {
|
|
|
3320
3585
|
* Variants are stored in OKHST: `h` / `s` are OKHSL hue/saturation and
|
|
3321
3586
|
* `t` is the canonical contrast-uniform tone (0–1, reference eps). The
|
|
3322
3587
|
* resolver works in tone for regular colors and converts to/from OKHSL
|
|
3323
|
-
* lightness only at the mix / shadow / glow and luminance edges. A
|
|
3324
|
-
*
|
|
3325
|
-
*
|
|
3326
|
-
*
|
|
3327
|
-
*
|
|
3588
|
+
* lightness only at the mix / shadow / glow and luminance edges. A mix reads
|
|
3589
|
+
* a base or target under another `pastel` flag in its own gamut, through the
|
|
3590
|
+
* color that endpoint paints, and measures its contrast floor on the color it
|
|
3591
|
+
* emits. A glow whose step crosses white with `headroom` above 0 also carries
|
|
3592
|
+
* `hdr` — its opaque base in OKHST plus an exposure in stops — and swaps the
|
|
3593
|
+
* SDR shortfall warning for one that fires only when the headroom cap falls
|
|
3594
|
+
* short. HDR values never feed another color: nothing may build on a glow.
|
|
3328
3595
|
*
|
|
3329
3596
|
* Every function receives a single `GlazeConfigResolved` so the full
|
|
3330
3597
|
* per-instance config (including overrides) is available without
|
|
3331
3598
|
* re-reading the global singleton mid-resolve.
|
|
3332
3599
|
*/
|
|
3600
|
+
/**
|
|
3601
|
+
* Defs anchored at their authored tone whatever their mode (see `darkAnchor`).
|
|
3602
|
+
* A `glaze.color()` token's hidden seed is `static` only so that its token's
|
|
3603
|
+
* `contrast` floor measures against the literal input; a delta on it still
|
|
3604
|
+
* steps in dark like one on an `auto` color authored at that tone.
|
|
3605
|
+
*/
|
|
3606
|
+
const authoredToneAnchors = /* @__PURE__ */ new WeakSet();
|
|
3607
|
+
/**
|
|
3608
|
+
* Anchor `def` at its authored tone in dark, whatever its mode (internal, for
|
|
3609
|
+
* a token's hidden seed; see `authoredToneAnchors`).
|
|
3610
|
+
*/
|
|
3611
|
+
function anchorAtAuthoredTone(def) {
|
|
3612
|
+
authoredToneAnchors.add(def);
|
|
3613
|
+
return def;
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* The base of a tone-less, contrast-less `auto` color, which takes its base's
|
|
3617
|
+
* dark anchor as is; `undefined` for any other def.
|
|
3618
|
+
*/
|
|
3619
|
+
function aliasBaseOf(def) {
|
|
3620
|
+
if (def === void 0 || isMixDef(def) || isLightingDef(def)) return;
|
|
3621
|
+
const regDef = def;
|
|
3622
|
+
if ((regDef.mode ?? "auto") !== "auto" || regDef.contrast !== void 0 || regDef.tone !== void 0 || regDef.from !== void 0) return;
|
|
3623
|
+
return regDef.base;
|
|
3624
|
+
}
|
|
3333
3625
|
function getSchemeVariant(color, isDark, isHighContrast) {
|
|
3334
3626
|
if (isDark && isHighContrast) return color.darkContrast;
|
|
3335
3627
|
if (isDark) return color.dark;
|
|
@@ -3550,11 +3842,11 @@ function readsStoredLightTone(isHighContrast, config) {
|
|
|
3550
3842
|
* Where a step of `delta` from `anchor` lands on the authored scale: clamped to
|
|
3551
3843
|
* `0–100`, widened to reach an anchor past the dark window (a base a contrast
|
|
3552
3844
|
* floor, an extreme's replay, or its own fixed / static placement put there).
|
|
3553
|
-
* A step back toward the scale then
|
|
3554
|
-
*
|
|
3555
|
-
*
|
|
3556
|
-
*
|
|
3557
|
-
*
|
|
3845
|
+
* A step back toward the scale then moves by the full delta, and one that
|
|
3846
|
+
* points further out stops at the anchor — the base itself — instead of
|
|
3847
|
+
* snapping back to the window's edge. Either way a step stops at the window's
|
|
3848
|
+
* edge or at its base, whichever is further out, so the result moves
|
|
3849
|
+
* continuously as the base crosses the edge.
|
|
3558
3850
|
*/
|
|
3559
3851
|
function stepFromAnchor(anchor, delta) {
|
|
3560
3852
|
return clamp(anchor + delta, Math.min(0, anchor), Math.max(100, anchor));
|
|
@@ -3586,6 +3878,9 @@ function stepFromAnchor(anchor, delta) {
|
|
|
3586
3878
|
* is this pass's. A base under another dark window — or one with no hook — is
|
|
3587
3879
|
* read back through this pass's dark window, so a `'+0'` lands on it and the
|
|
3588
3880
|
* step is this window's scale times the delta.
|
|
3881
|
+
* - A token's hidden seed (`anchorAtAuthoredTone`) is `static`, but returns its
|
|
3882
|
+
* tone as authored, like an `auto` color placed by a tone, so a delta on it
|
|
3883
|
+
* inverts in dark as it would from the same color in a theme.
|
|
3589
3884
|
*
|
|
3590
3885
|
* In high contrast and at `contrastLevel: 100` at the reference light eps
|
|
3591
3886
|
* (`readsStoredLightTone`), a light-side color's light tone *is* its authored
|
|
@@ -3595,14 +3890,29 @@ function stepFromAnchor(anchor, delta) {
|
|
|
3595
3890
|
*
|
|
3596
3891
|
* Memoized per resolve and pass on `ctx.anchors`, so a long chain of deltas
|
|
3597
3892
|
* resolves in linear time without deep recursion: the dark passes run in topo
|
|
3598
|
-
* order, so a base's anchor is cached before its dependents ask for it.
|
|
3893
|
+
* order, so a base's anchor is cached before its dependents ask for it. A
|
|
3894
|
+
* tone-less alias never asks for its own, so a chain of aliases is walked in a
|
|
3895
|
+
* loop instead.
|
|
3599
3896
|
*/
|
|
3600
3897
|
function darkAnchor(name, ctx, isHighContrast) {
|
|
3601
3898
|
const memo = ctx.anchors[isHighContrast ? 1 : 0];
|
|
3602
3899
|
const cached = memo.get(name);
|
|
3603
3900
|
if (cached) return cached;
|
|
3901
|
+
if (aliasBaseOf(ctx.defs[name]) !== void 0) {
|
|
3902
|
+
const chain = [];
|
|
3903
|
+
let target = name;
|
|
3904
|
+
for (let base = aliasBaseOf(ctx.defs[target]); base !== void 0 && !memo.has(target); base = aliasBaseOf(ctx.defs[target])) {
|
|
3905
|
+
chain.push(target);
|
|
3906
|
+
target = base;
|
|
3907
|
+
}
|
|
3908
|
+
let anchor = darkAnchor(target, ctx, isHighContrast);
|
|
3909
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
3910
|
+
anchor = withStoredLightTone(chain[i], anchor, ctx, isHighContrast);
|
|
3911
|
+
memo.set(chain[i], anchor);
|
|
3912
|
+
}
|
|
3913
|
+
return anchor;
|
|
3914
|
+
}
|
|
3604
3915
|
const color = ctx.resolved.get(name);
|
|
3605
|
-
const lightTone = getSchemeVariant(color, false, isHighContrast).t * 100;
|
|
3606
3916
|
const fromDark = (config) => ({
|
|
3607
3917
|
tone: unmapDarkTone(getSchemeVariant(color, true, isHighContrast).t * 100, "auto", isHighContrast, config),
|
|
3608
3918
|
lightSide: false
|
|
@@ -3622,7 +3932,7 @@ function darkAnchor(name, ctx, isHighContrast) {
|
|
|
3622
3932
|
const config = configForColor(regDef, ctx.config);
|
|
3623
3933
|
const rawTone = regDef.tone ?? fromSeed(regDef)?.tone;
|
|
3624
3934
|
const parsed = rawTone === void 0 ? void 0 : passTone(rawTone, isHighContrast, config);
|
|
3625
|
-
if (mode !== "auto" || regDef.contrast !== void 0) anchor = fromDark(config);
|
|
3935
|
+
if (mode !== "auto" && !authoredToneAnchors.has(regDef) || regDef.contrast !== void 0) anchor = fromDark(config);
|
|
3626
3936
|
else if (parsed === void 0) anchor = darkAnchor(regDef.base, ctx, isHighContrast);
|
|
3627
3937
|
else if (parsed.kind === "relative") {
|
|
3628
3938
|
const base = darkAnchor(regDef.base, ctx, isHighContrast);
|
|
@@ -3638,14 +3948,22 @@ function darkAnchor(name, ctx, isHighContrast) {
|
|
|
3638
3948
|
lightSide: true
|
|
3639
3949
|
};
|
|
3640
3950
|
}
|
|
3641
|
-
|
|
3642
|
-
tone: lightTone,
|
|
3643
|
-
lightSide: true
|
|
3644
|
-
};
|
|
3951
|
+
anchor = withStoredLightTone(name, anchor, ctx, isHighContrast);
|
|
3645
3952
|
memo.set(name, anchor);
|
|
3646
3953
|
return anchor;
|
|
3647
3954
|
}
|
|
3648
3955
|
/**
|
|
3956
|
+
* A light-side anchor read as the color's stored light tone where that is its
|
|
3957
|
+
* authored tone (`readsStoredLightTone`; see `darkAnchor`).
|
|
3958
|
+
*/
|
|
3959
|
+
function withStoredLightTone(name, anchor, ctx, isHighContrast) {
|
|
3960
|
+
if (!anchor.lightSide || !readsStoredLightTone(isHighContrast, ctx.config)) return anchor;
|
|
3961
|
+
return {
|
|
3962
|
+
tone: getSchemeVariant(ctx.resolved.get(name), false, isHighContrast).t * 100,
|
|
3963
|
+
lightSide: true
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3649
3967
|
* A resolved color's dark-delta anchor for one pass (see `darkAnchor`), read
|
|
3650
3968
|
* after its resolve. This is what a `glaze.color()` token hands the resolver
|
|
3651
3969
|
* (with its config, and cached per pass) when another token links to it as a
|
|
@@ -3797,6 +4115,10 @@ function resolveLightingForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
3797
4115
|
function okhslVariantToLinearRgb(v, pastel) {
|
|
3798
4116
|
return okhslToLinearSrgb(v.h, v.s, v.l, pastel);
|
|
3799
4117
|
}
|
|
4118
|
+
/** Saturation at or below which a mix endpoint counts as gray (no hue). */
|
|
4119
|
+
const MIX_SAT_EPSILON = 1e-6;
|
|
4120
|
+
/** Lightness margin from 0 and 1 for re-reading a mix endpoint's saturation. */
|
|
4121
|
+
const MIX_EXTREME_L = 1e-4;
|
|
3800
4122
|
/**
|
|
3801
4123
|
* Resolve hue for OKHSL mixing, handling achromatic colors.
|
|
3802
4124
|
* When one color has no saturation, its hue is meaningless —
|
|
@@ -3804,9 +4126,8 @@ function okhslVariantToLinearRgb(v, pastel) {
|
|
|
3804
4126
|
* color-mix "missing component" behavior).
|
|
3805
4127
|
*/
|
|
3806
4128
|
function mixHue(base, target, t) {
|
|
3807
|
-
const
|
|
3808
|
-
const
|
|
3809
|
-
const targetHasSat = target.s > SAT_EPSILON;
|
|
4129
|
+
const baseHasSat = base.s > MIX_SAT_EPSILON;
|
|
4130
|
+
const targetHasSat = target.s > MIX_SAT_EPSILON;
|
|
3810
4131
|
if (baseHasSat && targetHasSat) return circularLerp(base.h, target.h, t);
|
|
3811
4132
|
if (targetHasSat) return target.h;
|
|
3812
4133
|
return base.h;
|
|
@@ -3818,18 +4139,20 @@ function linearSrgbLerp(base, target, t) {
|
|
|
3818
4139
|
base[2] + (target[2] - base[2]) * t
|
|
3819
4140
|
];
|
|
3820
4141
|
}
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
4142
|
+
/**
|
|
4143
|
+
* Linear sRGB → OKHSL in one saturation space. Channels are clamped to sRGB
|
|
4144
|
+
* first, and a pastel read caps saturation at the pastel limit (same hue and
|
|
4145
|
+
* lightness), so the result is a color that space can paint.
|
|
4146
|
+
*/
|
|
4147
|
+
function linearRgbToOkhsl(rgb, pastel) {
|
|
4148
|
+
const [h, s, l] = srgbToOkhsl(toDisplayGamma(rgb), pastel);
|
|
4149
|
+
return {
|
|
3828
4150
|
h,
|
|
3829
4151
|
s,
|
|
3830
4152
|
l,
|
|
3831
|
-
alpha: 1
|
|
3832
|
-
|
|
4153
|
+
alpha: 1,
|
|
4154
|
+
pastel
|
|
4155
|
+
};
|
|
3833
4156
|
}
|
|
3834
4157
|
function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
3835
4158
|
const baseResolved = ctx.resolved.get(def.base);
|
|
@@ -3841,50 +4164,64 @@ function resolveMixForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
3841
4164
|
const space = def.space ?? "okhsl";
|
|
3842
4165
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
3843
4166
|
const pastel = def.pastel ?? ctx.config.pastel;
|
|
3844
|
-
const
|
|
3845
|
-
const
|
|
4167
|
+
const basePastel = baseVariant.pastel ?? ctx.config.pastel;
|
|
4168
|
+
const targetPastel = targetVariant.pastel ?? ctx.config.pastel;
|
|
4169
|
+
const baseLinear = okhslVariantToLinearRgb(baseVariant, basePastel);
|
|
4170
|
+
const targetLinear = okhslVariantToLinearRgb(targetVariant, targetPastel);
|
|
4171
|
+
const inMixSpace = (v, own) => {
|
|
4172
|
+
if (own === pastel || v.s <= MIX_SAT_EPSILON) return v;
|
|
4173
|
+
const l = clamp(v.l, MIX_EXTREME_L, 1 - MIX_EXTREME_L);
|
|
4174
|
+
const s = oklabToOkhsl(okhslToOklab(v.h, v.s, l, own), pastel)[1];
|
|
4175
|
+
return {
|
|
4176
|
+
...v,
|
|
4177
|
+
s,
|
|
4178
|
+
pastel
|
|
4179
|
+
};
|
|
4180
|
+
};
|
|
4181
|
+
let mixAt;
|
|
4182
|
+
let paintAt;
|
|
4183
|
+
if (blend === "transparent") {
|
|
4184
|
+
const overlay = inMixSpace(targetVariant, targetPastel);
|
|
4185
|
+
const baseGamma = toDisplayGamma(baseLinear);
|
|
4186
|
+
const overlayGamma = toDisplayGamma(okhslVariantToLinearRgb(overlay, pastel));
|
|
4187
|
+
mixAt = (v) => ({
|
|
4188
|
+
...overlay,
|
|
4189
|
+
alpha: clamp(v, 0, 1)
|
|
4190
|
+
});
|
|
4191
|
+
paintAt = (v) => compositeOverGamma(baseGamma, overlayGamma, v);
|
|
4192
|
+
} else if (space === "srgb") {
|
|
4193
|
+
const lerpAt = (v) => linearSrgbLerp(baseLinear, targetLinear, v);
|
|
4194
|
+
mixAt = (v) => linearRgbToOkhsl(lerpAt(v), pastel);
|
|
4195
|
+
paintAt = pastel ? (v) => {
|
|
4196
|
+
const mixed = mixAt(v);
|
|
4197
|
+
return mixed.s < 1 ? lerpAt(v) : okhslVariantToLinearRgb(mixed, true);
|
|
4198
|
+
} : lerpAt;
|
|
4199
|
+
} else {
|
|
4200
|
+
const from = inMixSpace(baseVariant, basePastel);
|
|
4201
|
+
const to = inMixSpace(targetVariant, targetPastel);
|
|
4202
|
+
mixAt = (v) => ({
|
|
4203
|
+
h: mixHue(from, to, v),
|
|
4204
|
+
s: clamp(from.s + (to.s - from.s) * v, 0, 1),
|
|
4205
|
+
l: clamp(from.l + (to.l - from.l) * v, 0, 1),
|
|
4206
|
+
alpha: 1
|
|
4207
|
+
});
|
|
4208
|
+
paintAt = (v) => okhslVariantToLinearRgb(mixAt(v), pastel);
|
|
4209
|
+
}
|
|
3846
4210
|
if (def.contrast !== void 0) {
|
|
3847
4211
|
const resolvedContrast = resolveContrastSpec(def.contrast, isHighContrast, ctx.config, polarity);
|
|
3848
4212
|
const metric = resolvedContrast.metric;
|
|
3849
|
-
let luminanceAt;
|
|
3850
|
-
if (blend === "transparent") {
|
|
3851
|
-
const baseGamma = toDisplayGamma(baseLinear);
|
|
3852
|
-
const targetGamma = toDisplayGamma(targetLinear);
|
|
3853
|
-
luminanceAt = (v) => metricLuminance(metric, compositeOverGamma(baseGamma, targetGamma, v));
|
|
3854
|
-
} else if (space === "srgb") luminanceAt = (v) => metricLuminance(metric, linearSrgbLerp(baseLinear, targetLinear, v));
|
|
3855
|
-
else luminanceAt = (v) => {
|
|
3856
|
-
return metricLuminance(metric, okhslToLinearSrgb(mixHue(baseVariant, targetVariant, v), baseVariant.s + (targetVariant.s - baseVariant.s) * v, baseVariant.l + (targetVariant.l - baseVariant.l) * v, pastel));
|
|
3857
|
-
};
|
|
3858
4213
|
const result = findValueForMixContrast({
|
|
3859
4214
|
preferredValue: t,
|
|
3860
4215
|
baseLinearRgb: baseLinear,
|
|
3861
4216
|
contrast: resolvedContrast,
|
|
3862
|
-
luminanceAtValue:
|
|
4217
|
+
luminanceAtValue: (v) => metricLuminance(metric, paintAt(v)),
|
|
3863
4218
|
flip: ctx.config.autoFlip
|
|
3864
4219
|
});
|
|
3865
4220
|
t = result.value;
|
|
3866
|
-
if (!result.met) warnMixContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
4221
|
+
if (!result.met) warnMixContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast, pastel && !targetPastel && metricScore(metric, metricLuminance(metric, targetLinear), metricLuminance(metric, baseLinear), resolvedContrast.polarity) >= resolvedContrast.target);
|
|
3867
4222
|
}
|
|
3868
|
-
if (blend === "transparent") return {
|
|
3869
|
-
...toToneVariant({
|
|
3870
|
-
h: targetVariant.h,
|
|
3871
|
-
s: targetVariant.s,
|
|
3872
|
-
l: targetVariant.l,
|
|
3873
|
-
alpha: clamp(t, 0, 1)
|
|
3874
|
-
}),
|
|
3875
|
-
pastel
|
|
3876
|
-
};
|
|
3877
|
-
if (space === "srgb") return {
|
|
3878
|
-
...linearRgbToToneVariant(linearSrgbLerp(baseLinear, targetLinear, t), pastel),
|
|
3879
|
-
pastel
|
|
3880
|
-
};
|
|
3881
4223
|
return {
|
|
3882
|
-
...toToneVariant(
|
|
3883
|
-
h: mixHue(baseVariant, targetVariant, t),
|
|
3884
|
-
s: clamp(baseVariant.s + (targetVariant.s - baseVariant.s) * t, 0, 1),
|
|
3885
|
-
l: clamp(baseVariant.l + (targetVariant.l - baseVariant.l) * t, 0, 1),
|
|
3886
|
-
alpha: 1
|
|
3887
|
-
}),
|
|
4224
|
+
...toToneVariant(mixAt(t)),
|
|
3888
4225
|
pastel
|
|
3889
4226
|
};
|
|
3890
4227
|
}
|
|
@@ -4054,8 +4391,8 @@ function resolveAllColors(seed, defs, config, externalBases, externalAnchors) {
|
|
|
4054
4391
|
* Output formatting for resolved color maps.
|
|
4055
4392
|
*
|
|
4056
4393
|
* Owns the CSS-string formatter dispatch table (`okhsl` / `rgb` / `hsl` /
|
|
4057
|
-
* `oklch`), the HDR glow writer (`formatHdrVariant
|
|
4058
|
-
* and the token-map shapes Glaze emits:
|
|
4394
|
+
* `oklch`), the HDR glow writer (`formatHdrVariant`: extended `color(srgb …)`,
|
|
4395
|
+
* or `color(srgb-linear …)` on request), and the token-map shapes Glaze emits:
|
|
4059
4396
|
* - `buildTokenMap` — Tasty style-to-state bindings (`#name` keys, state aliases).
|
|
4060
4397
|
* - `buildFlatTokenMap` — `{ light, dark, ... }` per-variant maps.
|
|
4061
4398
|
* - `buildJsonMap` — `{ name: { light, dark, ... } }` per-color JSON.
|
|
@@ -4066,7 +4403,8 @@ function resolveAllColors(seed, defs, config, externalBases, externalAnchors) {
|
|
|
4066
4403
|
*
|
|
4067
4404
|
* The HDR tier (`variant.hdr`, a glow past SDR white) is emitted only with
|
|
4068
4405
|
* `modes.hdr` (`css()`: its `hdr` option), and always as a literal
|
|
4069
|
-
* `color(srgb
|
|
4406
|
+
* `color(srgb R G B)` with channels past 1 — whatever `format`, even under
|
|
4407
|
+
* `splitHue`.
|
|
4070
4408
|
* Without it every shape is byte-identical to SDR. Each shape carries it the
|
|
4071
4409
|
* way its consumer composes values:
|
|
4072
4410
|
* - Tasty: a sparse `hdr` state key after each SDR key that has one (`''`,
|
|
@@ -4146,15 +4484,16 @@ function formatVariant(v, format = "oklch", pastel = false) {
|
|
|
4146
4484
|
}
|
|
4147
4485
|
/**
|
|
4148
4486
|
* Format a variant's HDR value (`variant.hdr`, set on a glow past white with
|
|
4149
|
-
* `headroom`) as `color(srgb
|
|
4150
|
-
*
|
|
4151
|
-
*
|
|
4152
|
-
*
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4487
|
+
* `headroom`) as extended `color(srgb R G B)` (default) or
|
|
4488
|
+
* `color(srgb-linear R G B)` — the same color, with the brightest channel
|
|
4489
|
+
* past 1 — or `undefined` when it has none. One of these whatever the SDR
|
|
4490
|
+
* format: OKLCH lightness clamps at parse time and `rgb()` / `hsl()` cannot
|
|
4491
|
+
* pass white. Emitted as a literal, never with a split hue.
|
|
4492
|
+
*/
|
|
4493
|
+
function formatHdrVariant(v, format = "srgb") {
|
|
4494
|
+
if (format !== "srgb" && format !== "srgb-linear") throw new Error(`glaze: unknown HDR format ${JSON.stringify(format)}. Use 'srgb' or 'srgb-linear'.`);
|
|
4156
4495
|
if (v.hdr === void 0) return void 0;
|
|
4157
|
-
return formatSrgbLinear(hdrToLinearSrgb(v.hdr));
|
|
4496
|
+
return format === "srgb" ? formatSrgb(hdrToSrgb(v.hdr)) : formatSrgbLinear(hdrToLinearSrgb(v.hdr));
|
|
4158
4497
|
}
|
|
4159
4498
|
/**
|
|
4160
4499
|
* Format a resolved variant as `oklch(L C <hueVar>)`, splicing a CSS hue var
|
|
@@ -4381,8 +4720,8 @@ const DTCG_EXTENSION_NAMESPACE = "me.tenphi.glaze";
|
|
|
4381
4720
|
* The DTCG form of an HDR value: `exposure` in stops, the luminance `tone`
|
|
4382
4721
|
* it lands (past 100 unless `headroom` caps a saturated base short of it), the
|
|
4383
4722
|
* opaque `base` as a DTCG sRGB color (always sRGB, whatever `colorSpace`, and
|
|
4384
|
-
* never pastel — the base is standard OKHSL), and the HDR color as
|
|
4385
|
-
* components, the brightest above 1.
|
|
4723
|
+
* never pastel — the base is standard OKHSL), and the HDR color as extended
|
|
4724
|
+
* sRGB components (the numbers of `glaze.formatHdr()`), the brightest above 1.
|
|
4386
4725
|
*/
|
|
4387
4726
|
function dtcgHdrValue(hdr) {
|
|
4388
4727
|
const linear = hdrToLinearSrgb(hdr);
|
|
@@ -4392,11 +4731,11 @@ function dtcgHdrValue(hdr) {
|
|
|
4392
4731
|
tone: roundTo(toneFromY(relativeLuminanceFromLinearRgb(linear)), 4),
|
|
4393
4732
|
base: dtcgSrgbValue(h, s, l, false),
|
|
4394
4733
|
value: {
|
|
4395
|
-
colorSpace: "srgb
|
|
4734
|
+
colorSpace: "srgb",
|
|
4396
4735
|
components: [
|
|
4397
|
-
roundTo(linear[0], 6),
|
|
4398
|
-
roundTo(linear[1], 6),
|
|
4399
|
-
roundTo(linear[2], 6)
|
|
4736
|
+
roundTo(sRGBLinearToGamma(linear[0]), 6),
|
|
4737
|
+
roundTo(sRGBLinearToGamma(linear[1]), 6),
|
|
4738
|
+
roundTo(sRGBLinearToGamma(linear[2]), 6)
|
|
4400
4739
|
]
|
|
4401
4740
|
}
|
|
4402
4741
|
};
|
|
@@ -4665,7 +5004,7 @@ function validateStructuredInput(input) {
|
|
|
4665
5004
|
if (input.saturationFactor !== void 0) {
|
|
4666
5005
|
if (!Number.isFinite(input.saturationFactor) || input.saturationFactor < 0 || input.saturationFactor > 1) throw new Error(`glaze.color: structured saturationFactor must be a finite number in 0–1 (got ${input.saturationFactor}).`);
|
|
4667
5006
|
}
|
|
4668
|
-
if (typeof input.darkHue === "number"
|
|
5007
|
+
if (input.darkHue !== void 0 && !(typeof input.darkHue === "number" ? Number.isFinite(input.darkHue) : isRelativeValue(input.darkHue))) throw new Error(`glaze.color: structured darkHue must be a finite number or a '+N' / '-N' offset (got ${showValue(input.darkHue)}).`);
|
|
4669
5008
|
if (input.darkSaturation !== void 0) {
|
|
4670
5009
|
if (!Number.isFinite(input.darkSaturation) || input.darkSaturation < 0 || input.darkSaturation > 100) throw new Error(`glaze.color: structured darkSaturation must be a finite number in 0–100 (got ${input.darkSaturation}).`);
|
|
4671
5010
|
}
|
|
@@ -4724,7 +5063,9 @@ function standaloneRole(role) {
|
|
|
4724
5063
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
4725
5064
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
4726
5065
|
* the seed pinned to the literal user-provided color across all four
|
|
4727
|
-
* variants, so the contrast solver always anchors against it.
|
|
5066
|
+
* variants, so the contrast solver always anchors against it. Its dark-delta
|
|
5067
|
+
* anchor is still its authored tone (`anchorAtAuthoredTone`), so a delta on it
|
|
5068
|
+
* inverts in dark like one on the same color in a theme.
|
|
4728
5069
|
*/
|
|
4729
5070
|
function buildStandaloneValueDefs(main, options) {
|
|
4730
5071
|
const seedHue = typeof options?.hue === "number" ? options.hue : main.h;
|
|
@@ -4754,13 +5095,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
4754
5095
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
4755
5096
|
};
|
|
4756
5097
|
const defs = { [primary]: valueDef };
|
|
4757
|
-
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
5098
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = anchorAtAuthoredTone({
|
|
4758
5099
|
hue: main.h,
|
|
4759
5100
|
saturation: 1,
|
|
4760
5101
|
tone: seedTone,
|
|
4761
5102
|
mode: "static",
|
|
4762
5103
|
role: "surface"
|
|
4763
|
-
};
|
|
5104
|
+
});
|
|
4764
5105
|
return {
|
|
4765
5106
|
seed: {
|
|
4766
5107
|
hue: seedHue,
|
|
@@ -4928,7 +5269,8 @@ function resolveBaseToken(base) {
|
|
|
4928
5269
|
function isGlazeColorToken(candidate) {
|
|
4929
5270
|
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate) && "resolve" in candidate && typeof candidate.resolve === "function";
|
|
4930
5271
|
}
|
|
4931
|
-
function createColorToken(
|
|
5272
|
+
function createColorToken(rawInput, configOverride) {
|
|
5273
|
+
const input = copyTokenOptions(rawInput);
|
|
4932
5274
|
validateStructuredInput(input);
|
|
4933
5275
|
const userName = input.name;
|
|
4934
5276
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
@@ -4949,12 +5291,12 @@ function createColorToken(input, configOverride) {
|
|
|
4949
5291
|
role: standaloneRole(input.role),
|
|
4950
5292
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
4951
5293
|
} };
|
|
4952
|
-
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
5294
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = anchorAtAuthoredTone({
|
|
4953
5295
|
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
4954
5296
|
saturation: 1,
|
|
4955
5297
|
mode: "static",
|
|
4956
5298
|
role: "surface"
|
|
4957
|
-
};
|
|
5299
|
+
});
|
|
4958
5300
|
const localOverride = copyConfigOverride(configOverride);
|
|
4959
5301
|
return createColorTokenFromDefs({
|
|
4960
5302
|
hue: input.hue,
|
|
@@ -4964,11 +5306,13 @@ function createColorToken(input, configOverride) {
|
|
|
4964
5306
|
kind: "color",
|
|
4965
5307
|
version: GLAZE_EXPORT_VERSION,
|
|
4966
5308
|
form: "structured",
|
|
4967
|
-
input: buildStructuredInputExport(input, exportArg),
|
|
5309
|
+
input: buildStructuredInputExport(input, baseToken, exportArg),
|
|
4968
5310
|
config: freezeConfigForExport(localOverride, exportArg)
|
|
4969
5311
|
}));
|
|
4970
5312
|
}
|
|
4971
|
-
function createColorTokenFromValue(
|
|
5313
|
+
function createColorTokenFromValue(input, inputOptions, configOverride) {
|
|
5314
|
+
const value = copyColorValue(input);
|
|
5315
|
+
const options = inputOptions && copyTokenOptions(inputOptions);
|
|
4972
5316
|
const main = extractOkhslFromValue(value);
|
|
4973
5317
|
const baseToken = resolveBaseToken(options?.base);
|
|
4974
5318
|
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
@@ -4977,40 +5321,43 @@ function createColorTokenFromValue(value, options, configOverride) {
|
|
|
4977
5321
|
kind: "color",
|
|
4978
5322
|
version: GLAZE_EXPORT_VERSION,
|
|
4979
5323
|
form: "value",
|
|
4980
|
-
input: value,
|
|
4981
|
-
...options !== void 0 ? { overrides: buildOverridesExport(options, exportArg) } : {},
|
|
5324
|
+
input: copyColorValue(value),
|
|
5325
|
+
...options !== void 0 ? { overrides: buildOverridesExport(options, baseToken, exportArg) } : {},
|
|
4982
5326
|
config: freezeConfigForExport(localOverride, exportArg)
|
|
4983
5327
|
}));
|
|
4984
5328
|
}
|
|
4985
5329
|
/**
|
|
4986
|
-
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is
|
|
4987
|
-
*
|
|
4988
|
-
*
|
|
5330
|
+
* Build a JSON-safe snapshot of `GlazeColorOverrides`. `base` is serialized
|
|
5331
|
+
* as its token's own snapshot: a raw value as the token it is wrapped in, so
|
|
5332
|
+
* its config is frozen with the rest and a restored token doesn't follow later
|
|
5333
|
+
* `glaze.configure()` calls through its base. Every object in it is a fresh
|
|
5334
|
+
* copy, so editing a snapshot never reaches the live token.
|
|
4989
5335
|
*/
|
|
4990
|
-
function buildOverridesExport(options, exportArg) {
|
|
5336
|
+
function buildOverridesExport(options, baseToken, exportArg) {
|
|
4991
5337
|
const out = {};
|
|
4992
5338
|
if (options.hue !== void 0) out.hue = options.hue;
|
|
4993
5339
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
4994
|
-
if (options.tone !== void 0) out.tone = options.tone;
|
|
5340
|
+
if (options.tone !== void 0) out.tone = copyPair(options.tone);
|
|
4995
5341
|
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
4996
5342
|
if (options.darkHue !== void 0) out.darkHue = options.darkHue;
|
|
4997
5343
|
if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
|
|
4998
5344
|
if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
|
|
4999
5345
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
5000
5346
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
5001
|
-
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
5347
|
+
if (options.contrast !== void 0) out.contrast = copyContrast(options.contrast);
|
|
5002
5348
|
if (options.opacity !== void 0) out.opacity = options.opacity;
|
|
5003
5349
|
if (options.name !== void 0) out.name = options.name;
|
|
5004
5350
|
if (options.pastel !== void 0) out.pastel = options.pastel;
|
|
5005
5351
|
if (options.role !== void 0) out.role = options.role;
|
|
5006
|
-
if (
|
|
5352
|
+
if (baseToken !== void 0) out.base = baseToken.export(exportArg);
|
|
5007
5353
|
return out;
|
|
5008
5354
|
}
|
|
5009
|
-
|
|
5355
|
+
/** The structured-input counterpart of `buildOverridesExport`. */
|
|
5356
|
+
function buildStructuredInputExport(input, baseToken, exportArg) {
|
|
5010
5357
|
const out = {
|
|
5011
5358
|
hue: input.hue,
|
|
5012
5359
|
saturation: input.saturation,
|
|
5013
|
-
tone: input.tone
|
|
5360
|
+
tone: copyPair(input.tone)
|
|
5014
5361
|
};
|
|
5015
5362
|
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
5016
5363
|
if (input.darkHue !== void 0) out.darkHue = input.darkHue;
|
|
@@ -5019,13 +5366,68 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
5019
5366
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
5020
5367
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
5021
5368
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
5022
|
-
if (input.contrast !== void 0) out.contrast = input.contrast;
|
|
5369
|
+
if (input.contrast !== void 0) out.contrast = copyContrast(input.contrast);
|
|
5023
5370
|
if (input.name !== void 0) out.name = input.name;
|
|
5024
5371
|
if (input.pastel !== void 0) out.pastel = input.pastel;
|
|
5025
5372
|
if (input.role !== void 0) out.role = input.role;
|
|
5026
|
-
if (
|
|
5373
|
+
if (baseToken !== void 0) out.base = baseToken.export(exportArg);
|
|
5027
5374
|
return out;
|
|
5028
5375
|
}
|
|
5376
|
+
/** A copy of a `[normal, highContrast]` pair, or the value itself. */
|
|
5377
|
+
function copyPair(value) {
|
|
5378
|
+
return Array.isArray(value) ? [...value] : value;
|
|
5379
|
+
}
|
|
5380
|
+
/**
|
|
5381
|
+
* A copy of a `contrast` value, read by property access so a proxy (reactive
|
|
5382
|
+
* state) copies too. Other keys of a spec object are kept for the validator.
|
|
5383
|
+
*/
|
|
5384
|
+
function copyContrast(contrast) {
|
|
5385
|
+
const copySpec = (spec) => {
|
|
5386
|
+
if (typeof spec !== "object" || spec === null) return spec;
|
|
5387
|
+
const out = { ...spec };
|
|
5388
|
+
for (const key of ["wcag", "apca"]) {
|
|
5389
|
+
const value = spec[key];
|
|
5390
|
+
if (value !== void 0) out[key] = copyPair(value);
|
|
5391
|
+
}
|
|
5392
|
+
return out;
|
|
5393
|
+
};
|
|
5394
|
+
return Array.isArray(contrast) ? contrast.map(copySpec) : copySpec(contrast);
|
|
5395
|
+
}
|
|
5396
|
+
/** Every field of `GlazeColorInput` / `GlazeColorOverrides`. */
|
|
5397
|
+
const TOKEN_OPTION_KEYS = [
|
|
5398
|
+
"hue",
|
|
5399
|
+
"saturation",
|
|
5400
|
+
"tone",
|
|
5401
|
+
"saturationFactor",
|
|
5402
|
+
"darkHue",
|
|
5403
|
+
"darkSaturation",
|
|
5404
|
+
"darkSaturationFactor",
|
|
5405
|
+
"mode",
|
|
5406
|
+
"autoFlip",
|
|
5407
|
+
"opacity",
|
|
5408
|
+
"contrast",
|
|
5409
|
+
"base",
|
|
5410
|
+
"name",
|
|
5411
|
+
"pastel",
|
|
5412
|
+
"role"
|
|
5413
|
+
];
|
|
5414
|
+
/**
|
|
5415
|
+
* A detached copy of a token's input or overrides, each field read by property
|
|
5416
|
+
* access so a getter, an inherited field or a proxy copies like a plain one.
|
|
5417
|
+
* The pairs, `contrast` and a raw value `base` are copied too, so a caller
|
|
5418
|
+
* editing them later doesn't change the token (a token `base` is kept, as
|
|
5419
|
+
* tokens are immutable). Other keys (a value form's `from`) are left out.
|
|
5420
|
+
*/
|
|
5421
|
+
function copyTokenOptions(options) {
|
|
5422
|
+
const source = options;
|
|
5423
|
+
const out = {};
|
|
5424
|
+
for (const key of TOKEN_OPTION_KEYS) if (source[key] !== void 0) out[key] = source[key];
|
|
5425
|
+
const copy = out;
|
|
5426
|
+
if (copy.tone !== void 0) copy.tone = copyPair(copy.tone);
|
|
5427
|
+
if (copy.contrast !== void 0) copy.contrast = copyContrast(copy.contrast);
|
|
5428
|
+
if (copy.base !== void 0 && !isGlazeColorToken(copy.base)) copy.base = copyColorValue(copy.base);
|
|
5429
|
+
return copy;
|
|
5430
|
+
}
|
|
5029
5431
|
/**
|
|
5030
5432
|
* Discriminate a `GlazeColorTokenExport` from a raw `GlazeColorValue`.
|
|
5031
5433
|
*/
|
|
@@ -5119,8 +5521,25 @@ function colorFromExport(data) {
|
|
|
5119
5521
|
* property, so a `suffix` that lands one on a hue var (`'-hue'`, or `''` with a
|
|
5120
5522
|
* color named `*-hue`) throws too.
|
|
5121
5523
|
*/
|
|
5524
|
+
/**
|
|
5525
|
+
* Reject a seed the resolver can't use: every color multiplies its hue and
|
|
5526
|
+
* saturation, so a non-finite one paints the whole theme `NaN`.
|
|
5527
|
+
*/
|
|
5528
|
+
function assertValidSeed(seed) {
|
|
5529
|
+
for (const field of [
|
|
5530
|
+
"hue",
|
|
5531
|
+
"saturation",
|
|
5532
|
+
"darkHue",
|
|
5533
|
+
"darkSaturation"
|
|
5534
|
+
]) {
|
|
5535
|
+
const value = seed?.[field];
|
|
5536
|
+
if ((field === "darkHue" || field === "darkSaturation") && value === void 0) continue;
|
|
5537
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`glaze: theme seed ${field} must be a finite number (got ${showValue(value)}).`);
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5122
5540
|
function createTheme(seed, initialColors, callerOverride) {
|
|
5123
5541
|
const configOverride = copyConfigOverride(callerOverride);
|
|
5542
|
+
assertValidSeed(seed);
|
|
5124
5543
|
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
5125
5544
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
5126
5545
|
let cache = null;
|
|
@@ -5587,8 +6006,8 @@ function glaze(hueOrOptions, saturationOrConfig, config) {
|
|
|
5587
6006
|
if (typeof hueOrOptions === "number") return createTheme({
|
|
5588
6007
|
hue: hueOrOptions,
|
|
5589
6008
|
saturation: (typeof saturationOrConfig === "number" ? saturationOrConfig : void 0) ?? 100
|
|
5590
|
-
}, void 0, config);
|
|
5591
|
-
return createTheme(hueOrOptions, void 0, typeof saturationOrConfig === "object" ? saturationOrConfig : config);
|
|
6009
|
+
}, void 0, (config === void 0 || config === null) && typeof saturationOrConfig === "object" && saturationOrConfig !== null ? saturationOrConfig : config);
|
|
6010
|
+
return createTheme(hueOrOptions, void 0, typeof saturationOrConfig === "object" && saturationOrConfig !== null ? saturationOrConfig : config);
|
|
5592
6011
|
}
|
|
5593
6012
|
/** Configure global glaze settings. */
|
|
5594
6013
|
glaze.configure = function configure$1(config) {
|
|
@@ -5653,14 +6072,16 @@ glaze.color = function color(input, config) {
|
|
|
5653
6072
|
if (typeof input === "string") return createColorTokenFromValue(input, void 0, config);
|
|
5654
6073
|
const obj = input;
|
|
5655
6074
|
if ("from" in obj) {
|
|
5656
|
-
const
|
|
5657
|
-
return createColorTokenFromValue(from,
|
|
6075
|
+
const fromInput = input;
|
|
6076
|
+
return createColorTokenFromValue(fromInput.from, fromInput, config);
|
|
5658
6077
|
}
|
|
5659
6078
|
if ("hue" in obj) return createColorToken(input, config);
|
|
5660
6079
|
return createColorTokenFromValue(input, void 0, config);
|
|
5661
6080
|
};
|
|
5662
6081
|
/** Shared body of `glaze.shadow()` / `glaze.glow()`. */
|
|
5663
6082
|
function standaloneLighting(kind, input) {
|
|
6083
|
+
if (input?.bg === void 0) throw new Error(`glaze.${kind}(): "bg" is required.`);
|
|
6084
|
+
if (typeof input.intensity !== "number" || !Number.isFinite(input.intensity)) throw new Error(`glaze.${kind}(): "intensity" must be a finite number (got ${showValue(input.intensity)}).`);
|
|
5664
6085
|
const bg = extractOkhslFromValue(input.bg);
|
|
5665
6086
|
const fg = input.fg ? extractOkhslFromValue(input.fg) : void 0;
|
|
5666
6087
|
const cfg = getConfig();
|
|
@@ -5714,19 +6135,19 @@ glaze.format = function format(variant, colorFormat, pastel) {
|
|
|
5714
6135
|
};
|
|
5715
6136
|
/**
|
|
5716
6137
|
* Format a variant's HDR value — set on a glow whose step crosses SDR white
|
|
5717
|
-
* with `headroom` above 0 — as `color(srgb
|
|
5718
|
-
* may exceed 1
|
|
5719
|
-
* `'srgb-linear'
|
|
5720
|
-
* `rgb()` / `hsl()` cannot go past white.
|
|
6138
|
+
* with `headroom` above 0 — as extended `color(srgb R G B)`, with channels
|
|
6139
|
+
* that may exceed 1 (`1` is SDR white), or as the same color in linear light
|
|
6140
|
+
* with `'srgb-linear'`. Returns `undefined` for a variant without one.
|
|
5721
6141
|
*
|
|
5722
6142
|
* @example
|
|
5723
6143
|
* ```ts
|
|
5724
6144
|
* const v = glaze.glow({ bg: '#ffffff', intensity: 12, tuning: { headroom: 1 } });
|
|
5725
|
-
* glaze.format(v);
|
|
5726
|
-
* glaze.formatHdr(v);
|
|
6145
|
+
* glaze.format(v); // SDR: 'oklch(1 0 0)'
|
|
6146
|
+
* glaze.formatHdr(v); // 'color(srgb 1.1813 1.1813 1.1813)'
|
|
6147
|
+
* glaze.formatHdr(v, 'srgb-linear'); // 'color(srgb-linear 1.4631 1.4631 1.4631)'
|
|
5727
6148
|
* ```
|
|
5728
6149
|
*/
|
|
5729
|
-
glaze.formatHdr = function formatHdr(variant, hdrFormat = "srgb
|
|
6150
|
+
glaze.formatHdr = function formatHdr(variant, hdrFormat = "srgb") {
|
|
5730
6151
|
return formatHdrVariant(variant, hdrFormat);
|
|
5731
6152
|
};
|
|
5732
6153
|
/**
|
|
@@ -5817,10 +6238,13 @@ exports.formatOkhsl = formatOkhsl;
|
|
|
5817
6238
|
exports.formatOkhst = formatOkhst;
|
|
5818
6239
|
exports.formatOklch = formatOklch;
|
|
5819
6240
|
exports.formatRgb = formatRgb;
|
|
6241
|
+
exports.formatSrgb = formatSrgb;
|
|
5820
6242
|
exports.formatSrgbLinear = formatSrgbLinear;
|
|
5821
6243
|
exports.fromTone = fromTone;
|
|
5822
6244
|
exports.gamutClampedLuminance = gamutClampedLuminance;
|
|
5823
6245
|
exports.glaze = glaze;
|
|
6246
|
+
exports.hdrToLinearSrgb = hdrToLinearSrgb;
|
|
6247
|
+
exports.hdrToSrgb = hdrToSrgb;
|
|
5824
6248
|
exports.hslToSrgb = hslToSrgb;
|
|
5825
6249
|
exports.inferRoleFromName = inferRoleFromName;
|
|
5826
6250
|
exports.isColorTokenExport = isColorTokenExport;
|