Skip to content

Latest commit

 

History

History
821 lines (588 loc) · 58.6 KB

File metadata and controls

821 lines (588 loc) · 58.6 KB

@tenphi/glaze

2.0.1

Patch Changes

  • #97 3366b6b Thanks @tenphi! - Include AGENTS.md in the npm package so documentation links remain valid.

2.0.0

Major Changes

  • #94 a50183d Thanks @tenphi! - Breaking: the format* writers now take s / l / t on the 0–1 scale the Glaze converters return.

    formatOkhsl, formatOkhst, formatRgb, formatHsl, and formatOklch took 0–100 percentages while resolve(), variantToOkhsl, srgbToOkhsl, oklabToOkhsl, and okhslToSrgb all return 0–1 — so composing a producer with a writer was off by 100x and failed silently, since 0.7 is a legal percentage and the result was a valid CSS string naming a near-black color. The library now speaks one scale end to end. (toTone / fromTone still speak the 0–100 tone axis the authoring API takes — divide by 100 before handing a tone to formatOkhst.)

    Drop the * 100 at the call site (formatOkhst(v.h, v.s, v.t)); a leftover one now warns instead of shifting the color quietly. Every export method — css(), tokens(), tasty(), json(), dtcg(), tailwind(), glaze.format() — emits the same colors as before: they were compensating internally, and dropping the redundant ×100 ÷100 round-trip only moves float noise (visible nowhere except the meaningless hue term of a fully-desaturated hsl() string).

1.5.0

Minor Changes

  • #91 5bf6195 Thanks @tenphi! - A manual contrastLevel no longer suppresses high-contrast output.

    The level now does one thing: it positions the normal light / dark variants on the 0–100 slider. The high-contrast tier stays the true high-contrast resolution — bit-identical to what 'auto' resolves — at every level, and modes.highContrast alone decides whether it is emitted. The two compose: a slider raises the baseline while a prefers-contrast: more block still escalates on top of it.

    Most visible in css(), which has no modes option and always returns four strings: at a mid level its lightContrast / darkContrast blocks now carry genuinely escalated values where they previously repeated the normal declarations.

    Two consequences of the corrected model:

    • contrastLevel: 0 now reproduces 'auto' output exactly, high-contrast tier included. It no longer implies "no high-contrast tier" — that is modes.highContrast: false, still the default.
    • At a global contrastLevel: 100 the normal variants already are the high-contrast ones, so a separate tier would duplicate them: a single light/dark set is emitted, even against an explicit modes.highContrast: true.

    Also fixes the side-stability probe leaking into the high-contrast passes, which made a mid-level high-contrast variant diverge from its 'auto' counterpart.

1.4.0

Minor Changes

  • #89 19cdde6 Thanks @tenphi! - Add from on theme color definitions. A color can now be seeded from a literal value — the same forms glaze.color() accepts — instead of from the theme:

    theme.colors({
      surface: { tone: 100, saturation: 0.12 },
      brand: { from: '#2f5bff', base: 'surface', contrast: 3 },
    });

    Most of Glaze answers "design me a palette". This answers the other question, "honor this color" — white-label products, multi-tenant branding and imported design tokens all arrive with a value already chosen, and it is a contract rather than a starting point.

    from supplies hue, tone, and — uniquely among theme colors — an absolute saturation. That last part is what makes the feature worth having. Every other color's saturation is a 0–1 factor of the theme seed, so the seed is a ceiling: the only way to place a color more saturated than its theme was to re-seed the theme, which drags every sibling along. A palette whose accent seed is shared with its status themes could not honor one brand color without re-chromatizing danger, success and the rest as a side effect. A from color carries its own chroma and is unaffected by the seed.

    The light, normal-contrast variant reproduces the value exactly (a local lightTone: false, matching the value-shorthand form of glaze.color()). Dark and high contrast adapt as usual — those are the variants a reader reaches for when the normal one does not work for them, so readability outranks fidelity there, and a color pinned across all four would just be a worse mode: 'static'. A contrast floor still applies everywhere and is still a floor rather than a target: a value that already clears it is emitted untouched.

    Sibling fields override what the value supplied, so { from: '#2f5bff', hue: 300 } keeps the saturation and tone and rotates the hue. A from color needs neither base nor tone — it is placed absolutely, so it stands as a root on its own.

    An unparseable from is rejected by validateColorDefs with the color's name in the message, rather than surfacing the parser's own error from inside the resolver — the string alone does not tell you which of fifty tokens carries it.

    Two smaller consequences. Under splitHue, a from color now gets its own --{name}-hue custom property in both schemes rather than referencing the theme's: it authors a hue that is not the theme's, so tracking the theme var would re-skin it on the next re-seed — the same failure mode fixed for darkHue-only colors in 1.3.1. And the value parsing / validation for GlazeColorValue moved from color-token.ts to a new internal color-value.ts so the resolver can reach it without an import cycle; no public export changed.

1.3.1

Patch Changes

  • #87 b5eb888 Thanks @tenphi! - Fix splitHue exports dropping a per-color darkHue. A color that authored only darkHue (no light hue) referenced the theme's --{name}-hue var in both schemes, so the --{color}-hue declaration emitted in the dark block was never read and the dark hue was silently ignored. A color that authors a hue in either scheme now gets its own hue custom property in both, tracking the theme hue in the scheme it did not author, so the shared var() reference stays valid and runtime re-skinning still works. Affects both css({ splitHue: true }) and the Tasty token map.

    Also corrects the tone: 'max' / 'min' with a base documentation: the high-contrast variants are not exempt from the light-shift replay. They follow the same rule, and because their tone window is already the full range the replay reproduces the plain mapping unless the base itself sits asymmetrically across schemes — a mode: 'fixed' or contrast-solved base, for example. The behavior is unchanged; only the docs and changelog claimed otherwise.

