Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/quiet-pugs-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@tenphi/glaze': minor
---

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.
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ glaze/
| [src/color-token.ts](src/color-token.ts) | Standalone `glaze.color()` tokens. Parses values through `color-value.ts`; owns the structured-input validator, the two factory paths, sparse local config + live resolve, and the JSON-safe export / `glaze.colorFrom` rehydrate round-trip. |
| [src/color-value.ts](src/color-value.ts) | `GlazeColorValue` parsing + validation: hex (3/6/8), the `rgb()` / `hsl()` / `okhsl()` / `okhst()` / `oklch()` functions, and the four value-object shapes, all normalized to OKHSL by `extractOkhslFromValue`. Split out of `color-token.ts` so the resolver can reach it for a theme color's `from` without closing an import cycle. Leaf module — depends only on the color math. |
| [src/serialize.ts](src/serialize.ts) | Authoring-export helpers: `GLAZE_EXPORT_VERSION`, `assertExportKind` / `assertExportVersion`, and `isThemeExport` / `isColorTokenExport` / `isPaletteExport` type guards. |
| [src/resolver.ts](src/resolver.ts) | Four-pass solver (light → light-HC → dark → dark-HC), or two-pass (light → dark) under a manual `contrastLevel`, where the HC slots mirror the normal ones, `passTone` / `passNumber` / `resolveContrastSpec` feed interpolated inputs into the ordinary pass, and a probe solve at the nearer endpoint pins which side of its base a contrast-solved color sits on. Stores canonical tone (`t`) in variants; per-scheme branches for regular, shadow, and mix defs; integrates the contrast solver, and the OKHST tone helpers. `resolveChannels` owns per-scheme hue/saturation (incl. the `darkHue` / `darkSaturation` seed + def overrides, the `darkDesaturation` bypass, and the absolute hue/saturation a `from` color carries in place of the seed factor) and feeds both the emitted variant and the contrast solver. `configForColor` gives a `from` color a local `lightTone: false` so its light variant reproduces the authored value. Converts to/from OKHSL lightness only at the mix/shadow edges. Pre-seeds externally-resolved bases for `glaze.color({ base })`. |
| [src/resolver.ts](src/resolver.ts) | Four-pass solver (light → light-HC → dark → dark-HC), dropping to two-pass (light → dark) with mirrored HC slots only at `contrastLevel: 100`, where the normal passes already are the HC ones (`hcMirrorsNormal`). Under any other manual level `passTone` / `passNumber` / `resolveContrastSpec` feed interpolated inputs into the two *normal* passes and a probe solve at the nearer endpoint pins which side of its base a contrast-solved color sits on; the HC passes bypass the level and the probe entirely, so the HC tier is bit-identical to `'auto'`. Stores canonical tone (`t`) in variants; per-scheme branches for regular, shadow, and mix defs; integrates the contrast solver, and the OKHST tone helpers. `resolveChannels` owns per-scheme hue/saturation (incl. the `darkHue` / `darkSaturation` seed + def overrides, the `darkDesaturation` bypass, and the absolute hue/saturation a `from` color carries in place of the seed factor) and feeds both the emitted variant and the contrast solver. `configForColor` gives a `from` color a local `lightTone: false` so its light variant reproduces the authored value. Converts to/from OKHSL lightness only at the mix/shadow edges. Pre-seeds externally-resolved bases for `glaze.color({ base })`. |
| [src/okhst.ts](src/okhst.ts) | The OKHST tone layer. `REF_EPS`, tone↔lightness transfers (`toTone`/`fromTone`, `toneFromY`/`yFromTone`), OKHST↔OKHSL conversions, `variantToOkhsl` (tone→lightness at render), `normalizeToneWindow` (`[lo,hi]` / `{lo,hi,eps}` / `false` → `{lo,hi,eps}`), `mapToneForScheme` (scheme inversion + window remap, HC bypass), `mapSaturationDark` (the `darkDesaturation` reducer the resolver skips when a dark saturation is authored), and `schemeToneRange` for the solver. `activeWindow` owns the HC window bypass and its continuous form under `contrastLevel`. Only tone adapts here — hue/saturation are the resolver's business. |
| [src/contrast-solver.ts](src/contrast-solver.ts) | Tone-based binary-search solver for WCAG **and** APCA. Public API: `findToneForContrast` (incl. the `preferInitial` tie-break the manual level uses to keep a color's side stable), `findValueForMixContrast`, `resolveContrastForMode`, `resolveContrastForLevel` (both ends resolved, target interpolated for a manual `contrastLevel`; throws on a metric switch), `contrastMetricOf`, `resolveMinContrast`, `apcaContrast`. Closed-form WCAG seed + tone search. |
| [src/contrast-solver.ts](src/contrast-solver.ts) | Tone-based binary-search solver for WCAG **and** APCA. Public API: `findToneForContrast` (incl. the `preferInitial` tie-break the manual level uses on its normal passes to keep a color's side stable), `findValueForMixContrast`, `resolveContrastForMode`, `resolveContrastForLevel` (both ends resolved, target interpolated for a manual `contrastLevel`; throws on a metric switch), `contrastMetricOf`, `resolveMinContrast`, `apcaContrast`. Closed-form WCAG seed + tone search. |
| [src/shadow.ts](src/shadow.ts) | Shadow + mix def predicates (`isShadowDef`, `isMixDef`), default `ShadowTuning`, tuning merge, the actual `computeShadow` math (hue blend, saturation cap, lightness clamp, `tanh` alpha curve) operating on OKHSL lightness at the edge, and `circularLerp` for hue. |
| [src/hc-pair.ts](src/hc-pair.ts) | Tiny shared helpers: HC-pair selection (`pairNormal` / `pairHC`), HC-pair interpolation for the manual contrast level (`contrastFraction`, `levelFraction`, `numberAt`, `parseToneValueAt`, `PAIR_SWITCH`), tone-value parsing (`parseToneValue` for absolute / relative / `'max'`/`'min'` extremes, `isExtremeTone`), root-tone discrimination (`isAbsoluteTone`, now incl. extremes), `clamp` / `lerp`, hue resolution (`resolveEffectiveHue`), relative-value parsing. Leaf module — types only, so every other module can import it. |
| [src/hc-pair.ts](src/hc-pair.ts) | Tiny shared helpers: HC-pair selection (`pairNormal` / `pairHC`), HC-pair interpolation for the manual contrast level (`contrastFraction`, `hcMirrorsNormal` — the level-100 "HC slots are duplicates" predicate, `levelFraction`, `numberAt`, `parseToneValueAt`, `PAIR_SWITCH`), tone-value parsing (`parseToneValue` for absolute / relative / `'max'`/`'min'` extremes, `isExtremeTone`), root-tone discrimination (`isAbsoluteTone`, now incl. extremes), `clamp` / `lerp`, hue resolution (`resolveEffectiveHue`), relative-value parsing. Leaf module — types only, so every other module can import it. |
| [src/formatters.ts](src/formatters.ts) | Variant→string dispatch (`okhsl` / `rgb` / `hsl` / `oklch`) and the four token-map shapes Glaze emits: `buildTokenMap` ([Tasty](https://tasty.style) `#name` keys + state aliases), `buildFlatTokenMap` (per-variant maps), `buildJsonMap` (per-color JSON), `buildCssMap` (CSS custom-property declarations). For `splitHue` it emits the light hue declarations plus, via `darkHueDeclarations` in `channels.ts`, the whole dark set when a `darkHue` makes it differ. Also the `resolveModes` helper used everywhere. |
| [src/okhsl-color-math.ts](src/okhsl-color-math.ts) | OKHSL ↔ linear-sRGB ↔ gamma-sRGB ↔ OKLab conversions, hex parsing (3/6/8 digits), gamut clamping, and the `formatOkhsl` / `formatRgb` / `formatHsl` / `formatOklch` writers. The only file with the actual color science. |
| [src/config.ts](src/config.ts) | Global config singleton. `defaultConfig()` is the one source of truth for defaults; `configure()` mutates the live object and bumps a monotonic `configVersion` so theme caches invalidate. `snapshotConfig()` powers `glaze.getConfig()`. `freezeConfigForExport()` freezes resolve-relevant fields for authoring export (`pastel` is instance-only; `contrastLevel` is frozen only when instance-authored, since a global level is a live preference). `normalizeContrastLevel()` validates the manual contrast level. |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ contrast floor.
- **Mix colors** — blend two colors with OKHSL or sRGB interpolation, opaque or transparent, with optional contrast solving
- **Shadow colors** — OKHSL-native shadow computation with automatic alpha, fg/bg tinting, and per-scheme adaptation
- **Light + Dark + High-Contrast** — all schemes from one definition
- **Manual contrast level** — swap the high-contrast tier for a `0–100` slider; `0` is your normal palette, `100` the high-contrast one, and every level in between is solved, not approximated
- **Manual contrast level** — put your normal palette on a `0–100` slider between itself and the high-contrast one; every level in between is solved, not approximated, and the high-contrast tier stays available on top
- **Per-color hue override** — absolute or relative hue shifts within a theme
- **Multi-format output** — native `rgb`, `hsl`, and `oklch`, plus
[Tasty](https://tasty.style)-compatible `okhsl` and `okhst`
Expand Down
Loading
Loading