@tenphi/glaze 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +246 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +98 -6
- package/dist/index.d.mts +98 -6
- package/dist/index.mjs +246 -95
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +90 -7
- package/docs/methodology.md +28 -0
- package/docs/okhst.md +21 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1977,34 +1977,77 @@ function resolveContrastSpec(spec, isHighContrast, polarity) {
|
|
|
1977
1977
|
/**
|
|
1978
1978
|
* Apply the relative-tone delta against a base, honoring `flip`.
|
|
1979
1979
|
*
|
|
1980
|
-
* When `flip` is on and `base + delta` falls outside `[0, 100]`,
|
|
1981
|
-
* delta to the other side of the base
|
|
1982
|
-
*
|
|
1980
|
+
* When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
|
|
1981
|
+
* the delta to the other side of the base. If the mirrored target is also out
|
|
1982
|
+
* of range, keep the original delta so the caller clamps on the authored side.
|
|
1983
|
+
* When off, the caller clamps as usual.
|
|
1983
1984
|
*/
|
|
1984
1985
|
function applyToneFlip(delta, baseTone, flip) {
|
|
1985
1986
|
if (!flip) return delta;
|
|
1986
1987
|
const target = baseTone + delta;
|
|
1987
1988
|
if (target >= 0 && target <= 100) return delta;
|
|
1988
|
-
|
|
1989
|
+
const mirrored = baseTone - delta;
|
|
1990
|
+
if (mirrored >= 0 && mirrored <= 100) return -delta;
|
|
1991
|
+
return delta;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Dark position for an extreme tone (`'max'` / `'min'`) anchored to a base.
|
|
1995
|
+
*
|
|
1996
|
+
* A windowed dark remap compresses the base→extreme span, so the pair loses
|
|
1997
|
+
* contrast in dark. Instead, measure the tone shift the light scheme applied
|
|
1998
|
+
* between the base and the extreme, then replay it against the base's dark
|
|
1999
|
+
* tone: `auto` mirrors the sign (both ends invert), `fixed` keeps it. The
|
|
2000
|
+
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2001
|
+
* intended, since the author asked for the extreme.
|
|
2002
|
+
*/
|
|
2003
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, baseDarkTone, config) {
|
|
2004
|
+
const delta = mapToneForScheme(authorTone, mode, false, isHighContrast, config) - getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2005
|
+
return clamp(baseDarkTone + (mode === "auto" ? -delta : delta), 0, 100);
|
|
1989
2006
|
}
|
|
1990
2007
|
function resolveRootColor(def, isHighContrast) {
|
|
1991
2008
|
const rawT = def.tone;
|
|
2009
|
+
return clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100);
|
|
2010
|
+
}
|
|
2011
|
+
/**
|
|
2012
|
+
* Effective hue and saturation for one color in one scheme.
|
|
2013
|
+
*
|
|
2014
|
+
* Dark schemes read the dark seed pair and the color's `darkHue` /
|
|
2015
|
+
* `darkSaturation`, each falling back to its light counterpart. An explicit
|
|
2016
|
+
* dark saturation — on the seed or the def — is taken literally; the global
|
|
2017
|
+
* `darkDesaturation` reduction applies only when neither is authored.
|
|
2018
|
+
* `mode: 'static'` pins one value across every scheme, so it skips all of it.
|
|
2019
|
+
*
|
|
2020
|
+
* Shared by `resolveColorForScheme` (the emitted variant) and
|
|
2021
|
+
* `resolveDependentColor` (the contrast solver's inputs) so the solver always
|
|
2022
|
+
* measures the color it is actually going to produce.
|
|
2023
|
+
*/
|
|
2024
|
+
function resolveChannels(def, ctx, isDark) {
|
|
2025
|
+
const mode = def.mode ?? "auto";
|
|
2026
|
+
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2027
|
+
if (!isDark || mode === "static") return {
|
|
2028
|
+
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2029
|
+
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2030
|
+
};
|
|
2031
|
+
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2032
|
+
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2033
|
+
const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2034
|
+
const raw = darkFactor * darkSeedSaturation / 100;
|
|
1992
2035
|
return {
|
|
1993
|
-
|
|
1994
|
-
|
|
2036
|
+
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2037
|
+
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
1995
2038
|
};
|
|
1996
2039
|
}
|
|
1997
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark,
|
|
2040
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
|
|
1998
2041
|
const baseName = def.base;
|
|
1999
2042
|
const baseResolved = ctx.resolved.get(baseName);
|
|
2000
2043
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
2001
2044
|
const mode = def.mode ?? "auto";
|
|
2002
|
-
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2003
2045
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2004
2046
|
const pastel = effectivePastel;
|
|
2005
2047
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2006
2048
|
const baseTone = baseVariant.t * 100;
|
|
2007
2049
|
let preferredTone;
|
|
2050
|
+
let isExtreme = false;
|
|
2008
2051
|
const rawTone = def.tone;
|
|
2009
2052
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2010
2053
|
else {
|
|
@@ -2013,22 +2056,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2013
2056
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2014
2057
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2015
2058
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2016
|
-
else
|
|
2059
|
+
else if (parsed.kind === "extreme" && isDark && mode !== "static") {
|
|
2060
|
+
isExtreme = true;
|
|
2061
|
+
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2062
|
+
} else {
|
|
2063
|
+
isExtreme = parsed.kind === "extreme";
|
|
2064
|
+
preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
|
|
2065
|
+
}
|
|
2017
2066
|
}
|
|
2018
2067
|
const rawContrast = def.contrast;
|
|
2019
2068
|
if (rawContrast !== void 0) {
|
|
2020
2069
|
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2021
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
2022
2070
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2023
2071
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2024
2072
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2025
2073
|
let initialDirection;
|
|
2026
2074
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2027
2075
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2076
|
+
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2028
2077
|
const result = findToneForContrast({
|
|
2029
|
-
hue:
|
|
2030
|
-
saturation:
|
|
2031
|
-
preferredTone:
|
|
2078
|
+
hue: channels.hue,
|
|
2079
|
+
saturation: channels.saturation,
|
|
2080
|
+
preferredTone: seedTone,
|
|
2032
2081
|
baseLinearRgb,
|
|
2033
2082
|
contrast: resolvedContrast,
|
|
2034
2083
|
toneRange: [0, 1],
|
|
@@ -2037,15 +2086,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2037
2086
|
pastel
|
|
2038
2087
|
});
|
|
2039
2088
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2040
|
-
return
|
|
2041
|
-
tone: result.tone * 100,
|
|
2042
|
-
satFactor
|
|
2043
|
-
};
|
|
2089
|
+
return result.tone * 100;
|
|
2044
2090
|
}
|
|
2045
|
-
return
|
|
2046
|
-
tone: clamp(preferredTone, 0, 100),
|
|
2047
|
-
satFactor
|
|
2048
|
-
};
|
|
2091
|
+
return clamp(preferredTone, 0, 100);
|
|
2049
2092
|
}
|
|
2050
2093
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2051
2094
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -2053,27 +2096,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2053
2096
|
const regDef = def;
|
|
2054
2097
|
const mode = regDef.mode ?? "auto";
|
|
2055
2098
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
2056
|
-
const
|
|
2099
|
+
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2057
2100
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2058
2101
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2059
|
-
|
|
2060
|
-
let satFactor;
|
|
2061
|
-
if (isRoot) {
|
|
2062
|
-
const root = resolveRootColor(regDef, isHighContrast);
|
|
2063
|
-
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
2064
|
-
satFactor = root.satFactor;
|
|
2065
|
-
} else {
|
|
2066
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
2067
|
-
finalTone = dep.tone;
|
|
2068
|
-
satFactor = dep.satFactor;
|
|
2069
|
-
}
|
|
2070
|
-
const baseSat = satFactor * ctx.saturation / 100;
|
|
2071
|
-
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
2072
|
-
const toneFraction = clamp(finalTone / 100, 0, 1);
|
|
2102
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2073
2103
|
return {
|
|
2074
|
-
h:
|
|
2075
|
-
s:
|
|
2076
|
-
t:
|
|
2104
|
+
h: channels.hue,
|
|
2105
|
+
s: channels.saturation,
|
|
2106
|
+
t: clamp(finalTone / 100, 0, 1),
|
|
2077
2107
|
alpha: regDef.opacity ?? 1,
|
|
2078
2108
|
pastel
|
|
2079
2109
|
};
|
|
@@ -2273,12 +2303,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2273
2303
|
}
|
|
2274
2304
|
}
|
|
2275
2305
|
}
|
|
2276
|
-
function resolveAllColors(
|
|
2306
|
+
function resolveAllColors(seed, defs, config, externalBases) {
|
|
2277
2307
|
validateColorDefs(defs, externalBases);
|
|
2278
2308
|
const order = topoSort(defs);
|
|
2279
2309
|
const ctx = {
|
|
2280
|
-
|
|
2281
|
-
saturation,
|
|
2310
|
+
...seed,
|
|
2282
2311
|
defs,
|
|
2283
2312
|
resolved: /* @__PURE__ */ new Map(),
|
|
2284
2313
|
config,
|
|
@@ -2311,8 +2340,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
2311
2340
|
/**
|
|
2312
2341
|
* Hue channel planning for `splitHue` exports.
|
|
2313
2342
|
*
|
|
2314
|
-
* Builds per-color hue var references and
|
|
2315
|
-
*
|
|
2343
|
+
* Builds per-color hue var references and the `--*-hue` declarations backing
|
|
2344
|
+
* them for oklch CSS / Tasty output when every color is pastel.
|
|
2345
|
+
*
|
|
2346
|
+
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2347
|
+
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2348
|
+
* set is emitted only when it actually differs — see `darkHueDeclarations`.
|
|
2316
2349
|
*/
|
|
2317
2350
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2318
2351
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2321,7 +2354,11 @@ function cssProp(prefix, name, suffix) {
|
|
|
2321
2354
|
function isAchromatic(v) {
|
|
2322
2355
|
return v.s <= ACHROMATIC_EPSILON;
|
|
2323
2356
|
}
|
|
2324
|
-
|
|
2357
|
+
/** Seed hue backing `--{baseName}-hue` in a given scheme. */
|
|
2358
|
+
function schemeSeedHue(ctx, scheme) {
|
|
2359
|
+
return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
|
|
2360
|
+
}
|
|
2361
|
+
function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
2325
2362
|
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2326
2363
|
hueVar: "",
|
|
2327
2364
|
inline: true,
|
|
@@ -2329,13 +2366,25 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2329
2366
|
};
|
|
2330
2367
|
const regDef = def;
|
|
2331
2368
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2332
|
-
|
|
2369
|
+
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2370
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2371
|
+
const pinned = resolveEffectiveHue(ctx.seedHue, regDef.hue);
|
|
2372
|
+
return {
|
|
2373
|
+
hueVar: `var(${prop})`,
|
|
2374
|
+
inline: false,
|
|
2375
|
+
declarations: [{
|
|
2376
|
+
prop,
|
|
2377
|
+
value: String(pinned)
|
|
2378
|
+
}]
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2382
|
+
if (authored === void 0) return {
|
|
2333
2383
|
hueVar: baseHueVar,
|
|
2334
2384
|
inline: false,
|
|
2335
2385
|
declarations: []
|
|
2336
2386
|
};
|
|
2337
|
-
const parsed = parseRelativeOrAbsolute(
|
|
2338
|
-
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2387
|
+
const parsed = parseRelativeOrAbsolute(authored);
|
|
2339
2388
|
if (parsed.relative) {
|
|
2340
2389
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2341
2390
|
const magnitude = Math.abs(parsed.value);
|
|
@@ -2359,13 +2408,13 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2359
2408
|
}]
|
|
2360
2409
|
};
|
|
2361
2410
|
}
|
|
2362
|
-
function standaloneHuePlan(name, variant, ctx) {
|
|
2411
|
+
function standaloneHuePlan(name, variant, ctx, scheme) {
|
|
2363
2412
|
if (isAchromatic(variant)) return {
|
|
2364
2413
|
hueVar: "",
|
|
2365
2414
|
inline: true,
|
|
2366
2415
|
declarations: []
|
|
2367
2416
|
};
|
|
2368
|
-
const hue = ctx.resolvedHue ?? variant.h;
|
|
2417
|
+
const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
|
|
2369
2418
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2370
2419
|
return {
|
|
2371
2420
|
hueVar: `var(${prop})`,
|
|
@@ -2376,12 +2425,12 @@ function standaloneHuePlan(name, variant, ctx) {
|
|
|
2376
2425
|
}]
|
|
2377
2426
|
};
|
|
2378
2427
|
}
|
|
2379
|
-
function buildHuePlan(name, def, variant, ctx) {
|
|
2380
|
-
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2381
|
-
return themeHuePlan(name, def, variant, ctx);
|
|
2428
|
+
function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
2429
|
+
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx, scheme);
|
|
2430
|
+
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2382
2431
|
}
|
|
2383
2432
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2384
|
-
function collectHueDeclarations(resolved, ctx) {
|
|
2433
|
+
function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
2385
2434
|
if (ctx.emitDeclarations === false) return [];
|
|
2386
2435
|
const seen = /* @__PURE__ */ new Set();
|
|
2387
2436
|
const out = [];
|
|
@@ -2392,15 +2441,32 @@ function collectHueDeclarations(resolved, ctx) {
|
|
|
2392
2441
|
};
|
|
2393
2442
|
if (ctx.mode === "theme") push({
|
|
2394
2443
|
prop: `--${ctx.baseName}-hue`,
|
|
2395
|
-
value: String(ctx
|
|
2444
|
+
value: String(schemeSeedHue(ctx, scheme))
|
|
2396
2445
|
});
|
|
2397
2446
|
for (const [name, color] of resolved) {
|
|
2398
2447
|
const def = ctx.defs[name];
|
|
2399
|
-
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2448
|
+
const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
|
|
2400
2449
|
for (const decl of plan.declarations) push(decl);
|
|
2401
2450
|
}
|
|
2402
2451
|
return out;
|
|
2403
2452
|
}
|
|
2453
|
+
/**
|
|
2454
|
+
* Dark-scheme hue declarations, or `[]` when dark resolves to the same hues as
|
|
2455
|
+
* light (the common case — no `darkHue` anywhere).
|
|
2456
|
+
*
|
|
2457
|
+
* When they differ, the **whole** set is returned rather than only the entries
|
|
2458
|
+
* whose text changed. A relative declaration reads
|
|
2459
|
+
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2460
|
+
* moves with the seed, and custom properties substitute `var()` at
|
|
2461
|
+
* computed-value time on the declaring element — so re-declaring
|
|
2462
|
+
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2463
|
+
* the light-substituted value.
|
|
2464
|
+
*/
|
|
2465
|
+
function darkHueDeclarations(resolved, ctx) {
|
|
2466
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2467
|
+
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2468
|
+
return dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark;
|
|
2469
|
+
}
|
|
2404
2470
|
function buildHuePlans(resolved, ctx) {
|
|
2405
2471
|
const plans = /* @__PURE__ */ new Map();
|
|
2406
2472
|
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
@@ -2474,17 +2540,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2474
2540
|
const tokens = {};
|
|
2475
2541
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2476
2542
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2477
|
-
const
|
|
2478
|
-
if (
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
}
|
|
2485
|
-
const colorKey = `#${prefix}${name}`;
|
|
2486
|
-
tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2543
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
2544
|
+
if (modes.dark) for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2545
|
+
const key = `$${decl.prop.slice(2)}`;
|
|
2546
|
+
tokens[key] = {
|
|
2547
|
+
...tokens[key],
|
|
2548
|
+
[states.dark]: decl.value
|
|
2549
|
+
};
|
|
2487
2550
|
}
|
|
2551
|
+
for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2488
2552
|
return tokens;
|
|
2489
2553
|
}
|
|
2490
2554
|
for (const [name, color] of resolved) {
|
|
@@ -2533,7 +2597,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2533
2597
|
darkContrast: []
|
|
2534
2598
|
};
|
|
2535
2599
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2536
|
-
if (huePlans !== void 0 && channelCtx !== void 0)
|
|
2600
|
+
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2601
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
2602
|
+
for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2603
|
+
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2604
|
+
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2537
2607
|
for (const [name, color] of resolved) {
|
|
2538
2608
|
const prop = `--${prefix}${name}${suffix}`;
|
|
2539
2609
|
const plan = huePlans?.get(name);
|
|
@@ -2970,6 +3040,13 @@ function validateStructuredInput(input) {
|
|
|
2970
3040
|
if (input.saturationFactor !== void 0) {
|
|
2971
3041
|
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}).`);
|
|
2972
3042
|
}
|
|
3043
|
+
if (typeof input.darkHue === "number" && !Number.isFinite(input.darkHue)) throw new Error(`glaze.color: structured darkHue must be a finite number (got ${input.darkHue}).`);
|
|
3044
|
+
if (input.darkSaturation !== void 0) {
|
|
3045
|
+
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}).`);
|
|
3046
|
+
}
|
|
3047
|
+
if (input.darkSaturationFactor !== void 0) {
|
|
3048
|
+
if (!Number.isFinite(input.darkSaturationFactor) || input.darkSaturationFactor < 0 || input.darkSaturationFactor > 1) throw new Error(`glaze.color: structured darkSaturationFactor must be a finite number in 0–1 (got ${input.darkSaturationFactor}).`);
|
|
3049
|
+
}
|
|
2973
3050
|
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
2974
3051
|
}
|
|
2975
3052
|
/**
|
|
@@ -3016,11 +3093,27 @@ function extractOkhslFromValue(value) {
|
|
|
3016
3093
|
return value;
|
|
3017
3094
|
}
|
|
3018
3095
|
/**
|
|
3096
|
+
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3097
|
+
*
|
|
3098
|
+
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|
|
3099
|
+
* to measure against — it is not an authored base. Pinning the extreme keeps
|
|
3100
|
+
* the token on the plain scheme mapping instead of the base-anchored dark
|
|
3101
|
+
* replay the resolver applies to real `base` references.
|
|
3102
|
+
*/
|
|
3103
|
+
function pinExtremeTone(tone) {
|
|
3104
|
+
const pin = (value) => value === "max" ? 100 : value === "min" ? 0 : value;
|
|
3105
|
+
return Array.isArray(tone) ? [pin(tone[0]), pin(tone[1])] : pin(tone);
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3019
3108
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
3020
3109
|
*
|
|
3021
3110
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
3022
3111
|
* across every value-shorthand form.
|
|
3023
3112
|
*
|
|
3113
|
+
* An absolute `hue` override becomes the seed while a relative one becomes a
|
|
3114
|
+
* def field; `darkHue` always lands on the def in either form, so an absolute
|
|
3115
|
+
* `darkHue` wins outright instead of being re-offset by a relative `hue`.
|
|
3116
|
+
*
|
|
3024
3117
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
3025
3118
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
3026
3119
|
* the seed pinned to the literal user-provided color across all four
|
|
@@ -3038,10 +3131,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3038
3131
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
3039
3132
|
const primary = userName ?? STANDALONE_VALUE;
|
|
3040
3133
|
const seedTone = toTone(main.l);
|
|
3134
|
+
const primaryTone = toneOption ?? seedTone;
|
|
3041
3135
|
const valueDef = {
|
|
3042
3136
|
hue: relativeHue,
|
|
3043
3137
|
saturation: options?.saturationFactor,
|
|
3044
|
-
|
|
3138
|
+
darkHue: options?.darkHue,
|
|
3139
|
+
darkSaturation: options?.darkSaturationFactor,
|
|
3140
|
+
tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
|
|
3045
3141
|
contrast: options?.contrast,
|
|
3046
3142
|
mode: options?.mode ?? "auto",
|
|
3047
3143
|
autoFlip: options?.autoFlip,
|
|
@@ -3058,13 +3154,16 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3058
3154
|
mode: "static"
|
|
3059
3155
|
};
|
|
3060
3156
|
return {
|
|
3061
|
-
|
|
3062
|
-
|
|
3157
|
+
seed: {
|
|
3158
|
+
hue: seedHue,
|
|
3159
|
+
saturation: seedSaturation,
|
|
3160
|
+
darkSaturation: options?.darkSaturation
|
|
3161
|
+
},
|
|
3063
3162
|
defs,
|
|
3064
3163
|
primary
|
|
3065
3164
|
};
|
|
3066
3165
|
}
|
|
3067
|
-
function createColorTokenFromDefs(
|
|
3166
|
+
function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
|
|
3068
3167
|
let cache = null;
|
|
3069
3168
|
function getEffectiveConfig() {
|
|
3070
3169
|
const version = getConfigVersion();
|
|
@@ -3081,7 +3180,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3081
3180
|
const version = getConfigVersion();
|
|
3082
3181
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3083
3182
|
const effectiveConfig = getEffectiveConfig();
|
|
3084
|
-
const map = resolveAllColors(
|
|
3183
|
+
const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
3085
3184
|
cache = {
|
|
3086
3185
|
map,
|
|
3087
3186
|
version,
|
|
@@ -3119,7 +3218,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3119
3218
|
if (options.splitHue && format === "oklch") {
|
|
3120
3219
|
assertAllPastel(renamed, resolveModes());
|
|
3121
3220
|
channelCtx = {
|
|
3122
|
-
seedHue,
|
|
3221
|
+
seedHue: seed.hue,
|
|
3123
3222
|
baseName: options.name,
|
|
3124
3223
|
prefix: "",
|
|
3125
3224
|
defs: { [options.name]: defs[primary] },
|
|
@@ -3203,8 +3302,10 @@ function createColorToken(input, configOverride) {
|
|
|
3203
3302
|
const hasExternalBase = baseToken !== void 0;
|
|
3204
3303
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
3205
3304
|
const defs = { [primary]: {
|
|
3206
|
-
tone: input.tone,
|
|
3305
|
+
tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
|
|
3207
3306
|
saturation: input.saturationFactor,
|
|
3307
|
+
darkHue: input.darkHue,
|
|
3308
|
+
darkSaturation: input.darkSaturationFactor,
|
|
3208
3309
|
mode: input.mode ?? "auto",
|
|
3209
3310
|
autoFlip: input.autoFlip,
|
|
3210
3311
|
contrast: input.contrast,
|
|
@@ -3213,16 +3314,17 @@ function createColorToken(input, configOverride) {
|
|
|
3213
3314
|
role: input.role,
|
|
3214
3315
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
3215
3316
|
} };
|
|
3216
|
-
if (needsSeedAnchor) {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
mode: "static"
|
|
3222
|
-
};
|
|
3223
|
-
}
|
|
3317
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
3318
|
+
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
3319
|
+
saturation: 1,
|
|
3320
|
+
mode: "static"
|
|
3321
|
+
};
|
|
3224
3322
|
const localOverride = configOverride;
|
|
3225
|
-
return createColorTokenFromDefs(
|
|
3323
|
+
return createColorTokenFromDefs({
|
|
3324
|
+
hue: input.hue,
|
|
3325
|
+
saturation: input.saturation,
|
|
3326
|
+
darkSaturation: input.darkSaturation
|
|
3327
|
+
}, defs, primary, localOverride, baseToken, (exportArg) => ({
|
|
3226
3328
|
kind: "color",
|
|
3227
3329
|
version: GLAZE_EXPORT_VERSION,
|
|
3228
3330
|
form: "structured",
|
|
@@ -3233,9 +3335,9 @@ function createColorToken(input, configOverride) {
|
|
|
3233
3335
|
function createColorTokenFromValue(value, options, configOverride) {
|
|
3234
3336
|
const main = extractOkhslFromValue(value);
|
|
3235
3337
|
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
3236
|
-
const {
|
|
3338
|
+
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
3237
3339
|
const localOverride = sparseValueFormLocal(configOverride);
|
|
3238
|
-
return createColorTokenFromDefs(
|
|
3340
|
+
return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
|
|
3239
3341
|
kind: "color",
|
|
3240
3342
|
version: GLAZE_EXPORT_VERSION,
|
|
3241
3343
|
form: "value",
|
|
@@ -3255,6 +3357,9 @@ function buildOverridesExport(options, exportArg) {
|
|
|
3255
3357
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
3256
3358
|
if (options.tone !== void 0) out.tone = options.tone;
|
|
3257
3359
|
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
3360
|
+
if (options.darkHue !== void 0) out.darkHue = options.darkHue;
|
|
3361
|
+
if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
|
|
3362
|
+
if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
|
|
3258
3363
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
3259
3364
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
3260
3365
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
@@ -3272,6 +3377,9 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
3272
3377
|
tone: input.tone
|
|
3273
3378
|
};
|
|
3274
3379
|
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
3380
|
+
if (input.darkHue !== void 0) out.darkHue = input.darkHue;
|
|
3381
|
+
if (input.darkSaturation !== void 0) out.darkSaturation = input.darkSaturation;
|
|
3382
|
+
if (input.darkSaturationFactor !== void 0) out.darkSaturationFactor = input.darkSaturationFactor;
|
|
3275
3383
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
3276
3384
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
3277
3385
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
@@ -3294,6 +3402,9 @@ function rehydrateOverrides(data) {
|
|
|
3294
3402
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
3295
3403
|
if (data.tone !== void 0) out.tone = data.tone;
|
|
3296
3404
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3405
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3406
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3407
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3297
3408
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3298
3409
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3299
3410
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
@@ -3311,6 +3422,9 @@ function rehydrateStructuredInput(data) {
|
|
|
3311
3422
|
tone: data.tone
|
|
3312
3423
|
};
|
|
3313
3424
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3425
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3426
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3427
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3314
3428
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3315
3429
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3316
3430
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
@@ -3344,7 +3458,8 @@ function colorFromExport(data) {
|
|
|
3344
3458
|
|
|
3345
3459
|
//#endregion
|
|
3346
3460
|
//#region src/theme.ts
|
|
3347
|
-
function createTheme(
|
|
3461
|
+
function createTheme(seed, initialColors, configOverride) {
|
|
3462
|
+
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
3348
3463
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
3349
3464
|
let cache = null;
|
|
3350
3465
|
function getEffectiveConfig() {
|
|
@@ -3362,7 +3477,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3362
3477
|
const version = getConfigVersion();
|
|
3363
3478
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3364
3479
|
const effectiveConfig = getEffectiveConfig();
|
|
3365
|
-
const map = resolveAllColors(
|
|
3480
|
+
const map = resolveAllColors(seed, colorDefs, effectiveConfig);
|
|
3366
3481
|
cache = {
|
|
3367
3482
|
map,
|
|
3368
3483
|
version,
|
|
@@ -3379,6 +3494,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3379
3494
|
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3380
3495
|
return {
|
|
3381
3496
|
seedHue: hue,
|
|
3497
|
+
darkSeedHue: darkHue,
|
|
3382
3498
|
baseName: options.name ?? "theme",
|
|
3383
3499
|
prefix,
|
|
3384
3500
|
defs: colorDefs,
|
|
@@ -3392,6 +3508,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3392
3508
|
get saturation() {
|
|
3393
3509
|
return saturation;
|
|
3394
3510
|
},
|
|
3511
|
+
get darkHue() {
|
|
3512
|
+
return darkHue;
|
|
3513
|
+
},
|
|
3514
|
+
get darkSaturation() {
|
|
3515
|
+
return darkSaturation;
|
|
3516
|
+
},
|
|
3395
3517
|
getConfig() {
|
|
3396
3518
|
return getEffectiveConfig();
|
|
3397
3519
|
},
|
|
@@ -3428,16 +3550,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3428
3550
|
version: GLAZE_EXPORT_VERSION,
|
|
3429
3551
|
hue,
|
|
3430
3552
|
saturation,
|
|
3553
|
+
...darkHue !== void 0 ? { darkHue } : {},
|
|
3554
|
+
...darkSaturation !== void 0 ? { darkSaturation } : {},
|
|
3431
3555
|
colors: structuredClone(colorDefs),
|
|
3432
3556
|
config: freezeConfigForExport(configOverride, override)
|
|
3433
3557
|
};
|
|
3434
3558
|
},
|
|
3435
3559
|
extend(options) {
|
|
3436
|
-
const
|
|
3437
|
-
|
|
3560
|
+
const childSeed = {
|
|
3561
|
+
hue: options.hue ?? hue,
|
|
3562
|
+
saturation: options.saturation ?? saturation,
|
|
3563
|
+
darkHue: options.darkHue ?? darkHue,
|
|
3564
|
+
darkSaturation: options.darkSaturation ?? darkSaturation
|
|
3565
|
+
};
|
|
3438
3566
|
const inheritedColors = {};
|
|
3439
3567
|
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
3440
|
-
return createTheme(
|
|
3568
|
+
return createTheme(childSeed, options.colors ? {
|
|
3441
3569
|
...inheritedColors,
|
|
3442
3570
|
...options.colors
|
|
3443
3571
|
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
@@ -3547,6 +3675,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
|
|
|
3547
3675
|
assertAllPastel(filtered, modes);
|
|
3548
3676
|
return {
|
|
3549
3677
|
seedHue: theme.hue,
|
|
3678
|
+
darkSeedHue: theme.darkHue,
|
|
3550
3679
|
baseName: themeName,
|
|
3551
3680
|
prefix: themedPrefix,
|
|
3552
3681
|
defs: colorMapFromTheme(theme),
|
|
@@ -3578,7 +3707,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
|
3578
3707
|
assertExportKind(data, "theme", factory);
|
|
3579
3708
|
assertExportVersion(data, factory);
|
|
3580
3709
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
3581
|
-
|
|
3710
|
+
for (const field of ["darkHue", "darkSaturation"]) if (data[field] !== void 0 && typeof data[field] !== "number") throw new Error(`${factory}: expected "${field}" to be a number when present (got ${JSON.stringify(data[field])}).`);
|
|
3711
|
+
return createTheme({
|
|
3712
|
+
hue: data.hue,
|
|
3713
|
+
saturation: data.saturation,
|
|
3714
|
+
darkHue: data.darkHue,
|
|
3715
|
+
darkSaturation: data.darkSaturation
|
|
3716
|
+
}, data.colors, data.config);
|
|
3582
3717
|
}
|
|
3583
3718
|
/**
|
|
3584
3719
|
* Rebuild a palette from a `palette.export()` snapshot.
|
|
@@ -3740,18 +3875,28 @@ function createPalette(themes, paletteOptions) {
|
|
|
3740
3875
|
* the theme still reacts to later `configure()` calls for fields it
|
|
3741
3876
|
* didn't override.
|
|
3742
3877
|
*
|
|
3878
|
+
* The object form also accepts `darkHue` / `darkSaturation` — a separate
|
|
3879
|
+
* seed pair for the dark and dark-high-contrast schemes. Both default to
|
|
3880
|
+
* their light counterparts, and an explicit `darkSaturation` opts the theme
|
|
3881
|
+
* out of the global `darkDesaturation` reduction.
|
|
3882
|
+
*
|
|
3743
3883
|
* @example
|
|
3744
3884
|
* ```ts
|
|
3745
3885
|
* const primary = glaze(280, 80);
|
|
3746
3886
|
* // or shorthand:
|
|
3747
3887
|
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
3888
|
+
* // with a retuned dark seed:
|
|
3889
|
+
* const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
|
|
3748
3890
|
* // with config override:
|
|
3749
3891
|
* const raw = glaze(280, 80, { lightTone: false });
|
|
3750
3892
|
* ```
|
|
3751
3893
|
*/
|
|
3752
3894
|
function glaze(hueOrOptions, saturation, config) {
|
|
3753
|
-
if (typeof hueOrOptions === "number") return createTheme(
|
|
3754
|
-
|
|
3895
|
+
if (typeof hueOrOptions === "number") return createTheme({
|
|
3896
|
+
hue: hueOrOptions,
|
|
3897
|
+
saturation: saturation ?? 100
|
|
3898
|
+
}, void 0, config);
|
|
3899
|
+
return createTheme(hueOrOptions, void 0, config);
|
|
3755
3900
|
}
|
|
3756
3901
|
/** Configure global glaze settings. */
|
|
3757
3902
|
glaze.configure = function configure$1(config) {
|
|
@@ -3865,7 +4010,10 @@ glaze.fromHex = function fromHex(hex) {
|
|
|
3865
4010
|
const rgb = parseHex(hex);
|
|
3866
4011
|
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
3867
4012
|
const [h, s] = srgbToOkhsl(rgb);
|
|
3868
|
-
return createTheme(
|
|
4013
|
+
return createTheme({
|
|
4014
|
+
hue: h,
|
|
4015
|
+
saturation: s * 100
|
|
4016
|
+
});
|
|
3869
4017
|
};
|
|
3870
4018
|
/**
|
|
3871
4019
|
* Create a theme from RGB values (0–255).
|
|
@@ -3877,7 +4025,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
3877
4025
|
g / 255,
|
|
3878
4026
|
b / 255
|
|
3879
4027
|
]);
|
|
3880
|
-
return createTheme(
|
|
4028
|
+
return createTheme({
|
|
4029
|
+
hue: h,
|
|
4030
|
+
saturation: s * 100
|
|
4031
|
+
});
|
|
3881
4032
|
};
|
|
3882
4033
|
/**
|
|
3883
4034
|
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|