From 3805128015688f057eefb7cc82a2a0f0ac67bba2 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 15 Jun 2026 10:24:40 -0500 Subject: docs: move specs to docs/specs/ with lifecycle-status filenames Separate the 27 formal specs from working notes. Specs move to docs/specs/, notes stay in docs/design/. Each spec carries its lifecycle in the filename (-spec, -spec-doing, -spec-implemented, -spec-superseded) plus an authoritative ID and STATUS property drawer. The status came from checking each spec against the code, not the doc's own field: 6 implemented, 8 in progress, 12 not started, 1 superseded. Inbound links become org-id links so future status renames don't break them; code-comment paths repoint to docs/specs/. Working notes, inventories, reviews, and brainstorms stay in docs/design/. --- .../theme-studio-perceptual-color-metrics-spec.org | 576 --------------------- 1 file changed, 576 deletions(-) delete mode 100644 docs/design/theme-studio-perceptual-color-metrics-spec.org (limited to 'docs/design/theme-studio-perceptual-color-metrics-spec.org') diff --git a/docs/design/theme-studio-perceptual-color-metrics-spec.org b/docs/design/theme-studio-perceptual-color-metrics-spec.org deleted file mode 100644 index 7e7dedb2..00000000 --- a/docs/design/theme-studio-perceptual-color-metrics-spec.org +++ /dev/null @@ -1,576 +0,0 @@ -#+TITLE: theme-studio — perceptual color metrics (OKLCH, APCA, ΔE) -#+AUTHOR: Craig Jennings -#+DATE: 2026-06-08 - -* Status - -Spec / review incorporated (Codex, 2026-06-08, two passes). Adds perceptual color metrics to -the theme-studio (=scripts/theme-studio/=) so it can build deliberately -low-contrast themes (Solarized / Zenburn class) with the same rigor it already -brings to high-contrast WCAG checking. Four additions: an OKLCH color model, a -per-color perceptual-lightness readout, an APCA contrast score alongside the -existing WCAG ratio, and a pairwise ΔE distinguishability check across the -palette. - -Came out of a design conversation comparing the low-contrast school (Solarized, -Zenburn) against Prot's high-contrast Modus themes. The conclusion: a theme has -three independent dials — contrast ratio, overall luminance, and chroma — and -the low-contrast camp turns down the first while Modus leaves it high and turns -down the other two. The current tool only measures the first (WCAG contrast) and -edits color in HSV, whose "lightness" is not perceptually uniform. To build -low-contrast themes by metric rather than by eye, the tool needs -perceptually-uniform lightness and chroma controls plus distinguishability and -polarity-aware contrast measures. - -Rubric: *Ready.* The four v1 product questions are decided in "Agreed decisions -(v1)" below and confirmed by Craig (2026-06-08); the testing strategy was -revised on his direction to a layered pyramid (Node-unit-tested color core + -thin UI hash tests + measured coverage). No remaining blocking ambiguity — the -implementer no longer has to invent product behavior while coding. Implementation -is sequenced into five phases, each independently shippable and tested. Tasks -filed in =todo.org=. - -* Background — the current color model - -The tool today works entirely in sRGB hex, HSV, and WCAG luminance. The relevant -cluster in =generate.py=: - -- =rl(hex)= (line 517) — WCAG relative luminance, via the existing =lin()= - sRGB-linearization helper. -- =contrast(a,b)= (line 519) — the WCAG 2.x ratio =(L1+0.05)/(L2+0.05)=. -- =rating(r)= / =ratingColor(r)= (lines 520-521) — AA (≥4.5) / AAA (≥7) verdict - and its display color. -- =hsv2rgb=, =rgb2hsv=, =hex2rgb=, =rgb2hex=, =normHex= (lines 604-609). -- The picker holds state as =pkH, pkS, pkV= (HSV) and renders an SV box (=#sv=), - a hue strip (=#hue=), crosshairs (=#svcur=, =#huecur=), a hex+contrast readout - (=#pkhex= / =#pkcon=, line 451), the contrast-mask buttons (=.pmode=, state - =pkMode=, values =any= / =aa= / =aaa=), and palette chips (=#pkchips=). -- =drawMask()= (line 613) greys SV-box regions whose contrast against the - background falls below the selected mask threshold (=pkThresh()=). -- Per-face contrast readouts appear across *three* tables — syntax (line 548), - UI (line 1064), and package faces (line 752) — each via =contrast()= + - =rating()=. The package-face tier has grown large since the tool's early - versions (51 packages in the current inventory), so any "add a column to the - table readouts" change now touches that whole surface, not just the two - original tables. - -Two limitations this spec addresses: - -1. *HSV lightness lies.* Two colors at the same HSV =V= can differ markedly in - perceived brightness, so the SV box cannot hold perceived lightness constant - while hue changes — exactly the operation a calm, even palette needs. -2. *WCAG 2.x is a known-flawed contrast model in the low-contrast / dark band.* - Its ratio misjudges contrast most where this work operates, and it is not - polarity-aware: it scores light-on-dark and dark-on-light identically, which - perception does not. WCAG 3 is reworking contrast but is years out — still a - Working Draft in 2026, with the final Recommendation not expected until - roughly 2028–2030 — and its contrast algorithm is undetermined: APCA was moved - *out* of the WCAG 3 draft in 2023 for further evaluation. So APCA enters here - as a well-regarded independent perceptual model used as an additional - diagnostic, not as a coming standard. WCAG 2.x stays the baseline precisely - because nothing has replaced it yet. - -* Goal - -Add four metrics, each a discrete increment: - -1. *OKLCH color model* — perceptually-uniform Lightness / Chroma / Hue, so the - editor can move one axis without disturbing the others, plus a gamut clamp - for OKLCH values outside sRGB. -2. *Perceptual-lightness readout* — show each color's OKLCH L (and C, H) in the - picker, so "low, even lightness steps" becomes a number rather than a guess. -3. *APCA score* — the Accessible Perceptual Contrast Algorithm Lc value - displayed next to the WCAG ratio, as the more trustworthy contrast metric in - the low-contrast band. -4. *Pairwise ΔE check* — perceptual color-difference between every pair of - palette entries, flagging pairs too similar to tell apart, which is the - constraint that keeps a low-chroma / low-lightness-spread palette from - collapsing into mush. - -Non-goals: replacing WCAG (it stays as the compatibility baseline, shown -alongside APCA, which is an additional perceptual diagnostic, not a -replacement); replacing the HSV picker outright (OKLCH is added as a parallel -color model, HSV remains the default); CIEDE2000 in v1 (ΔE-OK is the v1 -difference metric — see vNext). - -* Agreed decisions (v1) - -Settled on author + reviewer alignment and confirmed by Craig (2026-06-08). - -1. *ΔE metric and threshold.* Use ΔE-OK (Euclidean distance in OKLab) on its - native scale (OKLab L is 0..1). Default "too similar" warning threshold is - *0.02* — the just-noticeable-difference floor, so the warning fires only when - two palette colors are genuinely hard to tell apart. The threshold is a named - constant, calibratable in one place. CIEDE2000 (the CIE's 2000 perceptual - color-difference standard — more accurate than plain Euclidean distance, but - ~40 lines of trigonometric lightness/chroma/hue corrections plus a blue-region - rotation term) is deferred to vNext: ΔE-OK is accurate enough to flag - indistinguishable pairs, which is all this check needs, and it is five lines. -2. *Low-contrast preset.* v1 ships *readouts only* (OKLCH, APCA, ΔE). No named - low-contrast preset / mask mode yet. No such preset exists anywhere today — it - would be a new feature: a saved low-contrast target (e.g. an APCA Lc band, or - a contrast ceiling as well as a floor) that masks the palette to a comfortable - range in one click, the way the current any/AA+/AAA buttons mask by a contrast - floor. It is deferred until the raw readouts are in use, because only then is - it clear which band is worth presetting. v1 gives the numbers; the preset - would automate a judgment the numbers first have to inform. -3. *APCA placement.* v1 shows APCA *only in the picker* readout, not in the - syntax/UI/package table contrast cells. Adding it to the tables is - low-complexity once =apca()= exists — the same pattern as the existing - =contrast()= + =rating()= cells, repeated across the three tables — so the - deferral is about table *density*, not difficulty: the package table alone is - 51 packages wide, and a second contrast number per row risks clutter before - it is clear anyone reads it there. Table-wide APCA is a vNext candidate if - picker-only proves too hidden. -4. *Picker default model.* HSV stays the *default* picker model; OKLCH is - opt-in via a color-model control. The reason: HSV is the familiar 2D SV-box - the picker already has, and OKLCH is slider-only until the C×L plane (Phase - 4b) lands — so defaulting to OKLCH before 4b would hand users a worse default - editing surface than they have now. Once 4b ships the C×L plane, making OKLCH - the default becomes a real option worth revisiting; until then, HSV default - keeps the current editing experience intact and makes OKLCH an additive - choice, not a regression. - -* Color-math foundation (Phase 1, prerequisite) - -The pure color math is *extracted into its own importable module* rather than -inlined as loose functions in the page. This is the core architectural change -this spec makes to the test surface: the math is logic, so it gets tested as -logic — directly, in Node, with exhaustive fixtures — and the picker becomes a -thin UI layer over a tested core, not the only way to exercise the math. - -- New file: =scripts/theme-studio/colormath.js= — the pure, side-effect-free - conversion + metric functions, written as an ES module (each =export=-ed), - with a small guard so the same source loads both ways: =import=-ed by the Node - tests and spliced into the page by the generator. -- =generate.py= inlines =colormath.js= into the page's =