@tenphi/glaze 1.1.1 → 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 +239 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -5
- package/dist/index.d.mts +96 -5
- package/dist/index.mjs +239 -91
- package/dist/index.mjs.map +1 -1
- package/docs/api.md +85 -3
- package/docs/methodology.md +28 -0
- package/docs/okhst.md +21 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1990,24 +1990,64 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
1990
1990
|
if (mirrored >= 0 && mirrored <= 100) return -delta;
|
|
1991
1991
|
return delta;
|
|
1992
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);
|
|
2006
|
+
}
|
|
1993
2007
|
function resolveRootColor(def, isHighContrast) {
|
|
1994
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;
|
|
1995
2035
|
return {
|
|
1996
|
-
|
|
1997
|
-
|
|
2036
|
+
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2037
|
+
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
1998
2038
|
};
|
|
1999
2039
|
}
|
|
2000
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark,
|
|
2040
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
|
|
2001
2041
|
const baseName = def.base;
|
|
2002
2042
|
const baseResolved = ctx.resolved.get(baseName);
|
|
2003
2043
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
2004
2044
|
const mode = def.mode ?? "auto";
|
|
2005
|
-
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2006
2045
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2007
2046
|
const pastel = effectivePastel;
|
|
2008
2047
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2009
2048
|
const baseTone = baseVariant.t * 100;
|
|
2010
2049
|
let preferredTone;
|
|
2050
|
+
let isExtreme = false;
|
|
2011
2051
|
const rawTone = def.tone;
|
|
2012
2052
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2013
2053
|
else {
|
|
@@ -2016,22 +2056,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2016
2056
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2017
2057
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2018
2058
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2019
|
-
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
|
+
}
|
|
2020
2066
|
}
|
|
2021
2067
|
const rawContrast = def.contrast;
|
|
2022
2068
|
if (rawContrast !== void 0) {
|
|
2023
2069
|
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2024
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
2025
2070
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2026
2071
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2027
2072
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2028
2073
|
let initialDirection;
|
|
2029
2074
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2030
2075
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2076
|
+
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2031
2077
|
const result = findToneForContrast({
|
|
2032
|
-
hue:
|
|
2033
|
-
saturation:
|
|
2034
|
-
preferredTone:
|
|
2078
|
+
hue: channels.hue,
|
|
2079
|
+
saturation: channels.saturation,
|
|
2080
|
+
preferredTone: seedTone,
|
|
2035
2081
|
baseLinearRgb,
|
|
2036
2082
|
contrast: resolvedContrast,
|
|
2037
2083
|
toneRange: [0, 1],
|
|
@@ -2040,15 +2086,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2040
2086
|
pastel
|
|
2041
2087
|
});
|
|
2042
2088
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2043
|
-
return
|
|
2044
|
-
tone: result.tone * 100,
|
|
2045
|
-
satFactor
|
|
2046
|
-
};
|
|
2089
|
+
return result.tone * 100;
|
|
2047
2090
|
}
|
|
2048
|
-
return
|
|
2049
|
-
tone: clamp(preferredTone, 0, 100),
|
|
2050
|
-
satFactor
|
|
2051
|
-
};
|
|
2091
|
+
return clamp(preferredTone, 0, 100);
|
|
2052
2092
|
}
|
|
2053
2093
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2054
2094
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -2056,27 +2096,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2056
2096
|
const regDef = def;
|
|
2057
2097
|
const mode = regDef.mode ?? "auto";
|
|
2058
2098
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
2059
|
-
const
|
|
2099
|
+
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2060
2100
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2061
2101
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2062
|
-
|
|
2063
|
-
let satFactor;
|
|
2064
|
-
if (isRoot) {
|
|
2065
|
-
const root = resolveRootColor(regDef, isHighContrast);
|
|
2066
|
-
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
2067
|
-
satFactor = root.satFactor;
|
|
2068
|
-
} else {
|
|
2069
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
2070
|
-
finalTone = dep.tone;
|
|
2071
|
-
satFactor = dep.satFactor;
|
|
2072
|
-
}
|
|
2073
|
-
const baseSat = satFactor * ctx.saturation / 100;
|
|
2074
|
-
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
2075
|
-
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);
|
|
2076
2103
|
return {
|
|
2077
|
-
h:
|
|
2078
|
-
s:
|
|
2079
|
-
t:
|
|
2104
|
+
h: channels.hue,
|
|
2105
|
+
s: channels.saturation,
|
|
2106
|
+
t: clamp(finalTone / 100, 0, 1),
|
|
2080
2107
|
alpha: regDef.opacity ?? 1,
|
|
2081
2108
|
pastel
|
|
2082
2109
|
};
|
|
@@ -2276,12 +2303,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2276
2303
|
}
|
|
2277
2304
|
}
|
|
2278
2305
|
}
|
|
2279
|
-
function resolveAllColors(
|
|
2306
|
+
function resolveAllColors(seed, defs, config, externalBases) {
|
|
2280
2307
|
validateColorDefs(defs, externalBases);
|
|
2281
2308
|
const order = topoSort(defs);
|
|
2282
2309
|
const ctx = {
|
|
2283
|
-
|
|
2284
|
-
saturation,
|
|
2310
|
+
...seed,
|
|
2285
2311
|
defs,
|
|
2286
2312
|
resolved: /* @__PURE__ */ new Map(),
|
|
2287
2313
|
config,
|
|
@@ -2314,8 +2340,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
2314
2340
|
/**
|
|
2315
2341
|
* Hue channel planning for `splitHue` exports.
|
|
2316
2342
|
*
|
|
2317
|
-
* Builds per-color hue var references and
|
|
2318
|
-
*
|
|
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`.
|
|
2319
2349
|
*/
|
|
2320
2350
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2321
2351
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2324,7 +2354,11 @@ function cssProp(prefix, name, suffix) {
|
|
|
2324
2354
|
function isAchromatic(v) {
|
|
2325
2355
|
return v.s <= ACHROMATIC_EPSILON;
|
|
2326
2356
|
}
|
|
2327
|
-
|
|
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) {
|
|
2328
2362
|
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2329
2363
|
hueVar: "",
|
|
2330
2364
|
inline: true,
|
|
@@ -2332,13 +2366,25 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2332
2366
|
};
|
|
2333
2367
|
const regDef = def;
|
|
2334
2368
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2335
|
-
|
|
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 {
|
|
2336
2383
|
hueVar: baseHueVar,
|
|
2337
2384
|
inline: false,
|
|
2338
2385
|
declarations: []
|
|
2339
2386
|
};
|
|
2340
|
-
const parsed = parseRelativeOrAbsolute(
|
|
2341
|
-
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2387
|
+
const parsed = parseRelativeOrAbsolute(authored);
|
|
2342
2388
|
if (parsed.relative) {
|
|
2343
2389
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2344
2390
|
const magnitude = Math.abs(parsed.value);
|
|
@@ -2362,13 +2408,13 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2362
2408
|
}]
|
|
2363
2409
|
};
|
|
2364
2410
|
}
|
|
2365
|
-
function standaloneHuePlan(name, variant, ctx) {
|
|
2411
|
+
function standaloneHuePlan(name, variant, ctx, scheme) {
|
|
2366
2412
|
if (isAchromatic(variant)) return {
|
|
2367
2413
|
hueVar: "",
|
|
2368
2414
|
inline: true,
|
|
2369
2415
|
declarations: []
|
|
2370
2416
|
};
|
|
2371
|
-
const hue = ctx.resolvedHue ?? variant.h;
|
|
2417
|
+
const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
|
|
2372
2418
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2373
2419
|
return {
|
|
2374
2420
|
hueVar: `var(${prop})`,
|
|
@@ -2379,12 +2425,12 @@ function standaloneHuePlan(name, variant, ctx) {
|
|
|
2379
2425
|
}]
|
|
2380
2426
|
};
|
|
2381
2427
|
}
|
|
2382
|
-
function buildHuePlan(name, def, variant, ctx) {
|
|
2383
|
-
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2384
|
-
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);
|
|
2385
2431
|
}
|
|
2386
2432
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2387
|
-
function collectHueDeclarations(resolved, ctx) {
|
|
2433
|
+
function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
2388
2434
|
if (ctx.emitDeclarations === false) return [];
|
|
2389
2435
|
const seen = /* @__PURE__ */ new Set();
|
|
2390
2436
|
const out = [];
|
|
@@ -2395,15 +2441,32 @@ function collectHueDeclarations(resolved, ctx) {
|
|
|
2395
2441
|
};
|
|
2396
2442
|
if (ctx.mode === "theme") push({
|
|
2397
2443
|
prop: `--${ctx.baseName}-hue`,
|
|
2398
|
-
value: String(ctx
|
|
2444
|
+
value: String(schemeSeedHue(ctx, scheme))
|
|
2399
2445
|
});
|
|
2400
2446
|
for (const [name, color] of resolved) {
|
|
2401
2447
|
const def = ctx.defs[name];
|
|
2402
|
-
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2448
|
+
const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
|
|
2403
2449
|
for (const decl of plan.declarations) push(decl);
|
|
2404
2450
|
}
|
|
2405
2451
|
return out;
|
|
2406
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
|
+
}
|
|
2407
2470
|
function buildHuePlans(resolved, ctx) {
|
|
2408
2471
|
const plans = /* @__PURE__ */ new Map();
|
|
2409
2472
|
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
@@ -2477,17 +2540,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2477
2540
|
const tokens = {};
|
|
2478
2541
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2479
2542
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2480
|
-
const
|
|
2481
|
-
if (
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
}
|
|
2488
|
-
const colorKey = `#${prefix}${name}`;
|
|
2489
|
-
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
|
+
};
|
|
2490
2550
|
}
|
|
2551
|
+
for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2491
2552
|
return tokens;
|
|
2492
2553
|
}
|
|
2493
2554
|
for (const [name, color] of resolved) {
|
|
@@ -2536,7 +2597,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2536
2597
|
darkContrast: []
|
|
2537
2598
|
};
|
|
2538
2599
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2539
|
-
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
|
+
}
|
|
2540
2607
|
for (const [name, color] of resolved) {
|
|
2541
2608
|
const prop = `--${prefix}${name}${suffix}`;
|
|
2542
2609
|
const plan = huePlans?.get(name);
|
|
@@ -2973,6 +3040,13 @@ function validateStructuredInput(input) {
|
|
|
2973
3040
|
if (input.saturationFactor !== void 0) {
|
|
2974
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}).`);
|
|
2975
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
|
+
}
|
|
2976
3050
|
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
2977
3051
|
}
|
|
2978
3052
|
/**
|
|
@@ -3019,11 +3093,27 @@ function extractOkhslFromValue(value) {
|
|
|
3019
3093
|
return value;
|
|
3020
3094
|
}
|
|
3021
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
|
+
/**
|
|
3022
3108
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
3023
3109
|
*
|
|
3024
3110
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
3025
3111
|
* across every value-shorthand form.
|
|
3026
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
|
+
*
|
|
3027
3117
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
3028
3118
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
3029
3119
|
* the seed pinned to the literal user-provided color across all four
|
|
@@ -3041,10 +3131,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3041
3131
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
3042
3132
|
const primary = userName ?? STANDALONE_VALUE;
|
|
3043
3133
|
const seedTone = toTone(main.l);
|
|
3134
|
+
const primaryTone = toneOption ?? seedTone;
|
|
3044
3135
|
const valueDef = {
|
|
3045
3136
|
hue: relativeHue,
|
|
3046
3137
|
saturation: options?.saturationFactor,
|
|
3047
|
-
|
|
3138
|
+
darkHue: options?.darkHue,
|
|
3139
|
+
darkSaturation: options?.darkSaturationFactor,
|
|
3140
|
+
tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
|
|
3048
3141
|
contrast: options?.contrast,
|
|
3049
3142
|
mode: options?.mode ?? "auto",
|
|
3050
3143
|
autoFlip: options?.autoFlip,
|
|
@@ -3061,13 +3154,16 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3061
3154
|
mode: "static"
|
|
3062
3155
|
};
|
|
3063
3156
|
return {
|
|
3064
|
-
|
|
3065
|
-
|
|
3157
|
+
seed: {
|
|
3158
|
+
hue: seedHue,
|
|
3159
|
+
saturation: seedSaturation,
|
|
3160
|
+
darkSaturation: options?.darkSaturation
|
|
3161
|
+
},
|
|
3066
3162
|
defs,
|
|
3067
3163
|
primary
|
|
3068
3164
|
};
|
|
3069
3165
|
}
|
|
3070
|
-
function createColorTokenFromDefs(
|
|
3166
|
+
function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
|
|
3071
3167
|
let cache = null;
|
|
3072
3168
|
function getEffectiveConfig() {
|
|
3073
3169
|
const version = getConfigVersion();
|
|
@@ -3084,7 +3180,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3084
3180
|
const version = getConfigVersion();
|
|
3085
3181
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3086
3182
|
const effectiveConfig = getEffectiveConfig();
|
|
3087
|
-
const map = resolveAllColors(
|
|
3183
|
+
const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
3088
3184
|
cache = {
|
|
3089
3185
|
map,
|
|
3090
3186
|
version,
|
|
@@ -3122,7 +3218,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3122
3218
|
if (options.splitHue && format === "oklch") {
|
|
3123
3219
|
assertAllPastel(renamed, resolveModes());
|
|
3124
3220
|
channelCtx = {
|
|
3125
|
-
seedHue,
|
|
3221
|
+
seedHue: seed.hue,
|
|
3126
3222
|
baseName: options.name,
|
|
3127
3223
|
prefix: "",
|
|
3128
3224
|
defs: { [options.name]: defs[primary] },
|
|
@@ -3206,8 +3302,10 @@ function createColorToken(input, configOverride) {
|
|
|
3206
3302
|
const hasExternalBase = baseToken !== void 0;
|
|
3207
3303
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
3208
3304
|
const defs = { [primary]: {
|
|
3209
|
-
tone: input.tone,
|
|
3305
|
+
tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
|
|
3210
3306
|
saturation: input.saturationFactor,
|
|
3307
|
+
darkHue: input.darkHue,
|
|
3308
|
+
darkSaturation: input.darkSaturationFactor,
|
|
3211
3309
|
mode: input.mode ?? "auto",
|
|
3212
3310
|
autoFlip: input.autoFlip,
|
|
3213
3311
|
contrast: input.contrast,
|
|
@@ -3216,16 +3314,17 @@ function createColorToken(input, configOverride) {
|
|
|
3216
3314
|
role: input.role,
|
|
3217
3315
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
3218
3316
|
} };
|
|
3219
|
-
if (needsSeedAnchor) {
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
mode: "static"
|
|
3225
|
-
};
|
|
3226
|
-
}
|
|
3317
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
3318
|
+
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
3319
|
+
saturation: 1,
|
|
3320
|
+
mode: "static"
|
|
3321
|
+
};
|
|
3227
3322
|
const localOverride = configOverride;
|
|
3228
|
-
return createColorTokenFromDefs(
|
|
3323
|
+
return createColorTokenFromDefs({
|
|
3324
|
+
hue: input.hue,
|
|
3325
|
+
saturation: input.saturation,
|
|
3326
|
+
darkSaturation: input.darkSaturation
|
|
3327
|
+
}, defs, primary, localOverride, baseToken, (exportArg) => ({
|
|
3229
3328
|
kind: "color",
|
|
3230
3329
|
version: GLAZE_EXPORT_VERSION,
|
|
3231
3330
|
form: "structured",
|
|
@@ -3236,9 +3335,9 @@ function createColorToken(input, configOverride) {
|
|
|
3236
3335
|
function createColorTokenFromValue(value, options, configOverride) {
|
|
3237
3336
|
const main = extractOkhslFromValue(value);
|
|
3238
3337
|
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
3239
|
-
const {
|
|
3338
|
+
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
3240
3339
|
const localOverride = sparseValueFormLocal(configOverride);
|
|
3241
|
-
return createColorTokenFromDefs(
|
|
3340
|
+
return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
|
|
3242
3341
|
kind: "color",
|
|
3243
3342
|
version: GLAZE_EXPORT_VERSION,
|
|
3244
3343
|
form: "value",
|
|
@@ -3258,6 +3357,9 @@ function buildOverridesExport(options, exportArg) {
|
|
|
3258
3357
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
3259
3358
|
if (options.tone !== void 0) out.tone = options.tone;
|
|
3260
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;
|
|
3261
3363
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
3262
3364
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
3263
3365
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
@@ -3275,6 +3377,9 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
3275
3377
|
tone: input.tone
|
|
3276
3378
|
};
|
|
3277
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;
|
|
3278
3383
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
3279
3384
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
3280
3385
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
@@ -3297,6 +3402,9 @@ function rehydrateOverrides(data) {
|
|
|
3297
3402
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
3298
3403
|
if (data.tone !== void 0) out.tone = data.tone;
|
|
3299
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;
|
|
3300
3408
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3301
3409
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3302
3410
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
@@ -3314,6 +3422,9 @@ function rehydrateStructuredInput(data) {
|
|
|
3314
3422
|
tone: data.tone
|
|
3315
3423
|
};
|
|
3316
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;
|
|
3317
3428
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3318
3429
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3319
3430
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
@@ -3347,7 +3458,8 @@ function colorFromExport(data) {
|
|
|
3347
3458
|
|
|
3348
3459
|
//#endregion
|
|
3349
3460
|
//#region src/theme.ts
|
|
3350
|
-
function createTheme(
|
|
3461
|
+
function createTheme(seed, initialColors, configOverride) {
|
|
3462
|
+
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
3351
3463
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
3352
3464
|
let cache = null;
|
|
3353
3465
|
function getEffectiveConfig() {
|
|
@@ -3365,7 +3477,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3365
3477
|
const version = getConfigVersion();
|
|
3366
3478
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3367
3479
|
const effectiveConfig = getEffectiveConfig();
|
|
3368
|
-
const map = resolveAllColors(
|
|
3480
|
+
const map = resolveAllColors(seed, colorDefs, effectiveConfig);
|
|
3369
3481
|
cache = {
|
|
3370
3482
|
map,
|
|
3371
3483
|
version,
|
|
@@ -3382,6 +3494,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3382
3494
|
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3383
3495
|
return {
|
|
3384
3496
|
seedHue: hue,
|
|
3497
|
+
darkSeedHue: darkHue,
|
|
3385
3498
|
baseName: options.name ?? "theme",
|
|
3386
3499
|
prefix,
|
|
3387
3500
|
defs: colorDefs,
|
|
@@ -3395,6 +3508,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3395
3508
|
get saturation() {
|
|
3396
3509
|
return saturation;
|
|
3397
3510
|
},
|
|
3511
|
+
get darkHue() {
|
|
3512
|
+
return darkHue;
|
|
3513
|
+
},
|
|
3514
|
+
get darkSaturation() {
|
|
3515
|
+
return darkSaturation;
|
|
3516
|
+
},
|
|
3398
3517
|
getConfig() {
|
|
3399
3518
|
return getEffectiveConfig();
|
|
3400
3519
|
},
|
|
@@ -3431,16 +3550,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3431
3550
|
version: GLAZE_EXPORT_VERSION,
|
|
3432
3551
|
hue,
|
|
3433
3552
|
saturation,
|
|
3553
|
+
...darkHue !== void 0 ? { darkHue } : {},
|
|
3554
|
+
...darkSaturation !== void 0 ? { darkSaturation } : {},
|
|
3434
3555
|
colors: structuredClone(colorDefs),
|
|
3435
3556
|
config: freezeConfigForExport(configOverride, override)
|
|
3436
3557
|
};
|
|
3437
3558
|
},
|
|
3438
3559
|
extend(options) {
|
|
3439
|
-
const
|
|
3440
|
-
|
|
3560
|
+
const childSeed = {
|
|
3561
|
+
hue: options.hue ?? hue,
|
|
3562
|
+
saturation: options.saturation ?? saturation,
|
|
3563
|
+
darkHue: options.darkHue ?? darkHue,
|
|
3564
|
+
darkSaturation: options.darkSaturation ?? darkSaturation
|
|
3565
|
+
};
|
|
3441
3566
|
const inheritedColors = {};
|
|
3442
3567
|
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
3443
|
-
return createTheme(
|
|
3568
|
+
return createTheme(childSeed, options.colors ? {
|
|
3444
3569
|
...inheritedColors,
|
|
3445
3570
|
...options.colors
|
|
3446
3571
|
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
@@ -3550,6 +3675,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
|
|
|
3550
3675
|
assertAllPastel(filtered, modes);
|
|
3551
3676
|
return {
|
|
3552
3677
|
seedHue: theme.hue,
|
|
3678
|
+
darkSeedHue: theme.darkHue,
|
|
3553
3679
|
baseName: themeName,
|
|
3554
3680
|
prefix: themedPrefix,
|
|
3555
3681
|
defs: colorMapFromTheme(theme),
|
|
@@ -3581,7 +3707,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
|
3581
3707
|
assertExportKind(data, "theme", factory);
|
|
3582
3708
|
assertExportVersion(data, factory);
|
|
3583
3709
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
3584
|
-
|
|
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);
|
|
3585
3717
|
}
|
|
3586
3718
|
/**
|
|
3587
3719
|
* Rebuild a palette from a `palette.export()` snapshot.
|
|
@@ -3743,18 +3875,28 @@ function createPalette(themes, paletteOptions) {
|
|
|
3743
3875
|
* the theme still reacts to later `configure()` calls for fields it
|
|
3744
3876
|
* didn't override.
|
|
3745
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
|
+
*
|
|
3746
3883
|
* @example
|
|
3747
3884
|
* ```ts
|
|
3748
3885
|
* const primary = glaze(280, 80);
|
|
3749
3886
|
* // or shorthand:
|
|
3750
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 });
|
|
3751
3890
|
* // with config override:
|
|
3752
3891
|
* const raw = glaze(280, 80, { lightTone: false });
|
|
3753
3892
|
* ```
|
|
3754
3893
|
*/
|
|
3755
3894
|
function glaze(hueOrOptions, saturation, config) {
|
|
3756
|
-
if (typeof hueOrOptions === "number") return createTheme(
|
|
3757
|
-
|
|
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);
|
|
3758
3900
|
}
|
|
3759
3901
|
/** Configure global glaze settings. */
|
|
3760
3902
|
glaze.configure = function configure$1(config) {
|
|
@@ -3868,7 +4010,10 @@ glaze.fromHex = function fromHex(hex) {
|
|
|
3868
4010
|
const rgb = parseHex(hex);
|
|
3869
4011
|
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
3870
4012
|
const [h, s] = srgbToOkhsl(rgb);
|
|
3871
|
-
return createTheme(
|
|
4013
|
+
return createTheme({
|
|
4014
|
+
hue: h,
|
|
4015
|
+
saturation: s * 100
|
|
4016
|
+
});
|
|
3872
4017
|
};
|
|
3873
4018
|
/**
|
|
3874
4019
|
* Create a theme from RGB values (0–255).
|
|
@@ -3880,7 +4025,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
3880
4025
|
g / 255,
|
|
3881
4026
|
b / 255
|
|
3882
4027
|
]);
|
|
3883
|
-
return createTheme(
|
|
4028
|
+
return createTheme({
|
|
4029
|
+
hue: h,
|
|
4030
|
+
saturation: s * 100
|
|
4031
|
+
});
|
|
3884
4032
|
};
|
|
3885
4033
|
/**
|
|
3886
4034
|
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|