1.3.0

Minor Changes

  • #85 ff20d49 Thanks @tenphi! - Add contrastLevel — a manual contrast level that turns the two-tier high-contrast model into a 0–100 slider. Set it globally via glaze.configure(), or per theme / per token / through extend():

    glaze.configure({ contrastLevel: 60 });
    const theme = glaze(280, 80, { contrastLevel: 60 });
    glaze.color('#26fcb2', { contrastLevel: 60 });

    Level 0 reproduces the normal light / dark output and level 100 the lightContrast / darkContrast output, bit for bit. Levels in between are resolved at that level, not interpolated after the fact: Glaze interpolates the three things that make high contrast differ — authored [normal, highContrast] pairs, the tone-window widening (light [10,100] → [0,100], dark [15,95] → [0,100]), and the AA → AAA / APCA +15 Lc escalation — and feeds those through the ordinary resolve. A contrast floor is therefore genuinely solved at every level (contrast: 'AA' at level 50 solves for 5.75), and the floor is met at every level.

    While a level is set there is no separate high-contrast tier: lightContrast / darkContrast mirror their normal counterparts, and a global level turns high-contrast output off outright so no exporter emits the tier. modes.highContrast goes inert rather than fighting it — it reads as "emit a separate high-contrast set when contrast is automatic" — so a build config that leaves highContrast: true set keeps working, silently, when a user switches their preference from auto to manual. css() keeps its four-string shape with the high-contrast strings repeating the normal declarations, so existing @media (prefers-contrast: more) wiring keeps working untouched. A level set on one theme of a palette leaves its siblings' high-contrast tier alone.

    A color never swaps sides of its base mid-slider. autoFlip's tie-break — when both directions meet the floor, take the one nearer the authored tone — depends on the target, so along a ramp it would let a color leap across its base. The side is now decided once from the nearer endpoint and preferred throughout that half of the ramp: a color whose two ends agree never changes side, and one whose ends genuinely disagree changes exactly once, at level 50. Flipping is only re-ordered, never disabled, so a side that cannot physically reach the target still falls back. This is exposed as a new preferInitial option on findToneForContrast.

    Un-interpolable tone pairs ([50, 'max'], [50, '+20'], ['max', 'min']) switch at level 50 rather than blending across kinds.

    Also exports resolveContrastForLevel(spec, level, polarity?).

    A contrast pair may no longer switch metric. [4.5, { apca: 75 }] now throws a validation error: a WCAG ratio and an APCA Lc are different scales, so no target exists between them and the two variants are incomparable even without a manual level. Pair values inside one metric instead — { wcag: [4.5, 7] } or { apca: [60, 90] }. Previously such a pair resolved silently.

    Nothing else changes by default: contrastLevel defaults to 'auto', which is today's behavior exactly, and no existing type, signature, or output shape moved. As a bonus, a manual resolve runs two passes instead of four.

1.2.0

Minor Changes

  • #82 7002c2d Thanks @tenphi! - Preserve contrast for tone: 'max' / 'min' on colors with a base. The extreme is no longer re-mapped through the dark tone window (which compressed the base-to-extreme span and lowered contrast in dark). Glaze now measures the tone shift the light scheme applied between the base and the extreme and replays it against the base's resolved dark tone — mirrored under mode: 'auto', same-signed under 'fixed'. The result is clamped to [0, 100] only, so it may cross the darkTone boundary, and pins at the extreme when the shift does not fit. Root extremes (no base), mode: 'static', high-contrast variants, and standalone glaze.color() tokens are unchanged.

  • #84 5809f18 Thanks @tenphi! - Add darkHue / darkSaturation so a palette can seed a different hue and saturation for the dark schemes instead of relying on the flat darkDesaturation haircut.

    Set them on the theme seed for a whole-palette shift, or on a single color to retune just that token:

    const theme = glaze({
      hue: 280,
      saturation: 80,
      darkHue: 268, // dark schemes seed from 268
      darkSaturation: 65, // and from 65 rather than 80
    });
    
    theme.colors({
      accent: { tone: 55, hue: '+20', darkHue: '+35' },
      warning: { tone: 60, saturation: 0.9, darkSaturation: 0.6 },
    });

    Both apply to the dark and darkContrast variants, and both fall back to their light counterparts, so existing themes resolve exactly as before. Seed values use the same units as hue / saturation (0–360 and 0–100); a color def's darkSaturation is a 0–1 factor like its saturation. Relative darkHue: '+N' anchors to the theme's dark seed hue. Authoring any dark saturation bypasses darkDesaturation rather than stacking with it, and mode: 'static' ignores both. Shadows and mixes need no new fields — they derive their channels from the colors they reference.

    glaze.color() gains the same controls as darkHue, darkSaturation (0–100 seed) and darkSaturationFactor (0–1), all round-tripping through export() / glaze.colorFrom().

    splitHue exports stay correct: when a dark hue is authored, the hue custom properties are re-declared in the dark block and under the Tasty dark state.

1.1.1

Patch Changes

  • #80 9d0487e Thanks @tenphi! - Fix relative tone with autoFlip: when mirroring an overshooting delta still leaves [0, 100], keep the original direction and clamp instead of pinning to the wrong extreme.

1.1.0

Minor Changes

  • #78 8690d87 Thanks @tenphi! - Add palette authoring round-trip (palette.export() / glaze.paletteFrom()), a restore triad with glaze.themeFrom (and glaze.from as alias), kind/version on all authoring exports, palette theme introspection (theme / themes / list / primary), and export type guards. Authoring .export(override?) freezes getConfig() ∪ instance local ∪ override at call time (nested color base exports receive the same override). Live themes and color tokens keep a sparse local override and track live configure() for omitted fields. Remove pastel from glaze.configure() / GlazeConfig — set it per-theme, per-token, or per-color instead.

