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