Repository navigation
v3.0.0
klar 3.0 measures out-of-gamut colors honestly, and says so instead of passing them through silently.
A color authored in OKLCH can sit outside sRGB. It has to be resolved to a displayable color before it can be measured — so the number describes that mapped equivalent, not the color in your token file. Previous versions reported it without a word.
⚠️ Read this before upgrading
1. contrast now exits 1 on out-of-gamut input
The value still goes to stdout. The exit code stops it being adopted as though it described what you asked about.
Expect this to fire. On a real OKLCH-authored design system, 29% of distinct color tokens and 21% of measured pairings were outside sRGB — including subtle ones like oklch(0.97 0.02 278.14). The gamut narrows to a point at both lightness extremes, so low chroma is no protection.
Under set -e, a bare assignment aborts your script at the first such token, leaving a partial result that reads like a complete short run:
# Guard it
for T in "${TOKENS[@]}"; do
if OKCA=$(klar contrast "$T" "$BG" -q); then
echo "$T -> $OKCA"
else
echo "$T -> $OKCA (outside sRGB — figure describes the mapped color)"
fi
doneNode's execSync throws the same way, stranding the value in err.stdout:
let out;
try { out = execSync(cmd, { encoding: 'utf8' }); }
catch (e) { out = e.stdout; outOfGamut = true; } // exit 1 still produced a valuePass --allow-out-of-gamut to waive it per call. There is deliberately no environment variable for the waiver — a global mute gets set once in CI and never removed.
Colors klar produces — variants cells, find results, match output — are in gamut by construction and never trigger this.
2. Recorded figures shift by 0.1
okca is now scored at full precision instead of through an 8-bit hex round-trip. Figures move by exactly 0.1, never more, and toward accuracy — the number now scores the color you wrote down rather than its rounding.
How often depends on how your tokens were authored, and the range is wide: random OKLCH coordinates moved 6.5%, but a real design system of hand-authored round values (oklch(0.27 0.06 245.34)) moved 47% of 216 pairings. Deliberately-chosen OKLCH values essentially never land on the 8-bit grid, so assume most of your recorded figures shift.
On that same system, the shift produced zero pass/fail verdict changes against recorded floors. Re-baseline; don't treat it as a regression.
3. find may return a color with reduced chroma
An out-of-gamut reference is now normalized into the gamut before searching — mandatory, since otherwise there is no renderable color to return. Read axesAdjusted and gamut to see what moved. Targets that previously failed may now succeed.
It will not trade brand saturation for contrast on its own. When lightness alone falls short it exits 1 and reports resolvableBy — the chroma, lightness, and ΔE cost of a fix — without applying it. --allow-desaturation records that a human made that call.
Fixed
Issue #9 reported one defect. It turned out to be three instances of one pattern — a plausible value, no signal, error running in the permissive direction:
contrastmeasured a color nobody chose. It routed through hex serialization, which silently applies CSS Color 4 mapping, sooklch(0.79 0.22 25)was scored as#ff938b— neither the authored color nor anything deliberate. The ANSI swatch on the same line rendered#ff746f. Two unrelated gamut conversions that disagreed.findreturned the reference unchanged and unrenderable when its chroma was out of gamut at every lightness. The fallback written to prevent exactly that stayed null.lightnessinvented a range.oklch(0.5 0.9 25)renders at no lightness, but it reported{lightMin: 0.5, lightMax: 0.5}at exit 0.
Also: find could report a target unreachable that the unmodified reference already met, and achromatic colors would have returned null on the new code path had it not been caught.
New
Three flags, and machine-readable outcomes.
--gamut-map <css|clip|none> |
Out-of-gamut resolution on contrast. Defaults to CSS Color 4 mapping |
--allow-out-of-gamut |
Waive the exit 1 |
--allow-desaturation |
Permit find to reduce chroma |
KLAR_GAMUT_MAP |
Sets the mapping process-wide |
find gains reason (ok, lightness-exhausted, unreachable, chroma-exhausted), resolvableBy, axesAdjusted and deltaE. Branch on reason, never on message — the prose wording is not stable.
klar takes no position on what any particular browser does. Engines vary and change; the spec is a stable reference and an implementation is not.
Full migration notes: CHANGELOG.md — now shipped in the npm package too.
Deferred and tracked: #10 (wide gamut), #11 (plugin gamut declaration), #12 (bulk audit), #14 (dogfood check on the find default), #15 (build-before-test).
What's Changed
- 3.0: gamut-aware measurement, and two more instances of the same defect by @pawn002 in #13
- build: ship CHANGELOG.md in the published package by @pawn002 in #16
Full Changelog: v2.0.0...v3.0.0