1.0.0

Major Changes

  • 5b6e1e5 Thanks @tenphi! - First stable release (1.0.0). The public API is now semver-stable.

0.19.0

Minor Changes

  • #75 04e794f Thanks @tenphi! - Tasty exports now default to media-query state aliases (@media(prefers-color-scheme: dark) and @media(prefers-contrast: more)) instead of the custom @dark / @high-contrast aliases. Tokens now react to the OS preference out of the box without registering custom Tasty states. Override via glaze.configure({ states }) or per-export states to keep the old aliases.

0.18.0

Minor Changes

  • 8b6cd07 Thanks @tenphi! - High-contrast mode now auto-enhances a bare contrast target (no [normal, hc] pair at either the outer contrast or inner metric level). An explicit HC value via either pair always overrides.

    • APCA: a bare APCA scalar is boosted by the APCA-W3 "Enhanced Level" +15 Lc delta, clamped to 106 Lc.
    • WCAG: a bare WCAG preset is promoted to its spec-defined "Enhanced" successor (SC 1.4.3 → 1.4.6) — AA → AAA (4.5 → 7) and AA-large → AAA-large (3 → 4.5). AAA / AAA-large (top tier) and bare numeric targets are left unchanged.

    New exports: APCA_HC_ENHANCEMENT (15), APCA_MAX_LC (106); resolveContrastForMode gains an optional outerExplicitHC parameter.

0.17.0

Minor Changes

  • 10c0c0a Thanks @tenphi! - Rename the per-color flip configuration property to autoFlip to match the global configuration key and avoid confusion with dark-mode/scheme tone inversion.

0.16.2

Patch Changes

  • fa25eda Thanks @tenphi! - Default export format is now oklch for all exporters (tasty(), token(), css(), and internal formatter defaults). Use { format: 'okhsl' } or { format: 'rgb' } to opt into other formats.

0.16.1

Patch Changes

  • f7aaa85 Thanks @tenphi! - Fixes the tasty() output for splitHue feature.

0.16.0

