fix(thema): tekstknoppen en links waren onleesbaar in donkere modus (#744) #746

Merged
brenno merged 1 commit from fix/tekstknoppen-donker-744 into main 2026-07-23 13:47:41 +00:00
Owner

Route A uit #744. Het issue blijft open voor route B — zie onderaan.

Gevonden bij het naloopen van het toestemmingsscherm in donkere modus, na #735.

De fout

Material geeft een TextButton standaard colorScheme.primary als voorgrond. In
het profiel Donker is primary de merkkleur #111827, en op het eigen
oppervlak #1E293B is dat 1,21 : 1. Ter vergelijking: in het lichte profiel
staat dezelfde rol op 14,13 : 1. WCAG AA vraagt 4,5.

Dat trof élke tekstknop en élke link in de app. Het pijnlijkst op het
toestemmingsscherm: de twee routes naar de licentietekst ("Lees de volledige
licentie", "Volledige licentie online") waren donkere vlekken. Dat is het eerste
scherm dat iemand moet passeren, en het legt een licentie voor waarvan de weg
naar de tekst onzichtbaar is.

Het was half bekend. app_theme.dart had de constatering al staan:

outlinedButtonTheme: OutlinedButtonThemeData(
  // In donkere modus is [primary] donker (onleesbaar op donker); gebruik
  // dan de lichte tekstkleur.
  style: OutlinedButton.styleFrom(
    foregroundColor: profile.isDark ? text : primary,
  ),
),

Maar er was geen textButtonTheme. Dáárom was in "Over OciDeck" de omlijnde knop
"Alle licentieteksten tonen" leesbaar en de link "Website van de stichting"
ernaast niet — twee regels uit elkaar, hetzelfde paneel.

Wat er is gebeurd

Dezelfde regel op textButtonTheme. Geen nieuwe afweging; een bestaande
beslissing die één knopsoort oversloeg.

AppPalette.accentInk voor de zes plekken die colorScheme.primary
rechtstreeks als inkt namen: de links, opsommingstekens en citaatbalk van de
documentatielezer, de sectie-iconen van het toestemmingsscherm, en het wolkje bij
een op afstand opgehaald bestand.

secondary was de voor de hand liggende keuze en is fout: in het profiel
Europa is het accent EU-geel, en dat is op het witte oppervlak van dat profiel
net zo onleesbaar als het probleem dat hier wordt opgelost. accentInk volgt
daarom dezelfde modusregel als de knoppen.

AppPalette.of(theme) vervangt het harde extension<AppPalette>()! op de
nieuwe aanroepplekken. Dat was geen cosmetica: met de ! viel de eerste
make check om met 86 gevallen, omdat een kale MaterialApp in een test geen
extensie draagt en een ontbrekende accentkleur zo een crash werd in plaats van
een kleur. De afgeleide terugval houdt de regel aan (lichte inkt op een donker
thema), niet de waarden.

De regressietest

test/app_theme_contrast_test.dart (#606) mat tot nu toe kleuren die iemand
koos. Deze fout zat in de kleur die ThemeData uitdeelt als niemand kiest —
daar keek geen enkele toets. Twee toetsen erbij:

  1. Voor elk ingebouwd AppAppearanceProfile: de opgeloste voorgrond van
    textButtonTheme en outlinedButtonTheme, plus accentInk, moet 4,5:1 halen
    op het oppervlak van dát profiel. De toets eist ook dat de voorgrond
    bestaat — een ontbrekende knopstijl is precies wat hier gebeurd was.
  2. Een bronwacht: geen colorScheme.primary als color: van tekst of icoon.

Drie mutaties, alle drie rood gezien:

Mutatie Wat rood werd
textButtonTheme weghalen "TextButton heeft geen expliciete voorgrond" — op alle drie de profielen
textButtonTheme op primary laten staan {TextButton: 1.2126638…}, alléén op Donker
één aanroepplek terug naar colorScheme.primary de bronwacht, met bestand en regelnummer

Die tweede is de gemeten waarde uit het issue, uit de test zelf.

Met eigen ogen

flutter run -d macos in donkere modus: toestemmingsscherm (beide licentielinks
plus de sectie-iconen), openscherm ("Gebruikershandleiding", "Instellingen"), en
de documentatielezer (links, opsommingstekens, citaatbalk). Alles leest.

Om bij het toestemmingsscherm te komen heb ik lokaal flutter.app_consent_accepted
tijdelijk op false gezet en daarna teruggezet op true — de waarde staat weer
zoals hij stond.

Poorten

make check exit 0 (5.997 tests, dekking 86,6%, per-bestand-vloer 0),
make check-secrets 0, make sast 0 bevindingen over 687 bestanden. DAST niet
gedraaid — advisory, en deze wijziging raakt geen geserveerd oppervlak. Geen
nieuwe l10n.d('…'), geen afhankelijkheid, geen SBOM-gevolg.

Documentatie: CHANGELOG en docs/ACCESSIBILITY.md — dat laatste beweerde "zeven
dingen" en de klasse die hier misging stond er niet in.

Wat er níét in zit (#744 blijft open)

Route B: primary in het profiel Donker is nog steeds #111827, en dat is óók
de vulling van een selectievakje, een schakelaar, de focusring en de
tekstselectie. Die zijn nu bijna zwart. Vermoedelijk ook fout, maar dat verandert
hoe donkere modus eruitziet en verdient een eigen ronde met die gebruiken erbij —
niet meeliften op een contrastreparatie.

Route A uit #744. Het issue blijft **open** voor route B — zie onderaan. Gevonden bij het naloopen van het toestemmingsscherm in donkere modus, na #735. ## De fout Material geeft een `TextButton` standaard `colorScheme.primary` als voorgrond. In het profiel *Donker* is `primary` de merkkleur `#111827`, en op het eigen oppervlak `#1E293B` is dat **1,21 : 1**. Ter vergelijking: in het lichte profiel staat dezelfde rol op 14,13 : 1. WCAG AA vraagt 4,5. Dat trof élke tekstknop en élke link in de app. Het pijnlijkst op het toestemmingsscherm: de twee routes naar de licentietekst ("Lees de volledige licentie", "Volledige licentie online") waren donkere vlekken. Dat is het eerste scherm dat iemand moet passeren, en het legt een licentie voor waarvan de weg naar de tekst onzichtbaar is. Het was half bekend. `app_theme.dart` had de constatering al staan: ```dart outlinedButtonTheme: OutlinedButtonThemeData( // In donkere modus is [primary] donker (onleesbaar op donker); gebruik // dan de lichte tekstkleur. style: OutlinedButton.styleFrom( foregroundColor: profile.isDark ? text : primary, ), ), ``` Maar er was geen `textButtonTheme`. Dáárom was in "Over OciDeck" de omlijnde knop "Alle licentieteksten tonen" leesbaar en de link "Website van de stichting" ernaast niet — twee regels uit elkaar, hetzelfde paneel. ## Wat er is gebeurd **Dezelfde regel op `textButtonTheme`.** Geen nieuwe afweging; een bestaande beslissing die één knopsoort oversloeg. **`AppPalette.accentInk`** voor de zes plekken die `colorScheme.primary` rechtstreeks als inkt namen: de links, opsommingstekens en citaatbalk van de documentatielezer, de sectie-iconen van het toestemmingsscherm, en het wolkje bij een op afstand opgehaald bestand. `secondary` was de voor de hand liggende keuze en is **fout**: in het profiel *Europa* is het accent EU-geel, en dat is op het witte oppervlak van dat profiel net zo onleesbaar als het probleem dat hier wordt opgelost. `accentInk` volgt daarom dezelfde modusregel als de knoppen. **`AppPalette.of(theme)`** vervangt het harde `extension<AppPalette>()!` op de nieuwe aanroepplekken. Dat was geen cosmetica: met de `!` viel de eerste `make check` om met 86 gevallen, omdat een kale `MaterialApp` in een test geen extensie draagt en een ontbrekende accentkleur zo een crash werd in plaats van een kleur. De afgeleide terugval houdt de *regel* aan (lichte inkt op een donker thema), niet de waarden. ## De regressietest `test/app_theme_contrast_test.dart` (#606) mat tot nu toe kleuren die iemand *koos*. Deze fout zat in de kleur die `ThemeData` uitdeelt als niemand kiest — daar keek geen enkele toets. Twee toetsen erbij: 1. Voor **elk** ingebouwd `AppAppearanceProfile`: de opgeloste voorgrond van `textButtonTheme` en `outlinedButtonTheme`, plus `accentInk`, moet 4,5:1 halen op het oppervlak van dát profiel. De toets eist ook dat de voorgrond *bestaat* — een ontbrekende knopstijl is precies wat hier gebeurd was. 2. Een bronwacht: geen `colorScheme.primary` als `color:` van tekst of icoon. Drie mutaties, alle drie rood gezien: | Mutatie | Wat rood werd | |---|---| | `textButtonTheme` weghalen | "TextButton heeft geen expliciete voorgrond" — op alle drie de profielen | | `textButtonTheme` op `primary` laten staan | `{TextButton: 1.2126638…}`, alléén op *Donker* | | één aanroepplek terug naar `colorScheme.primary` | de bronwacht, met bestand en regelnummer | Die tweede is de gemeten waarde uit het issue, uit de test zelf. ## Met eigen ogen `flutter run -d macos` in donkere modus: toestemmingsscherm (beide licentielinks plus de sectie-iconen), openscherm ("Gebruikershandleiding", "Instellingen"), en de documentatielezer (links, opsommingstekens, citaatbalk). Alles leest. Om bij het toestemmingsscherm te komen heb ik lokaal `flutter.app_consent_accepted` tijdelijk op `false` gezet en daarna teruggezet op `true` — de waarde staat weer zoals hij stond. ## Poorten `make check` exit 0 (5.997 tests, dekking 86,6%, per-bestand-vloer 0), `make check-secrets` 0, `make sast` 0 bevindingen over 687 bestanden. DAST niet gedraaid — advisory, en deze wijziging raakt geen geserveerd oppervlak. Geen nieuwe `l10n.d('…')`, geen afhankelijkheid, geen SBOM-gevolg. Documentatie: CHANGELOG en `docs/ACCESSIBILITY.md` — dat laatste beweerde "zeven dingen" en de klasse die hier misging stond er niet in. ## Wat er níét in zit (#744 blijft open) Route B: `primary` in het profiel *Donker* is nog steeds `#111827`, en dat is óók de vulling van een selectievakje, een schakelaar, de focusring en de tekstselectie. Die zijn nu bijna zwart. Vermoedelijk ook fout, maar dat verandert hoe donkere modus eruitziet en verdient een eigen ronde met die gebruiken erbij — niet meeliften op een contrastreparatie.
fix(thema): tekstknoppen en links waren onleesbaar in donkere modus (#744)
Some checks failed
CI / Docs links (push) Failing after 22s
CI / Gate (Linux) · Format · Analyze · Coverage (push) Failing after 28s
CI / Web hardening (pull_request) Failing after 29s
CI / Gate (Linux) · Format · Analyze · Coverage (pull_request) Failing after 31s
CI / Web hardening (push) Failing after 31s
CI / Docs links (pull_request) Failing after 31s
CI / Supply-chain (Trivy · advisory) (pull_request) Failing after 33s
CI / Supply-chain (Trivy · advisory) (push) Failing after 19s
CI / Test (macos-latest) (pull_request) Has been cancelled
CI / Test (windows-latest) (pull_request) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
c3f681e644
Material geeft een TextButton standaard colorScheme.primary als
voorgrond. In het profiel Donker is primary de merkkleur #111827, en op
het eigen oppervlak #1E293B is dat 1,21:1 — weg. Het trof elke tekstknop
en elke link, waaronder de twee routes naar de licentietekst op het
toestemmingsscherm.

Half bekend was het al: app_theme.dart had de constatering staan, maar
alleen op outlinedButtonTheme. Daarom was in 'Over OciDeck' de omlijnde
knop leesbaar en de link ernaast niet. Nu dezelfde regel op
textButtonTheme.

AppPalette.accentInk voor de zes plekken die colorScheme.primary
rechtstreeks als inkt namen. Niet secondary: in het profiel Europa is het
accent EU-geel, op wit net zo onleesbaar als het probleem zelf.

AppPalette.of() vervangt het harde  op de
nieuwe aanroepplekken: een kale MaterialApp in een test heeft die
extensie niet, en dan werd een ontbrekende accentkleur een crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brenno merged commit 927c76a2da into main 2026-07-23 13:47:41 +00:00
Sign in to join this conversation.
No description provided.