diff --git a/docs/technical-debt.md b/docs/technical-debt.md index fb972b7..7a00b0b 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -297,6 +297,152 @@ millisecond for the `point` and `all` chains, and the codegen tests still pass b --- +## TD-6 — The quietest ink does not reach WCAG AA, and the rule does not reach 3:1 + +**Breaks:** [requirements.md](requirements.md) NFR-A11Y-2 — "non-canvas UI meets WCAG AA contrast". + +### What it does + +`style.yaml` sets three inks and four surfaces. Measured as WCAG 2 contrast ratios (sRGB relative +luminance, the standard formula), against the surfaces each ink is actually drawn on: + +| ink | on `ground` | on `surface` | on `surface-raised` | on `hover` | on `selected` | +|---|---|---|---|---|---| +| `ink` #EDEEF0 | 16.02 | 14.69 | 13.03 | 11.39 | 9.68 | +| `ink-dim` #9EA1A6 | 7.18 | 6.58 | 5.84 | 5.10 | **4.34** | +| `ink-faint` #71747A | **3.97** | **3.64** | **3.23** | **2.82** | **2.40** | +| `warn-ink` #C9A05A | 7.67 | 7.03 | 6.24 | 5.45 | 4.64 | +| `rule` #323438 | **1.49** | **1.37** | **1.21** | **1.06** | **1.11** | + +Every text size in the application is 11px, 13px, 17px or 24px, and WCAG's "large text" relief +begins at 18.66px bold or 24px regular — so all four of those thresholds are the normal-text one, +**4.5:1**, except the masthead. Bold entries fail it. + +The inverted cases pass and are worth stating so nobody re-measures them: `ground` on `active` +(#FFFFFF) is 18.60, on `active-dim` 11.20, on `active-pressed` 5.95, on `selected-ring` 13.02. The +near-white fills that `Button.primary`, `FilterChip.active` and the held tool-rail entry use are +the *best*-contrasting text in the interface, not the worst. + +So the failures are exactly two, and neither is where one would guess: + +- **`ink-faint` reaches 4.5:1 nowhere at all.** It is the ink for `Caption`, `PanelHeading`, + `Disclosure`, `Value`'s placeholder state and `FilterChip`'s count — every hint, every section + name, every "3 photographs" under a title. +- **`rule` reaches 3:1 nowhere.** WCAG 1.4.11 asks 3:1 of the boundary of a control the user must + perceive, and `rule` is the border of every `Button`, `Field`, `Panel`, `ChoiceChip` and + `IconButton`. An unfilled secondary button is a 1.4:1 outline on a 1.2:1 background. + +`ink-dim` on `selected` at 4.34 is a third case, marginal enough that a two-point lift fixes it. + +### Why + +Not an oversight — the direct consequence of the palette's own argument, which `style.yaml`'s +preamble makes at length and correctly. The chrome is deliberately quiet because a bright surround +biases how a photograph is judged, and hue is banned outright because an accent beside the image +shifts the perception of nearby colours. What is left to signal with is luminance, and the palette +spends its luminance range on the *photograph*, keeping the chrome inside a narrow band above the +ground. + +A narrow band is precisely what a contrast ratio measures. `ink-faint` exists to be skipped by the +reader who did not stop to look; that is a real design intent, and "text you are meant to skip" +and "text everyone can read" are in genuine tension rather than one being a mistake. + +### What it costs + +The photographer who cannot read a caption cannot read *any* caption, on any screen — this is one +token, so it fails everywhere at once. The hints under the settings switches say what a setting +costs, the section names say what a panel is, and the counts say how big a filter is. None of it is +decorative. + +### Paying it off + +Two token changes, and the second is the awkward one. + +`ink-faint` needs roughly #8A8D93 to clear 4.5:1 against `surface-raised`, the darkest surface it +is drawn on that matters — which puts it about where `ink-dim` sits today and collapses the +three-ink scale to two. So the real fix is to re-derive all three inks against the surfaces rather +than to nudge one: the scale wants to start higher and keep its steps, not compress. + +`rule` needs about #4A4D52 for 3:1 against `surface`. That is a visibly stronger line, and the +preamble's "instrument rather than absence" reasoning applies to it as much as to the greys — this +is a look change, not a number change, and it should be looked at rather than computed. + +Both are decisions about how the application appears next to a photograph, which is the one thing +this palette was designed around. They want a screenshot and an opinion, not a patch. + +**Done when:** every `Theme` ink reaches 4.5:1 against every surface it is drawn on, `rule` reaches +3:1 against `surface` and `surface-raised`, and a test recomputes those ratios from `style.yaml` so +the next palette edit cannot quietly undo it. The table above is the baseline to compare against. + +### Not in scope + +The histogram's `plot-*` inks (2.36 for `plot-luma` on `ground`) are drawn *on* the canvas, and +NFR-A11Y-2 scopes contrast to non-canvas UI. NFR-A11Y-3 covers what those need instead, and is +already met — the readouts name the channel in words. + +--- + +## TD-7 — Platform font scaling is not honoured + +**Breaks:** [requirements.md](requirements.md) NFR-A11Y-2 — "platform font scaling is honoured +without clipping". + +### What it does + +Nothing at all, which is the entry. Every type size is a constant in `style.yaml` — 11, 13, 17, 24 +— read as `Theme.text-sm` and friends at 65 call sites, and there is no multiplier anywhere between +the platform's font-size preference and those numbers. `scale_factor()` is read in `display_ui.rs` +and in `lib.rs`, but only to size the canvas in physical pixels for the render; it is display DPI, +which Slint already applies to logical lengths, and it is not the user's text-size setting. A +photographer who sets 130% text on GNOME or Android gets an application that ignores it. + +### Why + +Because honouring it is not a multiplier, and pretending it is would be worse than not doing it. + +The layout is built on constants that are not derived from the type size: `control-height` 28, +`touch-target` 44, `row-height` 26, `rail-entry-height` 54, `panel-width` 360, and a dozen fixed +heights written at their call sites — `ParamSlider`'s 46px, the folder picker's 220px box, the +readout column's 30px. Scaling the type alone clips against every one of them, silently, because +Slint elides rather than errors. `SwatchSlider` is the sharpest case: 12 hue bands × 3 channels in +a 360px column, sized so that a track and a swatch and a three-character readout fit on one line. + +And the failure is invisible to the person shipping it. The requirement's own phrase is "without +clipping", and clipping is exactly what a screenshot at 100% cannot show — the memory note on +verifying Slint changes exists because these files have a history of compiling, rendering and being +wrong. + +### What it costs + +The user for whom this matters most is not the screen-reader user the rest of this branch serves — +it is the one with usable but poor sight, who reads the interface and needs it larger. The +application is unusable to them at any setting, and there is no partial credit: text scaling is a +system-wide preference, so an app that ignores it is the one thing on the desktop that did. + +### Paying it off + +In the order the pieces depend on each other: + +1. **A scale token.** `Theme.text-sm` and the rest become `base × Theme.type-scale`, with the + scale an `in-out` property Rust writes at startup from the platform. `build.rs` already emits + `in-out` tokens under `live-style`, so the codegen half of this exists and is proven — that + feature is the mechanism, one line from being general. +2. **A source for the number.** GNOME publishes `text-scaling-factor` over the settings portal; + Android has `Configuration.fontScale` through JNI, beside the calls `lib.rs` already makes for + `ACTION_VIEW`. Both want a default of 1.0 and a sane clamp — 0.8 to 2.0 — because a user who has + set 300% for a phone launcher has not asked for a 72px slider readout. +3. **The constants that are not type.** Every fixed height a *label* sits inside has to follow the + scale; every touch target must not shrink and need not grow. That is the work, and it is where + the 46px and 220px literals get read one at a time. +4. **Evidence.** Screenshots at 1.0, 1.3 and 2.0 of the develop column, the settings page and the + colour mixer — the three densest layouts — because "without clipping" is a claim about the + worst case and nothing else will show it. + +**Done when:** the develop column, the settings page and the colour mixer render at a 2.0 scale +with no elided label and no touch target under 44 logical pixels. + +--- + ## Related, and deliberately not here The window-move rule, the grid's ordering index and the whole-library readout cache were *fixed*