[Bug] The dark theme fails the contrast bar the app applies to the user own slides #606

Closed
opened 2026-07-22 16:19:51 +00:00 by brenno · 4 comments
Owner

Found in the pre-publication accessibility review.

Evidence: lib/theme/app_theme.dart makes colours mode-dependent via _m(light, dark) — but 25 tokens are const and therefore do not flip. Measured against the dark background paper = #181B21 (line 57), 21 of the 25 fail WCAG AA 4.5:1:

  • severityCritical / danger700 / checklistAnomaly / scopeUnreachable #B91C1C2.67:1
  • severityNone #64748B → 2.28:1
  • success800 → 2.42:1
  • accent #2563EB → 3.34:1
  • success700 / severityLow / checklistTested #15803D → 3.44:1

They are used as text colours, not fills: lib/widgets/shell/status_bar.dart:196 (the seal-intact indicator), lib/widgets/editors/asset_overview_editor.dart:225, lib/l10n/slide_quality_localization.dart:272. In presentation mode there are a further 24 uses of Colors.white24/38/54/70 as a text colour in lib/widgets/presentation/; Colors.white24 on black is roughly 2.0:1 at fontSize: 11-12 (presenter_overlays.dart:383).

No test checks the app own UI for contrast — theme_profile_contrast_warning_test.dart and title_contrast_test.dart are about the user deck.

Why this matters now: OciDeck checks the contrast of your slides against WCAG AA and reports what fails — a feature most presentation tools lack, and one the README names as a differentiator. That the app itself misses that bar on 21 of 25 colours in dark mode is the easiest "physician, heal thyself" a reviewer can find, and the arithmetic takes them five minutes.

