@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.cjs
CHANGED
|
@@ -1992,24 +1992,64 @@ function applyToneFlip(delta, baseTone, flip) {
|
|
|
1992
1992
|
if (mirrored >= 0 && mirrored <= 100) return -delta;
|
|
1993
1993
|
return delta;
|
|
1994
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Dark position for an extreme tone (`'max'` / `'min'`) anchored to a base.
|
|
1997
|
+
*
|
|
1998
|
+
* A windowed dark remap compresses the base→extreme span, so the pair loses
|
|
1999
|
+
* contrast in dark. Instead, measure the tone shift the light scheme applied
|
|
2000
|
+
* between the base and the extreme, then replay it against the base's dark
|
|
2001
|
+
* tone: `auto` mirrors the sign (both ends invert), `fixed` keeps it. The
|
|
2002
|
+
* result is clamped to `[0, 100]` only — crossing the dark tone window is
|
|
2003
|
+
* intended, since the author asked for the extreme.
|
|
2004
|
+
*/
|
|
2005
|
+
function extremeDarkTone(authorTone, mode, isHighContrast, baseResolved, baseDarkTone, config) {
|
|
2006
|
+
const delta = mapToneForScheme(authorTone, mode, false, isHighContrast, config) - getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2007
|
+
return clamp(baseDarkTone + (mode === "auto" ? -delta : delta), 0, 100);
|
|
2008
|
+
}
|
|
1995
2009
|
function resolveRootColor(def, isHighContrast) {
|
|
1996
2010
|
const rawT = def.tone;
|
|
2011
|
+
return clamp(parseToneValue(isHighContrast ? pairHC(rawT) : pairNormal(rawT)).value, 0, 100);
|
|
2012
|
+
}
|
|
2013
|
+
/**
|
|
2014
|
+
* Effective hue and saturation for one color in one scheme.
|
|
2015
|
+
*
|
|
2016
|
+
* Dark schemes read the dark seed pair and the color's `darkHue` /
|
|
2017
|
+
* `darkSaturation`, each falling back to its light counterpart. An explicit
|
|
2018
|
+
* dark saturation — on the seed or the def — is taken literally; the global
|
|
2019
|
+
* `darkDesaturation` reduction applies only when neither is authored.
|
|
2020
|
+
* `mode: 'static'` pins one value across every scheme, so it skips all of it.
|
|
2021
|
+
*
|
|
2022
|
+
* Shared by `resolveColorForScheme` (the emitted variant) and
|
|
2023
|
+
* `resolveDependentColor` (the contrast solver's inputs) so the solver always
|
|
2024
|
+
* measures the color it is actually going to produce.
|
|
2025
|
+
*/
|
|
2026
|
+
function resolveChannels(def, ctx, isDark) {
|
|
2027
|
+
const mode = def.mode ?? "auto";
|
|
2028
|
+
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2029
|
+
if (!isDark || mode === "static") return {
|
|
2030
|
+
hue: resolveEffectiveHue(ctx.hue, def.hue),
|
|
2031
|
+
saturation: clamp(satFactor * ctx.saturation / 100, 0, 1)
|
|
2032
|
+
};
|
|
2033
|
+
const darkSeedSaturation = ctx.darkSaturation ?? ctx.saturation;
|
|
2034
|
+
const darkFactor = clamp(def.darkSaturation ?? satFactor, 0, 1);
|
|
2035
|
+
const explicitDark = def.darkSaturation !== void 0 || ctx.darkSaturation !== void 0;
|
|
2036
|
+
const raw = darkFactor * darkSeedSaturation / 100;
|
|
1997
2037
|
return {
|
|
1998
|
-
|
|
1999
|
-
|
|
2038
|
+
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2039
|
+
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
2000
2040
|
};
|
|
2001
2041
|
}
|
|
2002
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark,
|
|
2042
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
|
|
2003
2043
|
const baseName = def.base;
|
|
2004
2044
|
const baseResolved = ctx.resolved.get(baseName);
|
|
2005
2045
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
2006
2046
|
const mode = def.mode ?? "auto";
|
|
2007
|
-
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2008
2047
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2009
2048
|
const pastel = effectivePastel;
|
|
2010
2049
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2011
2050
|
const baseTone = baseVariant.t * 100;
|
|
2012
2051
|
let preferredTone;
|
|
2052
|
+
let isExtreme = false;
|
|
2013
2053
|
const rawTone = def.tone;
|
|
2014
2054
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2015
2055
|
else {
|
|
@@ -2018,22 +2058,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2018
2058
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2019
2059
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2020
2060
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2021
|
-
else
|
|
2061
|
+
else if (parsed.kind === "extreme" && isDark && mode !== "static") {
|
|
2062
|
+
isExtreme = true;
|
|
2063
|
+
preferredTone = extremeDarkTone(parsed.value, mode, isHighContrast, baseResolved, baseTone, ctx.config);
|
|
2064
|
+
} else {
|
|
2065
|
+
isExtreme = parsed.kind === "extreme";
|
|
2066
|
+
preferredTone = mapToneForScheme(parsed.value, mode, isDark, isHighContrast, ctx.config);
|
|
2067
|
+
}
|
|
2022
2068
|
}
|
|
2023
2069
|
const rawContrast = def.contrast;
|
|
2024
2070
|
if (rawContrast !== void 0) {
|
|
2025
2071
|
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2026
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
2027
2072
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2028
2073
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2029
2074
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2030
2075
|
let initialDirection;
|
|
2031
2076
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2032
2077
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2078
|
+
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2033
2079
|
const result = findToneForContrast({
|
|
2034
|
-
hue:
|
|
2035
|
-
saturation:
|
|
2036
|
-
preferredTone:
|
|
2080
|
+
hue: channels.hue,
|
|
2081
|
+
saturation: channels.saturation,
|
|
2082
|
+
preferredTone: seedTone,
|
|
2037
2083
|
baseLinearRgb,
|
|
2038
2084
|
contrast: resolvedContrast,
|
|
2039
2085
|
toneRange: [0, 1],
|
|
@@ -2042,15 +2088,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2042
2088
|
pastel
|
|
2043
2089
|
});
|
|
2044
2090
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2045
|
-
return
|
|
2046
|
-
tone: result.tone * 100,
|
|
2047
|
-
satFactor
|
|
2048
|
-
};
|
|
2091
|
+
return result.tone * 100;
|
|
2049
2092
|
}
|
|
2050
|
-
return
|
|
2051
|
-
tone: clamp(preferredTone, 0, 100),
|
|
2052
|
-
satFactor
|
|
2053
|
-
};
|
|
2093
|
+
return clamp(preferredTone, 0, 100);
|
|
2054
2094
|
}
|
|
2055
2095
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2056
2096
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -2058,27 +2098,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2058
2098
|
const regDef = def;
|
|
2059
2099
|
const mode = regDef.mode ?? "auto";
|
|
2060
2100
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
2061
|
-
const
|
|
2101
|
+
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2062
2102
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2063
2103
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2064
|
-
|
|
2065
|
-
let satFactor;
|
|
2066
|
-
if (isRoot) {
|
|
2067
|
-
const root = resolveRootColor(regDef, isHighContrast);
|
|
2068
|
-
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
2069
|
-
satFactor = root.satFactor;
|
|
2070
|
-
} else {
|
|
2071
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
2072
|
-
finalTone = dep.tone;
|
|
2073
|
-
satFactor = dep.satFactor;
|
|
2074
|
-
}
|
|
2075
|
-
const baseSat = satFactor * ctx.saturation / 100;
|
|
2076
|
-
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
2077
|
-
const toneFraction = clamp(finalTone / 100, 0, 1);
|
|
2104
|
+
const finalTone = isRoot ? mapToneForScheme(resolveRootColor(regDef, isHighContrast), mode, isDark, isHighContrast, ctx.config) : resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, channels, polarity, pastel);
|
|
2078
2105
|
return {
|
|
2079
|
-
h:
|
|
2080
|
-
s:
|
|
2081
|
-
t:
|
|
2106
|
+
h: channels.hue,
|
|
2107
|
+
s: channels.saturation,
|
|
2108
|
+
t: clamp(finalTone / 100, 0, 1),
|
|
2082
2109
|
alpha: regDef.opacity ?? 1,
|
|
2083
2110
|
pastel
|
|
2084
2111
|
};
|
|
@@ -2278,12 +2305,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2278
2305
|
}
|
|
2279
2306
|
}
|
|
2280
2307
|
}
|
|
2281
|
-
function resolveAllColors(
|
|
2308
|
+
function resolveAllColors(seed, defs, config, externalBases) {
|
|
2282
2309
|
validateColorDefs(defs, externalBases);
|
|
2283
2310
|
const order = topoSort(defs);
|
|
2284
2311
|
const ctx = {
|
|
2285
|
-
|
|
2286
|
-
saturation,
|
|
2312
|
+
...seed,
|
|
2287
2313
|
defs,
|
|
2288
2314
|
resolved: /* @__PURE__ */ new Map(),
|
|
2289
2315
|
config,
|
|
@@ -2316,8 +2342,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
2316
2342
|
/**
|
|
2317
2343
|
* Hue channel planning for `splitHue` exports.
|
|
2318
2344
|
*
|
|
2319
|
-
* Builds per-color hue var references and
|
|
2320
|
-
*
|
|
2345
|
+
* Builds per-color hue var references and the `--*-hue` declarations backing
|
|
2346
|
+
* them for oklch CSS / Tasty output when every color is pastel.
|
|
2347
|
+
*
|
|
2348
|
+
* Hue is scheme-independent unless the theme or a color authors a dark hue,
|
|
2349
|
+
* so declarations are collected per scheme (`'light'` / `'dark'`) and the dark
|
|
2350
|
+
* set is emitted only when it actually differs — see `darkHueDeclarations`.
|
|
2321
2351
|
*/
|
|
2322
2352
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2323
2353
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2326,7 +2356,11 @@ function cssProp(prefix, name, suffix) {
|
|
|
2326
2356
|
function isAchromatic(v) {
|
|
2327
2357
|
return v.s <= ACHROMATIC_EPSILON;
|
|
2328
2358
|
}
|
|
2329
|
-
|
|
2359
|
+
/** Seed hue backing `--{baseName}-hue` in a given scheme. */
|
|
2360
|
+
function schemeSeedHue(ctx, scheme) {
|
|
2361
|
+
return scheme === "dark" ? ctx.darkSeedHue ?? ctx.seedHue : ctx.seedHue;
|
|
2362
|
+
}
|
|
2363
|
+
function themeHuePlan(name, def, variant, ctx, scheme) {
|
|
2330
2364
|
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2331
2365
|
hueVar: "",
|
|
2332
2366
|
inline: true,
|
|
@@ -2334,13 +2368,25 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2334
2368
|
};
|
|
2335
2369
|
const regDef = def;
|
|
2336
2370
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2337
|
-
|
|
2371
|
+
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2372
|
+
if ((regDef.mode ?? "auto") === "static" && schemeSeedHue(ctx, "dark") !== ctx.seedHue) {
|
|
2373
|
+
const pinned = resolveEffectiveHue(ctx.seedHue, regDef.hue);
|
|
2374
|
+
return {
|
|
2375
|
+
hueVar: `var(${prop})`,
|
|
2376
|
+
inline: false,
|
|
2377
|
+
declarations: [{
|
|
2378
|
+
prop,
|
|
2379
|
+
value: String(pinned)
|
|
2380
|
+
}]
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
const authored = scheme === "dark" ? regDef.darkHue ?? regDef.hue : regDef.hue;
|
|
2384
|
+
if (authored === void 0) return {
|
|
2338
2385
|
hueVar: baseHueVar,
|
|
2339
2386
|
inline: false,
|
|
2340
2387
|
declarations: []
|
|
2341
2388
|
};
|
|
2342
|
-
const parsed = parseRelativeOrAbsolute(
|
|
2343
|
-
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2389
|
+
const parsed = parseRelativeOrAbsolute(authored);
|
|
2344
2390
|
if (parsed.relative) {
|
|
2345
2391
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2346
2392
|
const magnitude = Math.abs(parsed.value);
|
|
@@ -2364,13 +2410,13 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2364
2410
|
}]
|
|
2365
2411
|
};
|
|
2366
2412
|
}
|
|
2367
|
-
function standaloneHuePlan(name, variant, ctx) {
|
|
2413
|
+
function standaloneHuePlan(name, variant, ctx, scheme) {
|
|
2368
2414
|
if (isAchromatic(variant)) return {
|
|
2369
2415
|
hueVar: "",
|
|
2370
2416
|
inline: true,
|
|
2371
2417
|
declarations: []
|
|
2372
2418
|
};
|
|
2373
|
-
const hue = ctx.resolvedHue ?? variant.h;
|
|
2419
|
+
const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
|
|
2374
2420
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2375
2421
|
return {
|
|
2376
2422
|
hueVar: `var(${prop})`,
|
|
@@ -2381,12 +2427,12 @@ function standaloneHuePlan(name, variant, ctx) {
|
|
|
2381
2427
|
}]
|
|
2382
2428
|
};
|
|
2383
2429
|
}
|
|
2384
|
-
function buildHuePlan(name, def, variant, ctx) {
|
|
2385
|
-
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2386
|
-
return themeHuePlan(name, def, variant, ctx);
|
|
2430
|
+
function buildHuePlan(name, def, variant, ctx, scheme = "light") {
|
|
2431
|
+
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx, scheme);
|
|
2432
|
+
return themeHuePlan(name, def, variant, ctx, scheme);
|
|
2387
2433
|
}
|
|
2388
2434
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2389
|
-
function collectHueDeclarations(resolved, ctx) {
|
|
2435
|
+
function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
2390
2436
|
if (ctx.emitDeclarations === false) return [];
|
|
2391
2437
|
const seen = /* @__PURE__ */ new Set();
|
|
2392
2438
|
const out = [];
|
|
@@ -2397,15 +2443,32 @@ function collectHueDeclarations(resolved, ctx) {
|
|
|
2397
2443
|
};
|
|
2398
2444
|
if (ctx.mode === "theme") push({
|
|
2399
2445
|
prop: `--${ctx.baseName}-hue`,
|
|
2400
|
-
value: String(ctx
|
|
2446
|
+
value: String(schemeSeedHue(ctx, scheme))
|
|
2401
2447
|
});
|
|
2402
2448
|
for (const [name, color] of resolved) {
|
|
2403
2449
|
const def = ctx.defs[name];
|
|
2404
|
-
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2450
|
+
const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
|
|
2405
2451
|
for (const decl of plan.declarations) push(decl);
|
|
2406
2452
|
}
|
|
2407
2453
|
return out;
|
|
2408
2454
|
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Dark-scheme hue declarations, or `[]` when dark resolves to the same hues as
|
|
2457
|
+
* light (the common case — no `darkHue` anywhere).
|
|
2458
|
+
*
|
|
2459
|
+
* When they differ, the **whole** set is returned rather than only the entries
|
|
2460
|
+
* whose text changed. A relative declaration reads
|
|
2461
|
+
* `calc(var(--theme-hue) + N)` in both schemes even though its resolved value
|
|
2462
|
+
* moves with the seed, and custom properties substitute `var()` at
|
|
2463
|
+
* computed-value time on the declaring element — so re-declaring
|
|
2464
|
+
* `--{baseName}-hue` alone would leave inherited per-color vars still holding
|
|
2465
|
+
* the light-substituted value.
|
|
2466
|
+
*/
|
|
2467
|
+
function darkHueDeclarations(resolved, ctx) {
|
|
2468
|
+
const dark = collectHueDeclarations(resolved, ctx, "dark");
|
|
2469
|
+
const light = collectHueDeclarations(resolved, ctx, "light");
|
|
2470
|
+
return dark.length === light.length && dark.every((decl, i) => decl.prop === light[i].prop && decl.value === light[i].value) ? [] : dark;
|
|
2471
|
+
}
|
|
2409
2472
|
function buildHuePlans(resolved, ctx) {
|
|
2410
2473
|
const plans = /* @__PURE__ */ new Map();
|
|
2411
2474
|
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
@@ -2479,17 +2542,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2479
2542
|
const tokens = {};
|
|
2480
2543
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2481
2544
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2482
|
-
const
|
|
2483
|
-
if (
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
}
|
|
2490
|
-
const colorKey = `#${prefix}${name}`;
|
|
2491
|
-
tokens[colorKey] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2545
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) tokens[`$${decl.prop.slice(2)}`] = { "": decl.value };
|
|
2546
|
+
if (modes.dark) for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2547
|
+
const key = `$${decl.prop.slice(2)}`;
|
|
2548
|
+
tokens[key] = {
|
|
2549
|
+
...tokens[key],
|
|
2550
|
+
[states.dark]: decl.value
|
|
2551
|
+
};
|
|
2492
2552
|
}
|
|
2553
|
+
for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2493
2554
|
return tokens;
|
|
2494
2555
|
}
|
|
2495
2556
|
for (const [name, color] of resolved) {
|
|
@@ -2538,7 +2599,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2538
2599
|
darkContrast: []
|
|
2539
2600
|
};
|
|
2540
2601
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2541
|
-
if (huePlans !== void 0 && channelCtx !== void 0)
|
|
2602
|
+
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2603
|
+
for (const decl of collectHueDeclarations(resolved, channelCtx)) lines.light.push(`${decl.prop}: ${decl.value};`);
|
|
2604
|
+
for (const decl of darkHueDeclarations(resolved, channelCtx)) {
|
|
2605
|
+
lines.dark.push(`${decl.prop}: ${decl.value};`);
|
|
2606
|
+
lines.darkContrast.push(`${decl.prop}: ${decl.value};`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2542
2609
|
for (const [name, color] of resolved) {
|
|
2543
2610
|
const prop = `--${prefix}${name}${suffix}`;
|
|
2544
2611
|
const plan = huePlans?.get(name);
|
|
@@ -2975,6 +3042,13 @@ function validateStructuredInput(input) {
|
|
|
2975
3042
|
if (input.saturationFactor !== void 0) {
|
|
2976
3043
|
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}).`);
|
|
2977
3044
|
}
|
|
3045
|
+
if (typeof input.darkHue === "number" && !Number.isFinite(input.darkHue)) throw new Error(`glaze.color: structured darkHue must be a finite number (got ${input.darkHue}).`);
|
|
3046
|
+
if (input.darkSaturation !== void 0) {
|
|
3047
|
+
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}).`);
|
|
3048
|
+
}
|
|
3049
|
+
if (input.darkSaturationFactor !== void 0) {
|
|
3050
|
+
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}).`);
|
|
3051
|
+
}
|
|
2978
3052
|
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
2979
3053
|
}
|
|
2980
3054
|
/**
|
|
@@ -3021,11 +3095,27 @@ function extractOkhslFromValue(value) {
|
|
|
3021
3095
|
return value;
|
|
3022
3096
|
}
|
|
3023
3097
|
/**
|
|
3098
|
+
* Resolve `'max'` / `'min'` to their literal author tones (100 / 0).
|
|
3099
|
+
*
|
|
3100
|
+
* The hidden `STANDALONE_SEED` anchor exists only so `contrast` has something
|
|
3101
|
+
* to measure against — it is not an authored base. Pinning the extreme keeps
|
|
3102
|
+
* the token on the plain scheme mapping instead of the base-anchored dark
|
|
3103
|
+
* replay the resolver applies to real `base` references.
|
|
3104
|
+
*/
|
|
3105
|
+
function pinExtremeTone(tone) {
|
|
3106
|
+
const pin = (value) => value === "max" ? 100 : value === "min" ? 0 : value;
|
|
3107
|
+
return Array.isArray(tone) ? [pin(tone[0]), pin(tone[1])] : pin(tone);
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3024
3110
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
3025
3111
|
*
|
|
3026
3112
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
3027
3113
|
* across every value-shorthand form.
|
|
3028
3114
|
*
|
|
3115
|
+
* An absolute `hue` override becomes the seed while a relative one becomes a
|
|
3116
|
+
* def field; `darkHue` always lands on the def in either form, so an absolute
|
|
3117
|
+
* `darkHue` wins outright instead of being re-offset by a relative `hue`.
|
|
3118
|
+
*
|
|
3029
3119
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
3030
3120
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
3031
3121
|
* the seed pinned to the literal user-provided color across all four
|
|
@@ -3043,10 +3133,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3043
3133
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
3044
3134
|
const primary = userName ?? STANDALONE_VALUE;
|
|
3045
3135
|
const seedTone = toTone(main.l);
|
|
3136
|
+
const primaryTone = toneOption ?? seedTone;
|
|
3046
3137
|
const valueDef = {
|
|
3047
3138
|
hue: relativeHue,
|
|
3048
3139
|
saturation: options?.saturationFactor,
|
|
3049
|
-
|
|
3140
|
+
darkHue: options?.darkHue,
|
|
3141
|
+
darkSaturation: options?.darkSaturationFactor,
|
|
3142
|
+
tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
|
|
3050
3143
|
contrast: options?.contrast,
|
|
3051
3144
|
mode: options?.mode ?? "auto",
|
|
3052
3145
|
autoFlip: options?.autoFlip,
|
|
@@ -3063,13 +3156,16 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3063
3156
|
mode: "static"
|
|
3064
3157
|
};
|
|
3065
3158
|
return {
|
|
3066
|
-
|
|
3067
|
-
|
|
3159
|
+
seed: {
|
|
3160
|
+
hue: seedHue,
|
|
3161
|
+
saturation: seedSaturation,
|
|
3162
|
+
darkSaturation: options?.darkSaturation
|
|
3163
|
+
},
|
|
3068
3164
|
defs,
|
|
3069
3165
|
primary
|
|
3070
3166
|
};
|
|
3071
3167
|
}
|
|
3072
|
-
function createColorTokenFromDefs(
|
|
3168
|
+
function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
|
|
3073
3169
|
let cache = null;
|
|
3074
3170
|
function getEffectiveConfig() {
|
|
3075
3171
|
const version = getConfigVersion();
|
|
@@ -3086,7 +3182,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3086
3182
|
const version = getConfigVersion();
|
|
3087
3183
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3088
3184
|
const effectiveConfig = getEffectiveConfig();
|
|
3089
|
-
const map = resolveAllColors(
|
|
3185
|
+
const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
3090
3186
|
cache = {
|
|
3091
3187
|
map,
|
|
3092
3188
|
version,
|
|
@@ -3124,7 +3220,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3124
3220
|
if (options.splitHue && format === "oklch") {
|
|
3125
3221
|
assertAllPastel(renamed, resolveModes());
|
|
3126
3222
|
channelCtx = {
|
|
3127
|
-
seedHue,
|
|
3223
|
+
seedHue: seed.hue,
|
|
3128
3224
|
baseName: options.name,
|
|
3129
3225
|
prefix: "",
|
|
3130
3226
|
defs: { [options.name]: defs[primary] },
|
|
@@ -3208,8 +3304,10 @@ function createColorToken(input, configOverride) {
|
|
|
3208
3304
|
const hasExternalBase = baseToken !== void 0;
|
|
3209
3305
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
3210
3306
|
const defs = { [primary]: {
|
|
3211
|
-
tone: input.tone,
|
|
3307
|
+
tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
|
|
3212
3308
|
saturation: input.saturationFactor,
|
|
3309
|
+
darkHue: input.darkHue,
|
|
3310
|
+
darkSaturation: input.darkSaturationFactor,
|
|
3213
3311
|
mode: input.mode ?? "auto",
|
|
3214
3312
|
autoFlip: input.autoFlip,
|
|
3215
3313
|
contrast: input.contrast,
|
|
@@ -3218,16 +3316,17 @@ function createColorToken(input, configOverride) {
|
|
|
3218
3316
|
role: input.role,
|
|
3219
3317
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
3220
3318
|
} };
|
|
3221
|
-
if (needsSeedAnchor) {
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
mode: "static"
|
|
3227
|
-
};
|
|
3228
|
-
}
|
|
3319
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
3320
|
+
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
3321
|
+
saturation: 1,
|
|
3322
|
+
mode: "static"
|
|
3323
|
+
};
|
|
3229
3324
|
const localOverride = configOverride;
|
|
3230
|
-
return createColorTokenFromDefs(
|
|
3325
|
+
return createColorTokenFromDefs({
|
|
3326
|
+
hue: input.hue,
|
|
3327
|
+
saturation: input.saturation,
|
|
3328
|
+
darkSaturation: input.darkSaturation
|
|
3329
|
+
}, defs, primary, localOverride, baseToken, (exportArg) => ({
|
|
3231
3330
|
kind: "color",
|
|
3232
3331
|
version: GLAZE_EXPORT_VERSION,
|
|
3233
3332
|
form: "structured",
|
|
@@ -3238,9 +3337,9 @@ function createColorToken(input, configOverride) {
|
|
|
3238
3337
|
function createColorTokenFromValue(value, options, configOverride) {
|
|
3239
3338
|
const main = extractOkhslFromValue(value);
|
|
3240
3339
|
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
3241
|
-
const {
|
|
3340
|
+
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
3242
3341
|
const localOverride = sparseValueFormLocal(configOverride);
|
|
3243
|
-
return createColorTokenFromDefs(
|
|
3342
|
+
return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
|
|
3244
3343
|
kind: "color",
|
|
3245
3344
|
version: GLAZE_EXPORT_VERSION,
|
|
3246
3345
|
form: "value",
|
|
@@ -3260,6 +3359,9 @@ function buildOverridesExport(options, exportArg) {
|
|
|
3260
3359
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
3261
3360
|
if (options.tone !== void 0) out.tone = options.tone;
|
|
3262
3361
|
if (options.saturationFactor !== void 0) out.saturationFactor = options.saturationFactor;
|
|
3362
|
+
if (options.darkHue !== void 0) out.darkHue = options.darkHue;
|
|
3363
|
+
if (options.darkSaturation !== void 0) out.darkSaturation = options.darkSaturation;
|
|
3364
|
+
if (options.darkSaturationFactor !== void 0) out.darkSaturationFactor = options.darkSaturationFactor;
|
|
3263
3365
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
3264
3366
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
3265
3367
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
@@ -3277,6 +3379,9 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
3277
3379
|
tone: input.tone
|
|
3278
3380
|
};
|
|
3279
3381
|
if (input.saturationFactor !== void 0) out.saturationFactor = input.saturationFactor;
|
|
3382
|
+
if (input.darkHue !== void 0) out.darkHue = input.darkHue;
|
|
3383
|
+
if (input.darkSaturation !== void 0) out.darkSaturation = input.darkSaturation;
|
|
3384
|
+
if (input.darkSaturationFactor !== void 0) out.darkSaturationFactor = input.darkSaturationFactor;
|
|
3280
3385
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
3281
3386
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
3282
3387
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
@@ -3299,6 +3404,9 @@ function rehydrateOverrides(data) {
|
|
|
3299
3404
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
3300
3405
|
if (data.tone !== void 0) out.tone = data.tone;
|
|
3301
3406
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3407
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3408
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3409
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3302
3410
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3303
3411
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3304
3412
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
@@ -3316,6 +3424,9 @@ function rehydrateStructuredInput(data) {
|
|
|
3316
3424
|
tone: data.tone
|
|
3317
3425
|
};
|
|
3318
3426
|
if (data.saturationFactor !== void 0) out.saturationFactor = data.saturationFactor;
|
|
3427
|
+
if (data.darkHue !== void 0) out.darkHue = data.darkHue;
|
|
3428
|
+
if (data.darkSaturation !== void 0) out.darkSaturation = data.darkSaturation;
|
|
3429
|
+
if (data.darkSaturationFactor !== void 0) out.darkSaturationFactor = data.darkSaturationFactor;
|
|
3319
3430
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3320
3431
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3321
3432
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
@@ -3349,7 +3460,8 @@ function colorFromExport(data) {
|
|
|
3349
3460
|
|
|
3350
3461
|
//#endregion
|
|
3351
3462
|
//#region src/theme.ts
|
|
3352
|
-
function createTheme(
|
|
3463
|
+
function createTheme(seed, initialColors, configOverride) {
|
|
3464
|
+
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
3353
3465
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
3354
3466
|
let cache = null;
|
|
3355
3467
|
function getEffectiveConfig() {
|
|
@@ -3367,7 +3479,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3367
3479
|
const version = getConfigVersion();
|
|
3368
3480
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3369
3481
|
const effectiveConfig = getEffectiveConfig();
|
|
3370
|
-
const map = resolveAllColors(
|
|
3482
|
+
const map = resolveAllColors(seed, colorDefs, effectiveConfig);
|
|
3371
3483
|
cache = {
|
|
3372
3484
|
map,
|
|
3373
3485
|
version,
|
|
@@ -3384,6 +3496,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3384
3496
|
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3385
3497
|
return {
|
|
3386
3498
|
seedHue: hue,
|
|
3499
|
+
darkSeedHue: darkHue,
|
|
3387
3500
|
baseName: options.name ?? "theme",
|
|
3388
3501
|
prefix,
|
|
3389
3502
|
defs: colorDefs,
|
|
@@ -3397,6 +3510,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3397
3510
|
get saturation() {
|
|
3398
3511
|
return saturation;
|
|
3399
3512
|
},
|
|
3513
|
+
get darkHue() {
|
|
3514
|
+
return darkHue;
|
|
3515
|
+
},
|
|
3516
|
+
get darkSaturation() {
|
|
3517
|
+
return darkSaturation;
|
|
3518
|
+
},
|
|
3400
3519
|
getConfig() {
|
|
3401
3520
|
return getEffectiveConfig();
|
|
3402
3521
|
},
|
|
@@ -3433,16 +3552,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3433
3552
|
version: GLAZE_EXPORT_VERSION,
|
|
3434
3553
|
hue,
|
|
3435
3554
|
saturation,
|
|
3555
|
+
...darkHue !== void 0 ? { darkHue } : {},
|
|
3556
|
+
...darkSaturation !== void 0 ? { darkSaturation } : {},
|
|
3436
3557
|
colors: structuredClone(colorDefs),
|
|
3437
3558
|
config: freezeConfigForExport(configOverride, override)
|
|
3438
3559
|
};
|
|
3439
3560
|
},
|
|
3440
3561
|
extend(options) {
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3562
|
+
const childSeed = {
|
|
3563
|
+
hue: options.hue ?? hue,
|
|
3564
|
+
saturation: options.saturation ?? saturation,
|
|
3565
|
+
darkHue: options.darkHue ?? darkHue,
|
|
3566
|
+
darkSaturation: options.darkSaturation ?? darkSaturation
|
|
3567
|
+
};
|
|
3443
3568
|
const inheritedColors = {};
|
|
3444
3569
|
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
3445
|
-
return createTheme(
|
|
3570
|
+
return createTheme(childSeed, options.colors ? {
|
|
3446
3571
|
...inheritedColors,
|
|
3447
3572
|
...options.colors
|
|
3448
3573
|
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
@@ -3552,6 +3677,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
|
|
|
3552
3677
|
assertAllPastel(filtered, modes);
|
|
3553
3678
|
return {
|
|
3554
3679
|
seedHue: theme.hue,
|
|
3680
|
+
darkSeedHue: theme.darkHue,
|
|
3555
3681
|
baseName: themeName,
|
|
3556
3682
|
prefix: themedPrefix,
|
|
3557
3683
|
defs: colorMapFromTheme(theme),
|
|
@@ -3583,7 +3709,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
|
3583
3709
|
assertExportKind(data, "theme", factory);
|
|
3584
3710
|
assertExportVersion(data, factory);
|
|
3585
3711
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
3586
|
-
|
|
3712
|
+
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])}).`);
|
|
3713
|
+
return createTheme({
|
|
3714
|
+
hue: data.hue,
|
|
3715
|
+
saturation: data.saturation,
|
|
3716
|
+
darkHue: data.darkHue,
|
|
3717
|
+
darkSaturation: data.darkSaturation
|
|
3718
|
+
}, data.colors, data.config);
|
|
3587
3719
|
}
|
|
3588
3720
|
/**
|
|
3589
3721
|
* Rebuild a palette from a `palette.export()` snapshot.
|
|
@@ -3745,18 +3877,28 @@ function createPalette(themes, paletteOptions) {
|
|
|
3745
3877
|
* the theme still reacts to later `configure()` calls for fields it
|
|
3746
3878
|
* didn't override.
|
|
3747
3879
|
*
|
|
3880
|
+
* The object form also accepts `darkHue` / `darkSaturation` — a separate
|
|
3881
|
+
* seed pair for the dark and dark-high-contrast schemes. Both default to
|
|
3882
|
+
* their light counterparts, and an explicit `darkSaturation` opts the theme
|
|
3883
|
+
* out of the global `darkDesaturation` reduction.
|
|
3884
|
+
*
|
|
3748
3885
|
* @example
|
|
3749
3886
|
* ```ts
|
|
3750
3887
|
* const primary = glaze(280, 80);
|
|
3751
3888
|
* // or shorthand:
|
|
3752
3889
|
* const primary = glaze({ hue: 280, saturation: 80 });
|
|
3890
|
+
* // with a retuned dark seed:
|
|
3891
|
+
* const primary = glaze({ hue: 280, saturation: 80, darkHue: 268, darkSaturation: 65 });
|
|
3753
3892
|
* // with config override:
|
|
3754
3893
|
* const raw = glaze(280, 80, { lightTone: false });
|
|
3755
3894
|
* ```
|
|
3756
3895
|
*/
|
|
3757
3896
|
function glaze(hueOrOptions, saturation, config) {
|
|
3758
|
-
if (typeof hueOrOptions === "number") return createTheme(
|
|
3759
|
-
|
|
3897
|
+
if (typeof hueOrOptions === "number") return createTheme({
|
|
3898
|
+
hue: hueOrOptions,
|
|
3899
|
+
saturation: saturation ?? 100
|
|
3900
|
+
}, void 0, config);
|
|
3901
|
+
return createTheme(hueOrOptions, void 0, config);
|
|
3760
3902
|
}
|
|
3761
3903
|
/** Configure global glaze settings. */
|
|
3762
3904
|
glaze.configure = function configure$1(config) {
|
|
@@ -3870,7 +4012,10 @@ glaze.fromHex = function fromHex(hex) {
|
|
|
3870
4012
|
const rgb = parseHex(hex);
|
|
3871
4013
|
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
3872
4014
|
const [h, s] = srgbToOkhsl(rgb);
|
|
3873
|
-
return createTheme(
|
|
4015
|
+
return createTheme({
|
|
4016
|
+
hue: h,
|
|
4017
|
+
saturation: s * 100
|
|
4018
|
+
});
|
|
3874
4019
|
};
|
|
3875
4020
|
/**
|
|
3876
4021
|
* Create a theme from RGB values (0–255).
|
|
@@ -3882,7 +4027,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
3882
4027
|
g / 255,
|
|
3883
4028
|
b / 255
|
|
3884
4029
|
]);
|
|
3885
|
-
return createTheme(
|
|
4030
|
+
return createTheme({
|
|
4031
|
+
hue: h,
|
|
4032
|
+
saturation: s * 100
|
|
4033
|
+
});
|
|
3886
4034
|
};
|
|
3887
4035
|
/**
|
|
3888
4036
|
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|