Minor Changes

  • #69 86c3d27 Thanks @tenphi! - Add opt-in DTCG Resolver-Module export (dtcgResolver())

    • New export. theme.dtcgResolver(), palette.dtcgResolver(), and glaze.color().dtcgResolver() emit a single W3C DTCG Resolver-Module document describing every scheme variant in one file — sets (the light tokens as the default source) plus a single scheme modifier with a context per variant (light / dark / lightContrast / darkContrast) and a resolutionOrder. An alternative to dtcg()'s per-scheme files for resolver tools such as Dispersa.
    • Why one modifier. Glaze resolves darkContrast independently (it is not dark + lightContrast layered), so the four-context shape keeps every resolved value exact. Two independent modifiers would compose additively and produce wrong dark + high-contrast values.
    • Options. GlazeDtcgResolverOptions extends GlazeDtcgOptions (modes + colorSpace pass through) with setName (default 'base'), modifierName (default 'scheme'), contextNames (rename the four contexts), and version (default '2025.10'). Standalone glaze.color().dtcgResolver() requires name.
    • New public types: GlazeDtcgResolverDocument, GlazeDtcgResolverOptions, GlazeColorDtcgResolverOptions, DtcgTokenTree, DtcgResolverSet, DtcgResolverModifier, DtcgResolverRef.
  • #69 86c3d27 Thanks @tenphi! - Add DTCG and Tailwind CSS v4 token exports

    • dtcg() — W3C Design Tokens Format Module (2025.10) export. Available on themes, palettes, and standalone glaze.color() tokens. Returns one spec-conformant token document per scheme variant (light / dark / lightContrast / darkContrast``), each a { name: { $type: 'color', $value } }tree consumable by Figma, Tokens Studio, Style Dictionary v4+, Terrazzo, Penpot, and every DTCG-compatible tool. ThecolorSpaceoption selects the$valuerepresentation:'srgb'(default — gamma sRGBcomponentsin 0–1 plus ahexhint) or'oklch'(Glaze-native, wide-gamut[L, C, H], no hex). alpha` is emitted only when opacity is below 1. One document per scheme is the most tool-compatible convention (one file per Style Dictionary theme / Tokens Studio set / Figma variable mode).
    • tailwind() — Tailwind CSS v4 export. Returns a single ready-to-paste CSS string: an @theme block (light baseline) plus dark / high-contrast overrides under configurable selectors. The --color-* namespace (configurable via namespace) auto-generates bg-* / text-* / border-* utilities. darkSelector (default .dark) accepts an at-rule like '@media (prefers-color-scheme: dark)' (nests :root automatically); highContrastSelector (default .high-contrast) covers the HC variants, with the combined block at ${darkSelector}${highContrastSelector}. Default format is 'oklch'.
    • New types exported from the package entry: DtcgColorSpace, DtcgSrgbColorValue, DtcgOklchColorValue, DtcgColorValue, DtcgColorToken, DtcgDocument, GlazeDtcgResult, GlazeColorDtcgResult, GlazeDtcgOptions, GlazeTailwindOptions, GlazeColorTailwindOptions.
    • New color-math helpers exported for advanced use: srgbToHex(rgb) (sRGB 0–1 → #rrggbb) and okhslToOklch(h, s, l, pastel?) (OKHSL → [L, C, H]), shared by formatOklch and the DTCG exporter.
    • Both new exports honor modes (dark / high-contrast gating) and the palette prefix / primary options. On palette.tailwind(), the palette theme-prefix prefix is separate from GlazeTailwindOptions.namespace (the --color-* CSS namespace).
  • #69 86c3d27 Thanks @tenphi! - feat+breaking: oklch hue channel splitting (pastel-only); add okhst tasty-only output; okhsl/okhst are tasty-only; tokens/json default to oklch

    • Add splitHue on css() / tasty() (theme + palette) and standalone color.css() — emits hue as a separate custom property referenced via var() in oklch values. Requires every exported color to be pastel.
    • Add 'okhst' output format (okhst(H S% T%)) for Tasty exports.
    • okhsl and okhst throw on non-Tasty exports (css, tailwind, tokens, json).
    • tokens() / json() default format changes from okhsl to oklch (theme, palette, standalone .json()).
  • #69 86c3d27 Thanks @tenphi! - Add semantic color roles with APCA polarity and APCA presets

    • Roles. Colors now carry a semantic role ('text' | 'surface' | 'border', with aliases like bg/fg/divider/outline/fill/ink/…). The role fixes APCA contrast polarity — which side is the foreground vs the background — so the APCA solver uses the correct argument order instead of always treating the resolved color as text. WCAG is symmetric and unaffected.
    • Role inference. Roles are inferred from the color name by default (inferRole: true), with the last recognized token winning (button-text → text, input-bg → surface, card-outline → border). When a name doesn't infer, the opposite of the base's role is used; otherwise the color defaults to text (foreground), preserving previous behavior. Set glaze.configure({ inferRole: false }) to opt out of name inference.
    • APCA presets. APCA targets accept named Bronze Simple Mode presets: 'preferred' (Lc 90), 'body' (75), 'content' (60, ~AA), 'large' (45, ~3:1), 'non-text' (30), 'min' (15). Use anywhere an APCA target is accepted, e.g. contrast: { apca: 'content' } or contrast: { apca: ['content', 'body'] }. Presets are role-independent.
    • role is also available on MixColorDef and standalone glaze.color() inputs and survives the export() / glaze.colorFrom() round-trip.

0.15.1

Patch Changes

  • #67 3f70089 Thanks @tenphi! - Allow pastel to be set per color, not just globally.

    Every color definition (RegularColorDef, ShadowColorDef, MixColorDef) and glaze.color() token now accepts an optional pastel?: boolean that overrides the global / per-theme pastel config for that color only. Omit it to keep inheriting the config default.

0.15.0

Minor Changes

  • #65 7663cb8 Thanks @tenphi! - Add pastel config option, getConfig() to GlazeTheme, and export cuspLightness.

    • pastel: A new configuration option (boolean, default false) has been added to GlazeConfig and FindToneForContrastOptions. When enabled, it uses a hue-independent "safe" chroma limit across all colors so that scaling saturation never exceeds the sRGB boundary at any hue for the given lightness.
    • getConfig(): Added to GlazeTheme to allow retrieving the effective configuration (GlazeConfigResolved) for a theme.
    • cuspLightness(h): Exported from okhsl-color-math to allow retrieving the OKHSL lightness of the gamut cusp for a given hue.
  • #65 7663cb8 Thanks @tenphi! - Remove the cusp-anchored saturation taper completely.

    Breaking change

    • saturationTaper has been removed from GlazeConfig and GlazeConfigOverride as well as from glaze.configure(). The concept of tapering/clamping saturation at lightness extremes is no longer supported, and colors are allowed to maintain their requested saturation across the entire lightness spectrum.
    • saturationTaper has also been removed from FindToneForContrastOptions.

Patch Changes

  • #65 7663cb8 Thanks @tenphi! - docs: update project description to use OKHST instead of OKHSL

0.14.0

Minor Changes

  • #63 3b6e2a6 Thanks @tenphi! - Replace the OKHSL lightness axis with a contrast-uniform tone axis (OKHST) and remove the Möbius dark-mode curve.

    Breaking changes

    • The lightness authoring prop is gone. Use tone (0–100, contrast-uniform) everywhere — theme colors, glaze.color() structured input, and relative offsets. Equal tone steps now give equal WCAG contrast, so numeric values won't map to the same OKHSL lightness as before; re-check absolute mid-range values.
    • Config windows changed: lightLightness / darkLightness → lightTone / darkTone. A window is [lo, hi] (reference eps — the common form), { lo, hi, eps } (advanced eps tuning), or false to disable clamping. false removes the boundaries (full [0, 100] range) but keeps the contrast-uniform tone curve. darkCurve was removed.
    • ResolvedColorVariant now stores { h, s, t, alpha } (tone) instead of { h, s, l }. Use the new variantToOkhsl() helper to recover OKHSL lightness.
    • Export snapshots now carry lightTone / darkTone windows.
    • Relative tone offsets that overshoot [0, 100] now mirror to the other side of the base by default (the new flip, inheriting autoFlip) instead of clamping. Set flip: false (or autoFlip: false) to restore clamping.

    New

    • tone: 'max' / 'min' forces a color to the scheme's tone extreme (lightest / darkest) with no base and no contrast hack; under mode: 'auto' they invert in dark like any tone.
    • flip per-color prop (default: global autoFlip): mirrors out-of-bounds relative tone overshoot and unmet contrast to the opposite side of the base, or clamps when false.
    • Tone windows accept the [lo, hi] array shorthand alongside { lo, hi, eps } and false.
    • contrast accepts a metric selector: a bare number/preset is WCAG, { wcag } / { apca } picks the metric, and the [normal, hc] pair may live at the outer level or inside the metric ({ wcag: [4.5, 7] }).
    • APCA Lc contrast solving alongside WCAG, plus an APCA-based drift verification warning for chromatic swatches.
    • OKHST input: okhst(H S% T%) strings and { h, s, t } objects (input only — never emitted).
    • saturationTaper config knob (default 0.15) gently rolls off saturation toward the tone extremes.
    • New exports: toTone, fromTone, toneFromY, yFromTone, okhstToOkhsl, okhslToOkhst, variantToOkhsl, REF_EPS, findToneForContrast, resolveContrastForMode, apcaContrast, and the ContrastSpec / OkhstColor / ToneWindow / ExtremeValue / ToneValue types.

Patch Changes

  • #63 3b6e2a6 Thanks @tenphi! - Adjust the default tone-window floors: lightTone is now [10, 100] (was [13, 100]) and darkTone is now [15, 95] (was [10, 95]). The OKHST migration made dark schemes bottom out darker than the legacy pipeline for the same input; lifting the dark floor keeps the darkest dark-mode surfaces closer to the previous output, and lowering the light floor widens the usable light range. Override with lightTone: [13, 100] / darkTone: [10, 95] to restore the prior values.

0.13.0

Minor Changes

  • #61 43be630 Thanks @tenphi! - Redesign glaze.color() input API and add per-instance config overrides.

    Breaking: glaze.color() arg layout changed

    The old two-overload signature (value, overrides?, scaling? / structured, scaling?) is replaced by a single unified signature:

    glaze.color(color, config?)
    • color (arg 1): four shapes discriminated by structure:
      • Bare string → glaze.color('#26fcb2')
      • Value object → glaze.color({ h: 152, s: 0.95, l: 0.74 })
      • New { from, ...overrides } → glaze.color({ from: '#fff', base: bg, contrast: 'AA' })
      • Structured → glaze.color({ hue: 152, saturation: 95, lightness: 74 })
    • config (arg 2, optional): GlazeConfigOverride — overrides resolve-relevant config fields for this token only.

    Migration

    // Before
    glaze.color('#fff', { base: bg, contrast: 'AA' });
    glaze.color('#fff', undefined, { darkLightness: false });
    glaze.color('#fff', { opacity: 0.5 }, { lightLightness: false });
    
    // After
    glaze.color({ from: '#fff', base: bg, contrast: 'AA' });
    glaze.color('#fff', { darkLightness: false });
    glaze.color({ from: '#fff', opacity: 0.5 }, { lightLightness: false });

    New: per-instance config override (GlazeConfigOverride)

    • Applies to both glaze.color() and glaze() themes (second arg).
    • Fields: lightLightness, darkLightness, darkDesaturation, darkCurve, autoFlip, shadowTuning.
    • false for a lightness window disables clamping globally (= [0, 100]). Now accepted everywhere: configure(), per-token, and per-theme.
    • Token config is snapshotted at creation. Theme config merges over the live global at resolve time (non-overridden fields still react to configure()).
    • extend({ config }) inherits and merges parent + child overrides.
    • theme.export() / glaze.from() round-trip the config.

    New: { from } form for value colors with overrides

    Pass a raw color value alongside color overrides in one object:

    const text = glaze.color({ from: '#1a1a2e', base: bg, contrast: 'AA' });

    New: full-range base conversion

    When a value/from color links to a base created via the structured form, the contrast/lightness anchor uses the raw input lightness (not the windowed output). This ensures accurate anchoring without affecting the base's own resolved output.

    Breaking: removed GlazeColorScaling

    GlazeColorScaling is removed and replaced by GlazeConfigOverride. Scaling was limited to lightness windows; the new type covers the full resolve-relevant config.

0.12.0

Minor Changes

  • #60 5fef8c5 Thanks @tenphi! - Breaking: glaze.color() value-shorthand changes:
    • Removed RGB tuple [r, g, b] — use { r, g, b } instead.
    • Added RgbColor ({ r, g, b }) and OklchColor ({ l, c, h }) object inputs (also accepted by glaze.shadow()).
    • Unified scaling for all value-shorthand (strings and literal objects): lightLightness: false, darkLightness: globalConfig.darkLightness (snapshotted). Strings no longer use the extended [darkLo, 100] dark window — the default #000 → white dark flip is gone unless you pass explicit scaling: { darkLightness: [lo, 100] }.
    • Object/tuple value-shorthand no longer remap light lightness through globalConfig.lightLightness (structured { hue, saturation, lightness } still does). Opt back in with scaling: { lightLightness: [10, 100], ... }.

Patch Changes

  • #58 9fdb6bb Thanks @tenphi! - Respect authored lightness direction when contrast auto-flip is disabled.

0.11.1

Patch Changes

  • #56 978bf60 Thanks @tenphi! - Restructure documentation into focused README, API, methodology, and migration guides.

  • #56 978bf60 Thanks @tenphi! - Internal refactor: split the 2636-line src/glaze.ts into focused, flat modules (config, hc-pair, shadow, warnings, scheme-mapping, validation, resolver, formatters, theme, palette, color-token) and dedupe a few parallel structures:

    • The resolver's four-pass loop is now a single runPass() + seedField() helper called four times.
    • The palette tokens / tasty / css exporters share a buildPaletteOutput() driver instead of duplicating the per-theme loop / prefix resolution / collision filtering / primary-duplication logic.
    • The default-config literal is no longer duplicated between module init and resetConfig(); both call a shared defaultConfig().
    • theme exports now cache the resolve result and invalidate it on any def mutation or configure() / resetConfig() call (via a new configVersion counter), so back-to-back exports don't re-run the four-pass resolver.

    No public API or behavior changes.

0.11.0

Minor Changes

  • #54 88be8a4 Thanks @tenphi! - glaze.color() now defaults to mode: 'auto' across every input form, so non-string inputs adapt between light and dark like an ordinary theme color instead of being preserved verbatim with a linear dark mapping.

    • Object value-shorthand ({ h, s, l }), RGB tuple ([r, g, b]), and structured form ({ hue, saturation, lightness, ... }) now default to mode: 'auto' with snapshotted scaling { lightLightness: globalConfig.lightLightness, darkLightness: globalConfig.darkLightness }. The dark variant is Möbius-inverted into globalConfig.darkLightness (default [15, 95]), and the light variant is mapped through globalConfig.lightLightness (default [10, 100]) — exactly the same windows a theme color uses.
    • String value-shorthand (hex / rgb() / hsl() / okhsl() / oklch()) is unchanged. It already defaulted to mode: 'auto' with { lightLightness: false, darkLightness: [lo, 100] }, preserving the #000 ↔ #fff flip.

    Behavior change (minor bump):

    • glaze.color({ hue: H, saturation: S, lightness: 80 }).resolve() (and the equivalent object / tuple forms) now produces a near-dark dark.l (e.g. ~0.42 for lightness: 80 under defaults) instead of staying near 0.79.
    • light.l for object / tuple / structured inputs is now mapped through globalConfig.lightLightness rather than preserved verbatim (e.g. lightness: 0 now resolves to light.l ≈ 0.10 by default).
    • To restore the previous fixed-linear behavior, pass { mode: 'fixed' } on the input or in the overrides. To restore the previous "preserve light lightness verbatim" behavior, pass { lightLightness: false } as the trailing scaling argument.

    The new scaling shape is also reflected in token.export() snapshots — object / tuple / structured tokens now serialize { lightLightness: [10, 100], darkLightness: [15, 95] } (with the live globalConfig values frozen at create time) instead of { lightLightness: false, darkLightness: [15, 95] }. Rehydration via glaze.colorFrom() round-trips byte-for-byte.

0.10.1

Patch Changes

  • #52 1988ff8 Thanks @tenphi! - Fix srgbToOkhsl (and downstream glaze.color()) returning a bogus saturated hue/saturation for pure white (#FFFFFF) and other colors at the OKHSL lightness extremes. Floating-point residue from linearSrgbToOklab slipped past the existing chroma epsilon, sending the chromatic path through a degenerate gamut where saturation divides by ~zero. White now correctly resolves to okhsl(0 0% 100%) (light) / okhsl(0 0% 15%) (dark) instead of okhsl(89.88 55.83% 100%).

0.10.0

Minor Changes

  • #50 6e2d42d Thanks @tenphi! - Revamp glaze.color() with a value-shorthand overload, seed-anchored contrast solving, a per-call lightness-scaling argument, and a .css() export. glaze.shadow() now accepts the same value forms as glaze.color().

    New defaults for glaze.color() — split by input form so end-user string values (color picker / theme settings) get a natural light/dark inversion, while programmatic object / tuple / structured inputs keep predictable linear behavior:

    • String value-shorthand (hex, rgb(), hsl(), okhsl(), oklch()): mode: 'auto' with snapshotted scaling { lightLightness: false, darkLightness: [globalConfig.darkLightness[0], 100] }. Light preserves the input exactly; dark Möbius-inverts up to 100, so glaze.color('#000') renders as #fff in dark mode and glaze.color('#fff') falls to the dark lo floor (default 0.15). The dark lo is snapshotted from globalConfig at color-creation time, matching how an explicit scaling.darkLightness: [lo, hi] behaves.
    • Object / tuple value-shorthand ({ h, s, l }, [r, g, b]) and structured form: mode: 'fixed' with light preserved and dark linearly mapped into globalConfig.darkLightness (default [15, 95]), also snapshotted at create time so later glaze.configure() calls don't retroactively change already-created tokens.
    • Override per call via the new third positional argument GlazeColorScaling: { lightLightness?: false | [lo, hi]; darkLightness?: false | [lo, hi] }. false disables the remap, a tuple sets a custom window. To opt string inputs back into the previous fixed-linear default, pass { mode: 'fixed' } as the second arg or supply an explicit scaling.

    Behavior change (minor bump):

    • String value-shorthand callers will see a Möbius-inverted dark variant by default — glaze.color('#000').resolve().dark.l is now ≈ 1.0, not 0.15. To preserve the old fixed-linear behavior pass { mode: 'fixed' } as the second argument.
    • Structured callers without an explicit mode will see glaze.color({...}).resolve().light.l match the input lightness exactly instead of being remapped to globalConfig.lightLightness. To preserve the old behavior pass { lightLightness: globalConfig.lightLightness } as the second argument.
    • The default lightness windows for object / tuple / structured inputs are now snapshotted from globalConfig.darkLightness at color-creation time, matching the existing behavior for string inputs. Tokens created before a glaze.configure() call no longer pick up the new dark window on their next .resolve(). To get the old "live config" behavior, recreate the token after configure().

    Value shorthand additions:

    • Accepts hex (#rgb / #rrggbb / #rrggbbaa), the four CSS color functions Glaze itself emits (rgb(), hsl(), okhsl(), oklch()), OkhslColor objects ({ h, s, l }), and [r, g, b] (0–255) tuples as the first argument. Every string emitted by theme.tasty() / .json() / .css() round-trips back through glaze.color().
    • 8-digit hex and rgba() / hsla() / slash-alpha alpha components are parsed and dropped with a console.warn (standalone colors have no opacity field).
    • oklch() chroma now correctly interprets percent values per CSS Color 4 (100% → 0.4).
    • OkhslColor and [r, g, b] inputs are validated up front with helpful error messages — passing 0–100-scale s/l throws with a hint to use the structured form, and out-of-range RGB tuples throw with the offending value in the message.

    Anchor model: by default, relative lightness: '+N' and contrast: <ratio> are anchored to the literal seed (the value passed to glaze.color()), so the contrast solver compares against the unmapped user-provided color across every variant. Pass overrides.base (a GlazeColorToken) to anchor against another color's resolved variant per scheme instead.

    Color pairing via base: GlazeColorOverrides.base lets one standalone color depend on another. Accepts either a GlazeColorToken or any GlazeColorValue (hex / rgb() / OkhslColor / [r, g, b]); raw values are auto-wrapped via glaze.color(value) and inherit the same string-vs-object defaults. When set:

    • contrast is solved per scheme against the base's resolved variant (light / dark / lightContrast / darkContrast).
    • Relative lightness: '+N' / '-N' is anchored to the base's lightness per scheme (matches theme behavior for dependent colors).
    • Relative hue: '+N' still anchors to the seed (the value passed to glaze.color()), not the base.
    • mode is the per-pair knob — pass mode: 'fixed' to disable Möbius inversion for the dependent color, mode: 'auto' to keep it.

    The base token's .resolve() is called lazily on first resolve and the result is captured by reference, matching existing snapshot semantics. Internally, resolveAllColors accepts pre-resolved external bases and seeds them into the resolution context; validateColorDefs and topoSort treat external base names as leaves.

    opacity and name on glaze.color():

    • GlazeColorOverrides.opacity (and the same field on GlazeColorInput) sets a fixed alpha 0–1 that surfaces in every scheme variant. Combining with contrast is not recommended (perceived lightness becomes unpredictable) — glaze emits a console.warn in that case.
    • GlazeColorOverrides.name (and the same field on GlazeColorInput) is a human-readable label that surfaces in error and warning messages in place of the internal "value" sentinel. Empty / whitespace-only names and reserved internal names ("value", "seed", "externalBase") are rejected with a clear error.

    Structured form parity: the glaze.color({...}) overload now accepts opacity, contrast, base, and name in addition to the existing hue, saturation, lightness, saturationFactor, and mode. contrast without base synthesizes a hidden static seed from the input's normal-mode lightness so the contrast solver always has an anchor (mirrors value-form behavior). hue (finite), saturation / lightness (0–100), saturationFactor (0–1), and opacity (0–1) are range-checked up front with helpful error messages — non-finite or out-of-range values fail at creation rather than producing a NaN-laden token.

    Contrast warning: when the contrast solver cannot meet the requested target (e.g. AAA against a mid-grey base — physically unreachable), glaze emits a single console.warn per (name, scheme, target) triple naming the affected color, scheme, and the actual achieved ratio. The token still resolves to the closest passing variant. Use the name override to make the warning easier to trace.

    Persisting standalone colors: token.export() returns a JSON-safe snapshot containing the original value (or structured input), the overrides, and the captured scaling. Token-typed base is recursively serialized; value-typed base is preserved as the raw value. Pass the result to glaze.colorFrom(data) to rehydrate a token that resolves byte-for-byte identically to the original — across glaze.configure() calls and across processes. The captured scaling snapshots both lightLightness and darkLightness from globalConfig at create time, so later glaze.configure() calls don't retroactively change exported tokens regardless of input form.

    .css({ name }) export: new method on the standalone color token reaches export parity with theme.css(). Existing .token() / .tasty() / .json() continue to work unchanged.

    glaze.shadow() upgrade: bg and fg now accept any GlazeColorValue form — hex, rgb() / hsl() / okhsl() / oklch() strings, OkhslColor objects, or [r, g, b] tuples — sharing the same parser as glaze.color().

    Internal: standalone color tokens now memoize the underlying resolve across .resolve() / .token() / .tasty() / .json() / .css() calls.

    Public type additions: GlazeColorValue, GlazeColorOverrides, GlazeColorOverridesExport, GlazeColorCssOptions, GlazeColorScaling, GlazeColorTokenExport, GlazeColorInputExport. New glaze.colorFrom(data) factory and token.export() method on GlazeColorToken. New hslToSrgb, oklabToOkhsl, and parseHexAlpha math helpers re-exported from the package root.

0.9.3

Patch Changes

  • 762c204 Thanks @tenphi! - Unify contrast search overshoot to 1% in both lightness and mix solvers.

0.9.2

Patch Changes

  • #46 85111ca Thanks @tenphi! - Add inherit flag to color definitions to prevent inheritance during extend()

0.9.1

Patch Changes

  • #44 50340c4 Thanks @tenphi! - darkCurve now accepts a [normal, highContrast] pair for separate HC tuning.

0.9.0

Minor Changes

  • #43 0575838 Thanks @tenphi! - Move primary option from per-export to glaze.palette() creation, add collision detection (warn + first-write-wins) across all palette export methods.

Patch Changes

  • #40 c47cfd2 Thanks @tenphi! - Unify dark/HC lightness mapping into a single code path via lightnessWindow helper.

  • #42 941338a Thanks @tenphi! - darkCurve now accepts a [normal, highContrast] pair for separate HC tuning.

0.8.0

Minor Changes

  • #37 8b0b62b Thanks @tenphi! - Bypass lightLightness and darkLightness window constraints in high-contrast mode, allowing colors to use the full 0–100 lightness spectrum for increased perceivable contrast.

Patch Changes

  • #39 f9f6def Thanks @tenphi! - Add darkCurve config option for perceptual dark-theme lightness inversion using a power curve. Expands subtle near-white distinctions in dark mode. Default 0.5; set to 1 for legacy linear behavior. Widen contrast solver search range to [0, 1] so contrast targets are met regardless of dark lightness window.

  • #39 f9f6def Thanks @tenphi! - Replace power-curve dark lightness mapping with Möbius transformation for proportional expansion of lightness deltas across all sizes.

0.7.0

Minor Changes

  • #34 2278b4e Thanks @tenphi! - Add primary option to palette exports (tokens, tasty, css) that duplicates one theme's tokens without prefix. Palette prefix now defaults to true.

Patch Changes

  • #36 e9e6ef6 Thanks @tenphi! - Fix contrast solver undershooting WCAG targets when using OKLCH output format. Increase OKLCH hue precision to 2dp and widen solver overshoot margin.

0.6.3

Patch Changes

  • d148498 Thanks @tenphi! - Apply lightLightness mapping to dependent colors with absolute lightness, matching darkLightness behavior.

  • #32 90bd23c Thanks @tenphi! - Propagate scheme lightness range to contrast solver for dependent colors, preventing pure black/white output when contrast-solving against extreme lightness values.

0.6.2

Patch Changes

  • #30 06be989 Thanks @tenphi! - Use 1% proportional contrast overshoot to reliably meet WCAG targets after 8-bit RGB quantization.

0.6.1

Patch Changes

  • #28 8085e0f Thanks @tenphi! - Increase contrast search overshoot to avoid floating-point rounding below WCAG threshold.

0.6.0

Minor Changes

  • #25 7462021 Thanks @tenphi! - Add mix color type for blending two colors with optional contrast solving
    • New MixColorDef with type: 'mix' — blend two referenced colors via base and target
    • Opaque blend: interpolates in OKHSL or sRGB space, producing a solid color
    • Transparent blend: outputs the target color with controlled opacity (alpha = value/100)
    • space option: 'okhsl' (default, perceptually uniform) or 'srgb' (matches browser compositing)
    • contrast option: adjusts mix ratio or opacity to meet a WCAG contrast floor against the base
    • Achromatic hue handling: when mixing with unsaturated colors (e.g. white/black), the hue is taken from the saturated color
    • value and contrast support [normal, highContrast] pairs
    • Mix colors can reference other mix colors (chaining) but not shadow colors

Patch Changes

  • #27 175fc81 Thanks @tenphi! - Fix green-channel k4 sign error in OKHSL gamut mapping, deduplicate internal color math, add round-trip regression tests.

0.5.8

Patch Changes

  • #23 a06a843 Thanks @tenphi! - Fix contrast solver using wrong target variable for preferred lightness check, and increase decimal precision in OKHSL/HSL color formatting from 1 to 2 digits.

0.5.7

Patch Changes

  • d7f00b1 Thanks @tenphi! - Fix contrast solver computing WCAG luminance from unclamped linear sRGB, which caused it to overestimate contrast for high-saturation colors near gamut boundaries (e.g. lime green). The solver now matches the browser rendering pipeline by gamma-encoding, clamping to sRGB gamut, then linearizing before computing luminance.

0.5.6

Patch Changes

  • 5acac86 Thanks @tenphi! - Add +0.01 margin to the contrast solver's internal search target to prevent floating-point rounding from producing contrast ratios like 4.4999… that fail Lighthouse's exact WCAG AA threshold check.

0.5.5

Patch Changes

  • 3b8cdbf Thanks @tenphi! - Remove spurious warning when a color has both absolute lightness and base. This is a valid configuration — base is still used for minimum contrast calculation.

0.5.4

Patch Changes

  • #18 b4f799b Thanks @tenphi! - Add lightLightness configuration option for controlling lightness bounds in light schemes.

0.5.3

Patch Changes

  • #16 f34cb5e Thanks @tenphi! - Updated default darkLightness range from [10, 90] to [15, 95] for improved dark mode color mapping.

0.5.2

Patch Changes

  • #14 6baff6b Thanks @tenphi! - Fix relative lightness application: allow absolute lightness values when using base colors for contrast solving. Previously, colors with both base and absolute lightness were incorrectly rejected during validation and topological sorting.

0.5.1

Patch Changes

  • #12 e869204 Thanks @tenphi! - Fix shadow intensity normalization to properly scale alpha values across different background/foreground contrast pairs. Shadow alpha now correctly reaches alphaMax (default 1.0) at intensity=100 with maximum contrast.

0.5.0

Minor Changes

  • #10 79253fc Thanks @tenphi! - Add CSS custom property export method for themes and palettes

  • #10 79253fc Thanks @tenphi! - Add shadow color support and standalone shadow/format APIs

    • Shadow colors via ShadowColorDef (type: 'shadow') with OKHSL-native algorithm using tanh alpha curve
    • glaze.shadow() standalone factory for one-off shadow computation
    • glaze.format() to format any ResolvedColorVariant as CSS
    • opacity field on RegularColorDef for fixed alpha on regular colors
    • alpha field on ResolvedColorVariant (default 1)
    • shadowTuning on GlazeConfig for global shadow defaults
    • ResolvedColor.mode is now optional (omitted for shadow colors)
    • Intensity is clamped to [0, 100]
    • Validation: shadow bg/fg cannot reference other shadows; regular color base cannot reference a shadow
  • #10 79253fc Thanks @tenphi! - Switch formatRgb and formatHsl from comma syntax to modern CSS space syntax

    rgb(R, G, B) → rgb(R G B) and hsl(H, S%, L%) → hsl(H S% L%). rgb output now uses rounded integers instead of fractional values. This enables alpha support via the / alpha separator and aligns with modern CSS (supported since Chrome 65+, Firefox 52+, Safari 12.1+).

    Downstream code that parses Glaze's CSS output using comma-separated patterns must update to space-separated syntax.

0.4.0

Minor Changes

  • #8 eb7dec7 Thanks @tenphi! - Add explicit tasty() method for Tasty style-to-state bindings. tokens() now returns a flat variant-grouped format, while tasty() provides the original style-to-state binding format compatible with Tasty recipes and global styles.

0.3.0

Minor Changes

  • #6 bb4a0d1 Thanks @tenphi! - Add CSS custom property export method for themes and palettes

0.2.0

Minor Changes

  • #4 5cff288 Thanks @tenphi! - Redesigned ColorDef API: unified l/contrast into lightness (supports absolute numbers or relative strings), renamed ensureContrast to contrast, added per-color hue override, and renamed sat to saturation. Removed unsigned auto-flip behavior for contrast deltas.