Proposal: first, a test that applies the contrast calculation the app already owns to a list of (colour, background) pairs from AppTheme in both modes, with the current outcome as an explicit ratchet baseline — then the number lives in the repository rather than in a critic notebook. Then give the four worst tokens (#B91C1C, #15803D, #64748B, #2563EB) a _m() counterpart, as the slate palette already has. Put the real number in docs/ACCESSIBILITY.md afterwards.

Found in the pre-publication accessibility review. **Evidence:** `lib/theme/app_theme.dart` makes colours mode-dependent via `_m(light, dark)` — but 25 tokens are `const` and therefore do not flip. Measured against the dark background `paper = #181B21` (line 57), **21 of the 25 fail WCAG AA 4.5:1**: - `severityCritical` / `danger700` / `checklistAnomaly` / `scopeUnreachable` `#B91C1C` → **2.67:1** - `severityNone` `#64748B` → 2.28:1 - `success800` → 2.42:1 - `accent` `#2563EB` → 3.34:1 - `success700` / `severityLow` / `checklistTested` `#15803D` → 3.44:1 They are used as *text* colours, not fills: `lib/widgets/shell/status_bar.dart:196` (the seal-intact indicator), `lib/widgets/editors/asset_overview_editor.dart:225`, `lib/l10n/slide_quality_localization.dart:272`. In presentation mode there are a further 24 uses of `Colors.white24/38/54/70` as a text colour in `lib/widgets/presentation/`; `Colors.white24` on black is roughly **2.0:1** at `fontSize: 11-12` (`presenter_overlays.dart:383`). No test checks the app own UI for contrast — `theme_profile_contrast_warning_test.dart` and `title_contrast_test.dart` are about the *user* deck. **Why this matters now:** OciDeck checks the contrast of your slides against WCAG AA and reports what fails — a feature most presentation tools lack, and one the README names as a differentiator. That the app itself misses that bar on 21 of 25 colours in dark mode is the easiest "physician, heal thyself" a reviewer can find, and the arithmetic takes them five minutes. **Proposal:** first, a test that applies the contrast calculation the app already owns to a list of (colour, background) pairs from `AppTheme` in both modes, with the current outcome as an explicit ratchet baseline — then the number lives in the repository rather than in a critic notebook. Then give the four worst tokens (`#B91C1C`, `#15803D`, `#64748B`, `#2563EB`) a `_m()` counterpart, as the slate palette already has. Put the real number in `docs/ACCESSIBILITY.md` afterwards.
Author
Owner

Opgepakt. Tak: fix/donker-thema-contrast-606. Reikwijdte: een ratchet-test test/app_theme_contrast_test.dart, de drie call sites die je noemt, en docs/ACCESSIBILITY.md. De volledige audit van alle ~200 tokengebruiken past hier niet in; ik laat het issue open met wat er dan nog ligt.

Opgepakt. Tak: `fix/donker-thema-contrast-606`. Reikwijdte: een ratchet-test `test/app_theme_contrast_test.dart`, de drie call sites die je noemt, en `docs/ACCESSIBILITY.md`. De volledige audit van alle ~200 tokengebruiken past hier niet in; ik laat het issue open met wat er dan nog ligt.
Author
Owner

Gedeeltelijk opgelost in f336166a (PR #682), op main. Het issue blijft open voor de rest; hieronder staat wat dat is.

Wat er is. test/app_theme_contrast_test.dart meet elk kleurtoken dat de interface als tékst gebruikt tegen het oppervlak dat de modus werkelijk schildert, in beide modi, met de uitkomst als ratchet — stap 1 uit je voorstel. Hij faalt twee kanten op: een nieuw geval erbij, én een geval dat gerepareerd is maar in de lijst blijft staan. Plus de drie call sites die je bij naam noemt, nu op dangerFg/successFg.

De meting corrigeert je issue op twee punten. Donker telt 17 tokens onder 4,5:1, niet 21 — het verschil zit in vul- en randkleuren die de tekstlat niet hoeven te halen. En licht is niet in orde: severityHigh (#EA580C) haalt 3,6:1 op wit, severityMedium (#D97706) 3,3:1. Je mat alleen tegen de donkere achtergrond en noemde licht impliciet goed.

Waarom de rest niet in één keer kan. Je voorstel was de vier ergste tokens een _m()-tegenhanger geven. Dat kan niet zomaar: die tokens zijn bewust vast. Een bevinding moet in een headless export-isolate identiek renderen aan de preview (PENTEST_MIAUW §11), en een kleur die met de app-modus meebeweegt breekt dat — dan gaat het zegel over twee verschillende PDF's.

Wat er dus nog ligt: elk van de ~200 gebruiken van die tokens lezen als óf dia-inhoud (laten staan) óf interface-chrome (mode-afhankelijk maken). Dat is een audit, geen omzetting, en daar blijft dit issue voor open. docs/ACCESSIBILITY.md draagt de getallen en die tweedeling nu, zodat de stand in de repo staat.

Gedeeltelijk opgelost in `f336166a` (PR #682), op main. **Het issue blijft open** voor de rest; hieronder staat wat dat is. **Wat er is.** `test/app_theme_contrast_test.dart` meet elk kleurtoken dat de interface als tékst gebruikt tegen het oppervlak dat de modus werkelijk schildert, in beide modi, met de uitkomst als ratchet — stap 1 uit je voorstel. Hij faalt twee kanten op: een nieuw geval erbij, én een geval dat gerepareerd is maar in de lijst blijft staan. Plus de drie call sites die je bij naam noemt, nu op `dangerFg`/`successFg`. **De meting corrigeert je issue op twee punten.** Donker telt **17** tokens onder 4,5:1, niet 21 — het verschil zit in vul- en randkleuren die de tekstlat niet hoeven te halen. En **licht is niet in orde**: `severityHigh` (#EA580C) haalt 3,6:1 op wit, `severityMedium` (#D97706) 3,3:1. Je mat alleen tegen de donkere achtergrond en noemde licht impliciet goed. **Waarom de rest niet in één keer kan.** Je voorstel was de vier ergste tokens een `_m()`-tegenhanger geven. Dat kan niet zomaar: die tokens zijn *bewust* vast. Een bevinding moet in een headless export-isolate identiek renderen aan de preview (PENTEST_MIAUW §11), en een kleur die met de app-modus meebeweegt breekt dat — dan gaat het zegel over twee verschillende PDF's. **Wat er dus nog ligt:** elk van de ~200 gebruiken van die tokens lezen als óf dia-inhoud (laten staan) óf interface-chrome (mode-afhankelijk maken). Dat is een audit, geen omzetting, en daar blijft dit issue voor open. `docs/ACCESSIBILITY.md` draagt de getallen en die tweedeling nu, zodat de stand in de repo staat.
Author
Owner

De merkkleuren-audit staat op main: 7e73c9a9 (PR #712).

accent, navy en teal werden op zeventig plekken als tekst-/icoonkleur gebruikt en haalden in de donkere modus de lat niet. Ze hebben nu mode-afhankelijke tegenhangers (accentFg/brandFg/tealFg); de merkkleuren zélf blijven const. De scheidslijn is chroom-tegen-inhoud: tekst die je ín de app leest volgt de app, inkt die óp een dia landt niet — want een dia moet in de preview identiek zijn aan een headless export. De goldens bevestigen dat de dia's byte-identiek blijven, en de beeldkeuring keurde beide modi met eigen ogen goed.

Donker zakt daarmee van 17 naar 14 tokens onder de lat. De ratchet kreeg een tweede toets: de nieuwe tokens moeten de lat wél halen, anders is de verhuizing een stille no-op.

Wat overblijft, en dit is de kern van je oorspronkelijke voorstel dat níét zomaar kan: de ernst-, checklist- en scopepaletten (de 14 resterende donkere tokens). Díé renderen werkelijk in dia's, dus een _m()-tegenhanger geven zou het zegel over twee verschillende PDF's laten gaan. Ze vragen dezelfde lezing per gebruik als de merkkleuren nu kregen — dia-inhoud laten staan, chroom mode-afhankelijk maken. ACCESSIBILITY.md draagt die stand en de tweedeling.

Ik laat het issue daarvoor open.

De merkkleuren-audit staat op main: `7e73c9a9` (PR #712). `accent`, `navy` en `teal` werden op zeventig plekken als tekst-/icoonkleur gebruikt en haalden in de donkere modus de lat niet. Ze hebben nu mode-afhankelijke tegenhangers (`accentFg`/`brandFg`/`tealFg`); de merkkleuren zélf blijven const. De scheidslijn is chroom-tegen-inhoud: tekst die je ín de app leest volgt de app, inkt die óp een dia landt niet — want een dia moet in de preview identiek zijn aan een headless export. De goldens bevestigen dat de dia's byte-identiek blijven, en de beeldkeuring keurde beide modi met eigen ogen goed. Donker zakt daarmee van 17 naar 14 tokens onder de lat. De ratchet kreeg een tweede toets: de nieuwe tokens moeten de lat wél halen, anders is de verhuizing een stille no-op. **Wat overblijft, en dit is de kern van je oorspronkelijke voorstel dat níét zomaar kan:** de ernst-, checklist- en scopepaletten (de 14 resterende donkere tokens). Díé renderen werkelijk in dia's, dus een `_m()`-tegenhanger geven zou het zegel over twee verschillende PDF's laten gaan. Ze vragen dezelfde lezing per gebruik als de merkkleuren nu kregen — dia-inhoud laten staan, chroom mode-afhankelijk maken. `ACCESSIBILITY.md` draagt die stand en de tweedeling. Ik laat het issue daarvoor open.
Author
Owner

Volledig opgelost in PR #715, op main — de audit met beeldcontrole in beide thema's.

De regel die eruit kwam is niet licht-tegen-donker maar chrome tegen inhoud. Tekst die je ín de app leest volgt de app; inkt die óp een dia landt niet, want een dia moet identiek renderen in de preview en in een headless export-isolate.

37 chrome-plekken naar de mode-afhankelijke varianten. 43 plekken in previews/ naar nieuwe vaste dia-inkt — en dát was de grootste vondst, uit de beeldkeuring: de previews schilderden hun grijzen met de mode-afhankelijke slate-schaal op een canvas dat wit blijft. In donkere modus werd slate700 1,3:1; de tekst van een checklist, scope-matrix en bevindingenoverzicht verdween zo goed als helemaal. En het week af van de export, die zonder thema draait en altijd de lichte waarden schrijft — terwijl het exportdialoog belooft dat de export exact de weergave uit de editor gebruikt. Alle 33 goldens bleven byte-identiek.

En de meting klopte niet, en dat was van mij. De eerste ronde legde elk vast token naast het interface-oppervlak en noteerde zeventien tekortkomingen. Maar een ernstkleur wordt op een witte dia gelezen. Tegen de juiste achtergrond en de juiste lat haalt alles het: veertien tokens zijn diatekst (4,5:1 op wit), twee zijn nooit tekst maar een tint, een randstreep en een badge-vulling met groot vet label (3:1). Er staat geen basislijn meer open — grotendeels een categoriefout, de rest gerepareerd.

Drie kleinere uit de keuring, twee door mij veroorzaakt: het exportdialoog was half omgezet (faaltak op 3,1:1 in donker terwijl de succes-tak schreeuwde), de gebruikersnotities-kop was zwakker geworden dan zijn eigen ondertitel, en dangerFg was in licht zo bleek dat hij op de scorecard-chip naar 4,2:1 zakte — onder AA, voor de kleur die alarm betekent.

Twee bronwachten zetten dit vast: geen vaste merk-/ernstkleur als tekst buiten de dia-renderende code, en geen mode-afhankelijk grijs binnen previews/. Beide lezen de bron en zijn eerst rood gemaakt met een geplante overtreding.

Los hiervan gemeld: een gebroken PDF-export (#714).

Volledig opgelost in PR #715, op main — de audit met beeldcontrole in beide thema's. **De regel die eruit kwam is niet licht-tegen-donker maar chrome tegen inhoud.** Tekst die je ín de app leest volgt de app; inkt die óp een dia landt niet, want een dia moet identiek renderen in de preview en in een headless export-isolate. **37 chrome-plekken** naar de mode-afhankelijke varianten. **43 plekken in `previews/`** naar nieuwe vaste dia-inkt — en dát was de grootste vondst, uit de beeldkeuring: de previews schilderden hun grijzen met de *mode-afhankelijke* slate-schaal op een canvas dat wit blijft. In donkere modus werd `slate700` **1,3:1**; de tekst van een checklist, scope-matrix en bevindingenoverzicht verdween zo goed als helemaal. En het week af van de export, die zonder thema draait en altijd de lichte waarden schrijft — terwijl het exportdialoog belooft dat de export exact de weergave uit de editor gebruikt. Alle 33 goldens bleven byte-identiek. **En de meting klopte niet, en dat was van mij.** De eerste ronde legde elk vast token naast het *interface*-oppervlak en noteerde zeventien tekortkomingen. Maar een ernstkleur wordt op een **witte dia** gelezen. Tegen de juiste achtergrond en de juiste lat haalt alles het: veertien tokens zijn diatekst (4,5:1 op wit), twee zijn nooit tekst maar een tint, een randstreep en een badge-vulling met groot vet label (3:1). Er staat geen basislijn meer open — grotendeels een categoriefout, de rest gerepareerd. **Drie kleinere uit de keuring, twee door mij veroorzaakt:** het exportdialoog was half omgezet (faaltak op 3,1:1 in donker terwijl de succes-tak schreeuwde), de gebruikersnotities-kop was zwakker geworden dan zijn eigen ondertitel, en `dangerFg` was in licht zo bleek dat hij op de scorecard-chip naar 4,2:1 zakte — onder AA, voor de kleur die alarm betekent. **Twee bronwachten zetten dit vast:** geen vaste merk-/ernstkleur als tekst buiten de dia-renderende code, en geen mode-afhankelijk grijs binnen `previews/`. Beide lezen de bron en zijn eerst rood gemaakt met een geplante overtreding. Los hiervan gemeld: een gebroken PDF-export (#714).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
LibreKAT/Ocideck#606
No description provided.