@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.cjs
CHANGED
|
@@ -1979,34 +1979,77 @@ function resolveContrastSpec(spec, isHighContrast, polarity) {
|
|
|
1979
1979
|
/**
|
|
1980
1980
|
* Apply the relative-tone delta against a base, honoring `flip`.
|
|
1981
1981
|
*
|
|
1982
|
-
* When `flip` is on and `base + delta` falls outside `[0, 100]`,
|
|
1983
|
-
* delta to the other side of the base
|
|
1984
|
-
*
|
|
1982
|
+
* When `flip` is on and `base + delta` falls outside `[0, 100]`, try mirroring
|
|
1983
|
+
* the delta to the other side of the base. If the mirrored target is also out
|
|
1984
|
+
* of range, keep the original delta so the caller clamps on the authored side.
|
|
1985
|
+
* When off, the caller clamps as usual.
|
|
1985
1986
|
*/
|
|
1986
1987
|
function applyToneFlip(delta, baseTone, flip) {
|
|
1987
1988
|
if (!flip) return delta;
|
|
1988
1989
|
const target = baseTone + delta;
|
|
1989
1990
|
if (target >= 0 && target <= 100) return delta;
|
|
1990
|
-
|
|
1991
|
+
const mirrored = baseTone - delta;
|
|
1992
|
+
if (mirrored >= 0 && mirrored <= 100) return -delta;
|
|
1993
|
+
return delta;
|
|
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);
|
|
1991
2008
|
}
|
|
1992
2009
|
function resolveRootColor(def, isHighContrast) {
|
|
1993
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;
|
|
1994
2037
|
return {
|
|
1995
|
-
|
|
1996
|
-
|
|
2038
|
+
hue: resolveEffectiveHue(ctx.darkHue ?? ctx.hue, def.darkHue ?? def.hue),
|
|
2039
|
+
saturation: clamp(explicitDark ? raw : mapSaturationDark(raw, mode, ctx.config), 0, 1)
|
|
1997
2040
|
};
|
|
1998
2041
|
}
|
|
1999
|
-
function resolveDependentColor(name, def, ctx, isHighContrast, isDark,
|
|
2042
|
+
function resolveDependentColor(name, def, ctx, isHighContrast, isDark, channels, polarity, effectivePastel) {
|
|
2000
2043
|
const baseName = def.base;
|
|
2001
2044
|
const baseResolved = ctx.resolved.get(baseName);
|
|
2002
2045
|
if (!baseResolved) throw new Error(`glaze: base "${baseName}" not yet resolved for "${name}".`);
|
|
2003
2046
|
const mode = def.mode ?? "auto";
|
|
2004
|
-
const satFactor = clamp(def.saturation ?? 1, 0, 1);
|
|
2005
2047
|
const flip = def.autoFlip ?? ctx.config.autoFlip;
|
|
2006
2048
|
const pastel = effectivePastel;
|
|
2007
2049
|
const baseVariant = getSchemeVariant(baseResolved, isDark, isHighContrast);
|
|
2008
2050
|
const baseTone = baseVariant.t * 100;
|
|
2009
2051
|
let preferredTone;
|
|
2052
|
+
let isExtreme = false;
|
|
2010
2053
|
const rawTone = def.tone;
|
|
2011
2054
|
if (rawTone === void 0) preferredTone = baseTone;
|
|
2012
2055
|
else {
|
|
@@ -2015,22 +2058,28 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2015
2058
|
const baseLightTone = getSchemeVariant(baseResolved, false, isHighContrast).t * 100;
|
|
2016
2059
|
preferredTone = mapToneForScheme(clamp(baseLightTone + applyToneFlip(parsed.value, baseLightTone, flip), 0, 100), "auto", true, isHighContrast, ctx.config);
|
|
2017
2060
|
} else preferredTone = clamp(baseTone + applyToneFlip(parsed.value, baseTone, flip), 0, 100);
|
|
2018
|
-
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
|
+
}
|
|
2019
2068
|
}
|
|
2020
2069
|
const rawContrast = def.contrast;
|
|
2021
2070
|
if (rawContrast !== void 0) {
|
|
2022
2071
|
const resolvedContrast = resolveContrastSpec(rawContrast, isHighContrast, polarity);
|
|
2023
|
-
const effectiveSat = isDark ? mapSaturationDark(satFactor * ctx.saturation / 100, mode, ctx.config) : satFactor * ctx.saturation / 100;
|
|
2024
2072
|
const baseOkhsl = toOkhslVariant(baseVariant);
|
|
2025
2073
|
const baseLinearRgb = okhslToLinearSrgb(baseOkhsl.h, baseOkhsl.s, baseOkhsl.l, baseVariant.pastel ?? ctx.config.pastel);
|
|
2026
2074
|
const toneRange = schemeToneRange(isDark, mode, isHighContrast, ctx.config);
|
|
2027
2075
|
let initialDirection;
|
|
2028
2076
|
if (preferredTone < baseTone) initialDirection = "darker";
|
|
2029
2077
|
else if (preferredTone > baseTone) initialDirection = "lighter";
|
|
2078
|
+
const seedTone = isExtreme ? clamp(preferredTone / 100, 0, 1) : clamp(preferredTone / 100, toneRange[0], toneRange[1]);
|
|
2030
2079
|
const result = findToneForContrast({
|
|
2031
|
-
hue:
|
|
2032
|
-
saturation:
|
|
2033
|
-
preferredTone:
|
|
2080
|
+
hue: channels.hue,
|
|
2081
|
+
saturation: channels.saturation,
|
|
2082
|
+
preferredTone: seedTone,
|
|
2034
2083
|
baseLinearRgb,
|
|
2035
2084
|
contrast: resolvedContrast,
|
|
2036
2085
|
toneRange: [0, 1],
|
|
@@ -2039,15 +2088,9 @@ function resolveDependentColor(name, def, ctx, isHighContrast, isDark, effective
|
|
|
2039
2088
|
pastel
|
|
2040
2089
|
});
|
|
2041
2090
|
if (!result.met) warnContrastUnmet(name, isDark, isHighContrast, resolvedContrast, result.contrast);
|
|
2042
|
-
return
|
|
2043
|
-
tone: result.tone * 100,
|
|
2044
|
-
satFactor
|
|
2045
|
-
};
|
|
2091
|
+
return result.tone * 100;
|
|
2046
2092
|
}
|
|
2047
|
-
return
|
|
2048
|
-
tone: clamp(preferredTone, 0, 100),
|
|
2049
|
-
satFactor
|
|
2050
|
-
};
|
|
2093
|
+
return clamp(preferredTone, 0, 100);
|
|
2051
2094
|
}
|
|
2052
2095
|
function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
2053
2096
|
if (isShadowDef(def)) return resolveShadowForScheme(def, ctx, isDark, isHighContrast);
|
|
@@ -2055,27 +2098,14 @@ function resolveColorForScheme(name, def, ctx, isDark, isHighContrast) {
|
|
|
2055
2098
|
const regDef = def;
|
|
2056
2099
|
const mode = regDef.mode ?? "auto";
|
|
2057
2100
|
const isRoot = isAbsoluteTone(regDef.tone) && !regDef.base;
|
|
2058
|
-
const
|
|
2101
|
+
const channels = resolveChannels(regDef, ctx, isDark);
|
|
2059
2102
|
const polarity = roleToPolarity(resolveRole(name, def, ctx));
|
|
2060
2103
|
const pastel = regDef.pastel ?? ctx.config.pastel;
|
|
2061
|
-
|
|
2062
|
-
let satFactor;
|
|
2063
|
-
if (isRoot) {
|
|
2064
|
-
const root = resolveRootColor(regDef, isHighContrast);
|
|
2065
|
-
finalTone = mapToneForScheme(root.authorTone, mode, isDark, isHighContrast, ctx.config);
|
|
2066
|
-
satFactor = root.satFactor;
|
|
2067
|
-
} else {
|
|
2068
|
-
const dep = resolveDependentColor(name, regDef, ctx, isHighContrast, isDark, effectiveHue, polarity, pastel);
|
|
2069
|
-
finalTone = dep.tone;
|
|
2070
|
-
satFactor = dep.satFactor;
|
|
2071
|
-
}
|
|
2072
|
-
const baseSat = satFactor * ctx.saturation / 100;
|
|
2073
|
-
const finalSat = isDark ? mapSaturationDark(baseSat, mode, ctx.config) : baseSat;
|
|
2074
|
-
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);
|
|
2075
2105
|
return {
|
|
2076
|
-
h:
|
|
2077
|
-
s:
|
|
2078
|
-
t:
|
|
2106
|
+
h: channels.hue,
|
|
2107
|
+
s: channels.saturation,
|
|
2108
|
+
t: clamp(finalTone / 100, 0, 1),
|
|
2079
2109
|
alpha: regDef.opacity ?? 1,
|
|
2080
2110
|
pastel
|
|
2081
2111
|
};
|
|
@@ -2275,12 +2305,11 @@ function verifyContrastDrift(order, defs, result, config) {
|
|
|
2275
2305
|
}
|
|
2276
2306
|
}
|
|
2277
2307
|
}
|
|
2278
|
-
function resolveAllColors(
|
|
2308
|
+
function resolveAllColors(seed, defs, config, externalBases) {
|
|
2279
2309
|
validateColorDefs(defs, externalBases);
|
|
2280
2310
|
const order = topoSort(defs);
|
|
2281
2311
|
const ctx = {
|
|
2282
|
-
|
|
2283
|
-
saturation,
|
|
2312
|
+
...seed,
|
|
2284
2313
|
defs,
|
|
2285
2314
|
resolved: /* @__PURE__ */ new Map(),
|
|
2286
2315
|
config,
|
|
@@ -2313,8 +2342,12 @@ function resolveAllColors(hue, saturation, defs, config, externalBases) {
|
|
|
2313
2342
|
/**
|
|
2314
2343
|
* Hue channel planning for `splitHue` exports.
|
|
2315
2344
|
*
|
|
2316
|
-
* Builds per-color hue var references and
|
|
2317
|
-
*
|
|
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`.
|
|
2318
2351
|
*/
|
|
2319
2352
|
const ACHROMATIC_EPSILON = 1e-6;
|
|
2320
2353
|
function cssProp(prefix, name, suffix) {
|
|
@@ -2323,7 +2356,11 @@ function cssProp(prefix, name, suffix) {
|
|
|
2323
2356
|
function isAchromatic(v) {
|
|
2324
2357
|
return v.s <= ACHROMATIC_EPSILON;
|
|
2325
2358
|
}
|
|
2326
|
-
|
|
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) {
|
|
2327
2364
|
if (def === void 0 || isShadowDef(def) || isMixDef(def) || isAchromatic(variant)) return {
|
|
2328
2365
|
hueVar: "",
|
|
2329
2366
|
inline: true,
|
|
@@ -2331,13 +2368,25 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2331
2368
|
};
|
|
2332
2369
|
const regDef = def;
|
|
2333
2370
|
const baseHueVar = `var(--${ctx.baseName}-hue)`;
|
|
2334
|
-
|
|
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 {
|
|
2335
2385
|
hueVar: baseHueVar,
|
|
2336
2386
|
inline: false,
|
|
2337
2387
|
declarations: []
|
|
2338
2388
|
};
|
|
2339
|
-
const parsed = parseRelativeOrAbsolute(
|
|
2340
|
-
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2389
|
+
const parsed = parseRelativeOrAbsolute(authored);
|
|
2341
2390
|
if (parsed.relative) {
|
|
2342
2391
|
const sign = parsed.value >= 0 ? "+" : "-";
|
|
2343
2392
|
const magnitude = Math.abs(parsed.value);
|
|
@@ -2361,13 +2410,13 @@ function themeHuePlan(name, def, variant, ctx) {
|
|
|
2361
2410
|
}]
|
|
2362
2411
|
};
|
|
2363
2412
|
}
|
|
2364
|
-
function standaloneHuePlan(name, variant, ctx) {
|
|
2413
|
+
function standaloneHuePlan(name, variant, ctx, scheme) {
|
|
2365
2414
|
if (isAchromatic(variant)) return {
|
|
2366
2415
|
hueVar: "",
|
|
2367
2416
|
inline: true,
|
|
2368
2417
|
declarations: []
|
|
2369
2418
|
};
|
|
2370
|
-
const hue = ctx.resolvedHue ?? variant.h;
|
|
2419
|
+
const hue = scheme === "dark" ? variant.h : ctx.resolvedHue ?? variant.h;
|
|
2371
2420
|
const prop = cssProp(ctx.prefix, name, "-hue");
|
|
2372
2421
|
return {
|
|
2373
2422
|
hueVar: `var(${prop})`,
|
|
@@ -2378,12 +2427,12 @@ function standaloneHuePlan(name, variant, ctx) {
|
|
|
2378
2427
|
}]
|
|
2379
2428
|
};
|
|
2380
2429
|
}
|
|
2381
|
-
function buildHuePlan(name, def, variant, ctx) {
|
|
2382
|
-
if (ctx.mode === "standalone") return standaloneHuePlan(name, variant, ctx);
|
|
2383
|
-
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);
|
|
2384
2433
|
}
|
|
2385
2434
|
/** Collect unique hue declarations across all colors (theme + per-color). */
|
|
2386
|
-
function collectHueDeclarations(resolved, ctx) {
|
|
2435
|
+
function collectHueDeclarations(resolved, ctx, scheme = "light") {
|
|
2387
2436
|
if (ctx.emitDeclarations === false) return [];
|
|
2388
2437
|
const seen = /* @__PURE__ */ new Set();
|
|
2389
2438
|
const out = [];
|
|
@@ -2394,15 +2443,32 @@ function collectHueDeclarations(resolved, ctx) {
|
|
|
2394
2443
|
};
|
|
2395
2444
|
if (ctx.mode === "theme") push({
|
|
2396
2445
|
prop: `--${ctx.baseName}-hue`,
|
|
2397
|
-
value: String(ctx
|
|
2446
|
+
value: String(schemeSeedHue(ctx, scheme))
|
|
2398
2447
|
});
|
|
2399
2448
|
for (const [name, color] of resolved) {
|
|
2400
2449
|
const def = ctx.defs[name];
|
|
2401
|
-
const plan = buildHuePlan(name, def, color.light, ctx);
|
|
2450
|
+
const plan = buildHuePlan(name, def, scheme === "dark" ? color.dark : color.light, ctx, scheme);
|
|
2402
2451
|
for (const decl of plan.declarations) push(decl);
|
|
2403
2452
|
}
|
|
2404
2453
|
return out;
|
|
2405
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
|
+
}
|
|
2406
2472
|
function buildHuePlans(resolved, ctx) {
|
|
2407
2473
|
const plans = /* @__PURE__ */ new Map();
|
|
2408
2474
|
for (const [name, color] of resolved) plans.set(name, buildHuePlan(name, ctx.defs[name], color.light, ctx));
|
|
@@ -2476,17 +2542,15 @@ function buildTokenMap(resolved, prefix, states, modes, format = "oklch", pastel
|
|
|
2476
2542
|
const tokens = {};
|
|
2477
2543
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2478
2544
|
if (huePlans !== void 0 && channelCtx !== void 0) {
|
|
2479
|
-
const
|
|
2480
|
-
if (
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
const colorKey = `#${prefix}${name}`;
|
|
2488
|
-
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
|
+
};
|
|
2489
2552
|
}
|
|
2553
|
+
for (const [name, color] of resolved) tokens[`#${prefix}${name}`] = buildTokenEntry(color, states, modes, format, pastel, huePlans.get(name));
|
|
2490
2554
|
return tokens;
|
|
2491
2555
|
}
|
|
2492
2556
|
for (const [name, color] of resolved) {
|
|
@@ -2535,7 +2599,13 @@ function buildCssMap(resolved, prefix, suffix, format, pastel = false, channelCt
|
|
|
2535
2599
|
darkContrast: []
|
|
2536
2600
|
};
|
|
2537
2601
|
const huePlans = channelCtx !== void 0 && format === "oklch" ? buildHuePlans(resolved, channelCtx) : void 0;
|
|
2538
|
-
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
|
+
}
|
|
2539
2609
|
for (const [name, color] of resolved) {
|
|
2540
2610
|
const prop = `--${prefix}${name}${suffix}`;
|
|
2541
2611
|
const plan = huePlans?.get(name);
|
|
@@ -2972,6 +3042,13 @@ function validateStructuredInput(input) {
|
|
|
2972
3042
|
if (input.saturationFactor !== void 0) {
|
|
2973
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}).`);
|
|
2974
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
|
+
}
|
|
2975
3052
|
if (input.opacity !== void 0) validateStandaloneOpacity(input.opacity);
|
|
2976
3053
|
}
|
|
2977
3054
|
/**
|
|
@@ -3018,11 +3095,27 @@ function extractOkhslFromValue(value) {
|
|
|
3018
3095
|
return value;
|
|
3019
3096
|
}
|
|
3020
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
|
+
/**
|
|
3021
3110
|
* Build the `ColorMap` for a value-shorthand `glaze.color()` call.
|
|
3022
3111
|
*
|
|
3023
3112
|
* The user-facing color (`STANDALONE_VALUE`) defaults to `mode: 'auto'`
|
|
3024
3113
|
* across every value-shorthand form.
|
|
3025
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
|
+
*
|
|
3026
3119
|
* When the user requests `contrast` or relative `tone`, a hidden
|
|
3027
3120
|
* `STANDALONE_SEED` def is synthesized at `mode: 'static'`. That keeps
|
|
3028
3121
|
* the seed pinned to the literal user-provided color across all four
|
|
@@ -3040,10 +3133,13 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3040
3133
|
if (userName !== void 0) validateStandaloneName(userName);
|
|
3041
3134
|
const primary = userName ?? STANDALONE_VALUE;
|
|
3042
3135
|
const seedTone = toTone(main.l);
|
|
3136
|
+
const primaryTone = toneOption ?? seedTone;
|
|
3043
3137
|
const valueDef = {
|
|
3044
3138
|
hue: relativeHue,
|
|
3045
3139
|
saturation: options?.saturationFactor,
|
|
3046
|
-
|
|
3140
|
+
darkHue: options?.darkHue,
|
|
3141
|
+
darkSaturation: options?.darkSaturationFactor,
|
|
3142
|
+
tone: needsSeedAnchor ? pinExtremeTone(primaryTone) : primaryTone,
|
|
3047
3143
|
contrast: options?.contrast,
|
|
3048
3144
|
mode: options?.mode ?? "auto",
|
|
3049
3145
|
autoFlip: options?.autoFlip,
|
|
@@ -3060,13 +3156,16 @@ function buildStandaloneValueDefs(main, options) {
|
|
|
3060
3156
|
mode: "static"
|
|
3061
3157
|
};
|
|
3062
3158
|
return {
|
|
3063
|
-
|
|
3064
|
-
|
|
3159
|
+
seed: {
|
|
3160
|
+
hue: seedHue,
|
|
3161
|
+
saturation: seedSaturation,
|
|
3162
|
+
darkSaturation: options?.darkSaturation
|
|
3163
|
+
},
|
|
3065
3164
|
defs,
|
|
3066
3165
|
primary
|
|
3067
3166
|
};
|
|
3068
3167
|
}
|
|
3069
|
-
function createColorTokenFromDefs(
|
|
3168
|
+
function createColorTokenFromDefs(seed, defs, primary, configOverride, baseToken, buildExport) {
|
|
3070
3169
|
let cache = null;
|
|
3071
3170
|
function getEffectiveConfig() {
|
|
3072
3171
|
const version = getConfigVersion();
|
|
@@ -3083,7 +3182,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3083
3182
|
const version = getConfigVersion();
|
|
3084
3183
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3085
3184
|
const effectiveConfig = getEffectiveConfig();
|
|
3086
|
-
const map = resolveAllColors(
|
|
3185
|
+
const map = resolveAllColors(seed, defs, effectiveConfig, baseToken ? new Map([[STANDALONE_BASE, baseToken.resolve()]]) : void 0);
|
|
3087
3186
|
cache = {
|
|
3088
3187
|
map,
|
|
3089
3188
|
version,
|
|
@@ -3121,7 +3220,7 @@ function createColorTokenFromDefs(seedHue, seedSaturation, defs, primary, config
|
|
|
3121
3220
|
if (options.splitHue && format === "oklch") {
|
|
3122
3221
|
assertAllPastel(renamed, resolveModes());
|
|
3123
3222
|
channelCtx = {
|
|
3124
|
-
seedHue,
|
|
3223
|
+
seedHue: seed.hue,
|
|
3125
3224
|
baseName: options.name,
|
|
3126
3225
|
prefix: "",
|
|
3127
3226
|
defs: { [options.name]: defs[primary] },
|
|
@@ -3205,8 +3304,10 @@ function createColorToken(input, configOverride) {
|
|
|
3205
3304
|
const hasExternalBase = baseToken !== void 0;
|
|
3206
3305
|
const needsSeedAnchor = !hasExternalBase && input.contrast !== void 0;
|
|
3207
3306
|
const defs = { [primary]: {
|
|
3208
|
-
tone: input.tone,
|
|
3307
|
+
tone: needsSeedAnchor ? pinExtremeTone(input.tone) : input.tone,
|
|
3209
3308
|
saturation: input.saturationFactor,
|
|
3309
|
+
darkHue: input.darkHue,
|
|
3310
|
+
darkSaturation: input.darkSaturationFactor,
|
|
3210
3311
|
mode: input.mode ?? "auto",
|
|
3211
3312
|
autoFlip: input.autoFlip,
|
|
3212
3313
|
contrast: input.contrast,
|
|
@@ -3215,16 +3316,17 @@ function createColorToken(input, configOverride) {
|
|
|
3215
3316
|
role: input.role,
|
|
3216
3317
|
base: hasExternalBase ? STANDALONE_BASE : needsSeedAnchor ? STANDALONE_SEED : void 0
|
|
3217
3318
|
} };
|
|
3218
|
-
if (needsSeedAnchor) {
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
mode: "static"
|
|
3224
|
-
};
|
|
3225
|
-
}
|
|
3319
|
+
if (needsSeedAnchor) defs[STANDALONE_SEED] = {
|
|
3320
|
+
tone: pinExtremeTone(pairNormal(input.tone)),
|
|
3321
|
+
saturation: 1,
|
|
3322
|
+
mode: "static"
|
|
3323
|
+
};
|
|
3226
3324
|
const localOverride = configOverride;
|
|
3227
|
-
return createColorTokenFromDefs(
|
|
3325
|
+
return createColorTokenFromDefs({
|
|
3326
|
+
hue: input.hue,
|
|
3327
|
+
saturation: input.saturation,
|
|
3328
|
+
darkSaturation: input.darkSaturation
|
|
3329
|
+
}, defs, primary, localOverride, baseToken, (exportArg) => ({
|
|
3228
3330
|
kind: "color",
|
|
3229
3331
|
version: GLAZE_EXPORT_VERSION,
|
|
3230
3332
|
form: "structured",
|
|
@@ -3235,9 +3337,9 @@ function createColorToken(input, configOverride) {
|
|
|
3235
3337
|
function createColorTokenFromValue(value, options, configOverride) {
|
|
3236
3338
|
const main = extractOkhslFromValue(value);
|
|
3237
3339
|
const linkingBase = toLinkingBase(resolveBaseToken(options?.base));
|
|
3238
|
-
const {
|
|
3340
|
+
const { seed, defs, primary } = buildStandaloneValueDefs(main, options);
|
|
3239
3341
|
const localOverride = sparseValueFormLocal(configOverride);
|
|
3240
|
-
return createColorTokenFromDefs(
|
|
3342
|
+
return createColorTokenFromDefs(seed, defs, primary, localOverride, linkingBase, (exportArg) => ({
|
|
3241
3343
|
kind: "color",
|
|
3242
3344
|
version: GLAZE_EXPORT_VERSION,
|
|
3243
3345
|
form: "value",
|
|
@@ -3257,6 +3359,9 @@ function buildOverridesExport(options, exportArg) {
|
|
|
3257
3359
|
if (options.saturation !== void 0) out.saturation = options.saturation;
|
|
3258
3360
|
if (options.tone !== void 0) out.tone = options.tone;
|
|
3259
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;
|
|
3260
3365
|
if (options.mode !== void 0) out.mode = options.mode;
|
|
3261
3366
|
if (options.autoFlip !== void 0) out.autoFlip = options.autoFlip;
|
|
3262
3367
|
if (options.contrast !== void 0) out.contrast = options.contrast;
|
|
@@ -3274,6 +3379,9 @@ function buildStructuredInputExport(input, exportArg) {
|
|
|
3274
3379
|
tone: input.tone
|
|
3275
3380
|
};
|
|
3276
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;
|
|
3277
3385
|
if (input.mode !== void 0) out.mode = input.mode;
|
|
3278
3386
|
if (input.autoFlip !== void 0) out.autoFlip = input.autoFlip;
|
|
3279
3387
|
if (input.opacity !== void 0) out.opacity = input.opacity;
|
|
@@ -3296,6 +3404,9 @@ function rehydrateOverrides(data) {
|
|
|
3296
3404
|
if (data.saturation !== void 0) out.saturation = data.saturation;
|
|
3297
3405
|
if (data.tone !== void 0) out.tone = data.tone;
|
|
3298
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;
|
|
3299
3410
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3300
3411
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3301
3412
|
if (data.contrast !== void 0) out.contrast = data.contrast;
|
|
@@ -3313,6 +3424,9 @@ function rehydrateStructuredInput(data) {
|
|
|
3313
3424
|
tone: data.tone
|
|
3314
3425
|
};
|
|
3315
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;
|
|
3316
3430
|
if (data.mode !== void 0) out.mode = data.mode;
|
|
3317
3431
|
if (data.autoFlip !== void 0) out.autoFlip = data.autoFlip;
|
|
3318
3432
|
if (data.opacity !== void 0) out.opacity = data.opacity;
|
|
@@ -3346,7 +3460,8 @@ function colorFromExport(data) {
|
|
|
3346
3460
|
|
|
3347
3461
|
//#endregion
|
|
3348
3462
|
//#region src/theme.ts
|
|
3349
|
-
function createTheme(
|
|
3463
|
+
function createTheme(seed, initialColors, configOverride) {
|
|
3464
|
+
const { hue, saturation, darkHue, darkSaturation } = seed;
|
|
3350
3465
|
let colorDefs = initialColors ? { ...initialColors } : {};
|
|
3351
3466
|
let cache = null;
|
|
3352
3467
|
function getEffectiveConfig() {
|
|
@@ -3364,7 +3479,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3364
3479
|
const version = getConfigVersion();
|
|
3365
3480
|
if (cache && cache.version === version && cache.map) return cache.map;
|
|
3366
3481
|
const effectiveConfig = getEffectiveConfig();
|
|
3367
|
-
const map = resolveAllColors(
|
|
3482
|
+
const map = resolveAllColors(seed, colorDefs, effectiveConfig);
|
|
3368
3483
|
cache = {
|
|
3369
3484
|
map,
|
|
3370
3485
|
version,
|
|
@@ -3381,6 +3496,7 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3381
3496
|
assertAllPastel(resolveCached(), resolveModes(options?.modes));
|
|
3382
3497
|
return {
|
|
3383
3498
|
seedHue: hue,
|
|
3499
|
+
darkSeedHue: darkHue,
|
|
3384
3500
|
baseName: options.name ?? "theme",
|
|
3385
3501
|
prefix,
|
|
3386
3502
|
defs: colorDefs,
|
|
@@ -3394,6 +3510,12 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3394
3510
|
get saturation() {
|
|
3395
3511
|
return saturation;
|
|
3396
3512
|
},
|
|
3513
|
+
get darkHue() {
|
|
3514
|
+
return darkHue;
|
|
3515
|
+
},
|
|
3516
|
+
get darkSaturation() {
|
|
3517
|
+
return darkSaturation;
|
|
3518
|
+
},
|
|
3397
3519
|
getConfig() {
|
|
3398
3520
|
return getEffectiveConfig();
|
|
3399
3521
|
},
|
|
@@ -3430,16 +3552,22 @@ function createTheme(hue, saturation, initialColors, configOverride) {
|
|
|
3430
3552
|
version: GLAZE_EXPORT_VERSION,
|
|
3431
3553
|
hue,
|
|
3432
3554
|
saturation,
|
|
3555
|
+
...darkHue !== void 0 ? { darkHue } : {},
|
|
3556
|
+
...darkSaturation !== void 0 ? { darkSaturation } : {},
|
|
3433
3557
|
colors: structuredClone(colorDefs),
|
|
3434
3558
|
config: freezeConfigForExport(configOverride, override)
|
|
3435
3559
|
};
|
|
3436
3560
|
},
|
|
3437
3561
|
extend(options) {
|
|
3438
|
-
const
|
|
3439
|
-
|
|
3562
|
+
const childSeed = {
|
|
3563
|
+
hue: options.hue ?? hue,
|
|
3564
|
+
saturation: options.saturation ?? saturation,
|
|
3565
|
+
darkHue: options.darkHue ?? darkHue,
|
|
3566
|
+
darkSaturation: options.darkSaturation ?? darkSaturation
|
|
3567
|
+
};
|
|
3440
3568
|
const inheritedColors = {};
|
|
3441
3569
|
for (const [name, def] of Object.entries(colorDefs)) if (def.inherit !== false) inheritedColors[name] = def;
|
|
3442
|
-
return createTheme(
|
|
3570
|
+
return createTheme(childSeed, options.colors ? {
|
|
3443
3571
|
...inheritedColors,
|
|
3444
3572
|
...options.colors
|
|
3445
3573
|
} : { ...inheritedColors }, configOverride || options.config ? {
|
|
@@ -3549,6 +3677,7 @@ function channelCtxForTheme(theme, themeName, passPrefix, themedPrefix, splitHue
|
|
|
3549
3677
|
assertAllPastel(filtered, modes);
|
|
3550
3678
|
return {
|
|
3551
3679
|
seedHue: theme.hue,
|
|
3680
|
+
darkSeedHue: theme.darkHue,
|
|
3552
3681
|
baseName: themeName,
|
|
3553
3682
|
prefix: themedPrefix,
|
|
3554
3683
|
defs: colorMapFromTheme(theme),
|
|
@@ -3580,7 +3709,13 @@ function createThemeFromExport(data, factory = "glaze.themeFrom") {
|
|
|
3580
3709
|
assertExportKind(data, "theme", factory);
|
|
3581
3710
|
assertExportVersion(data, factory);
|
|
3582
3711
|
if (typeof data.hue !== "number" || typeof data.saturation !== "number") throw new Error(`${factory}: expected numeric "hue" and "saturation" fields.`);
|
|
3583
|
-
|
|
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);
|
|
3584
3719
|
}
|
|
3585
3720
|
/**
|
|
3586
3721
|
* Rebuild a palette from a `palette.export()` snapshot.
|
|
@@ -3742,18 +3877,28 @@ function createPalette(themes, paletteOptions) {
|
|
|
3742
3877
|
* the theme still reacts to later `configure()` calls for fields it
|
|
3743
3878
|
* didn't override.
|
|
3744
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
|
+
*
|
|
3745
3885
|
* @example
|
|
3746
3886
|
* ```ts
|
|
3747
3887
|
* const primary = glaze(280, 80);
|
|
3748
3888
|
* // or shorthand:
|
|
3749
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 });
|
|
3750
3892
|
* // with config override:
|
|
3751
3893
|
* const raw = glaze(280, 80, { lightTone: false });
|
|
3752
3894
|
* ```
|
|
3753
3895
|
*/
|
|
3754
3896
|
function glaze(hueOrOptions, saturation, config) {
|
|
3755
|
-
if (typeof hueOrOptions === "number") return createTheme(
|
|
3756
|
-
|
|
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);
|
|
3757
3902
|
}
|
|
3758
3903
|
/** Configure global glaze settings. */
|
|
3759
3904
|
glaze.configure = function configure$1(config) {
|
|
@@ -3867,7 +4012,10 @@ glaze.fromHex = function fromHex(hex) {
|
|
|
3867
4012
|
const rgb = parseHex(hex);
|
|
3868
4013
|
if (!rgb) throw new Error(`glaze: invalid hex color "${hex}".`);
|
|
3869
4014
|
const [h, s] = srgbToOkhsl(rgb);
|
|
3870
|
-
return createTheme(
|
|
4015
|
+
return createTheme({
|
|
4016
|
+
hue: h,
|
|
4017
|
+
saturation: s * 100
|
|
4018
|
+
});
|
|
3871
4019
|
};
|
|
3872
4020
|
/**
|
|
3873
4021
|
* Create a theme from RGB values (0–255).
|
|
@@ -3879,7 +4027,10 @@ glaze.fromRgb = function fromRgb(r, g, b) {
|
|
|
3879
4027
|
g / 255,
|
|
3880
4028
|
b / 255
|
|
3881
4029
|
]);
|
|
3882
|
-
return createTheme(
|
|
4030
|
+
return createTheme({
|
|
4031
|
+
hue: h,
|
|
4032
|
+
saturation: s * 100
|
|
4033
|
+
});
|
|
3883
4034
|
};
|
|
3884
4035
|
/**
|
|
3885
4036
|
* Rehydrate a `glaze.color()` token from a `.export()` snapshot.
|