[Bug] Tekstknoppen en links zijn onleesbaar in donkere modus (1,21:1) #744

Closed
opened 2026-07-23 13:03:45 +00:00 by brenno · 3 comments
Owner

Wat er gebeurt

In donkere modus zijn tekstknoppen, links en een aantal iconen praktisch
onzichtbaar: bijna zwart op een donkere ondergrond.

Het meest zichtbaar op het toestemmingsscherm — de eerste keer dat iemand de
app opent. De twee licentielinks ("Lees de volledige licentie", "Volledige
licentie online (23 officiële taalversies)") staan er als donkere vlekken in de
kaart. Wie de app voor het eerst start in donkere modus krijgt een licentie
voorgelegd waarvan hij de weg naar de tekst niet kan zien.

Reproduceren: Instellingen → App-thema → profiel Donker. Zichtbaar op het
openscherm ("Gebruikershandleiding", "Instellingen"), in "Over OciDeck"
("Website van de stichting"), in de documentatielezer, en op het
toestemmingsscherm.

Waarom

Het profiel Donker zet primaryColor: '#111827' — bijna zwart. Dat is prima
als merk-/paneelkleur, maar het gaat in AppTheme.fromProfile ongewijzigd door
naar ColorScheme.primary, en Material gebruikt primary als voorgrondkleur
van een TextButton.

Gemeten:

op surface
primary #111827 op #1E293B (donker) 1,21 : 1
primary #1C2B47 op #FFFFFF (licht) 14,13 : 1

WCAG AA vraagt 4,5:1 voor gewone tekst. 1,21:1 is geen "krap", dat is weg.

Het is bovendien al eens half opgelost. app_theme.dart heeft precies deze
constatering staan, maar alleen voor de omlijnde knop:

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

Er is géén textButtonTheme. Daarom is in "Over OciDeck" de knop "Alle
licentieteksten tonen" (omlijnd) wél leesbaar en de link "Website van de
stichting" (tekstknop) ernaast niet — twee regels uit elkaar, hetzelfde paneel.

Daarnaast wordt colorScheme.primary op zes plekken rechtstreeks als voorgrond
gebruikt en heeft daar hetzelfde probleem:

  • lib/widgets/privacy_statement_content.dart:265 — de sectie-iconen op het
    toestemmingsscherm
  • lib/widgets/reader/document_markdown_view.dart:473,477,482 — opsommingsteken,
    link en citaatbalk in de documentatielezer
  • lib/widgets/shell/welcome_screen.dart:397 — het wolkje bij een op afstand
    opgehaald recent bestand
  • lib/widgets/dialogs/consent_dialog.dart:125 — rand van het akkoordvak (30%
    alfa, dus minder erg, maar dezelfde bron)

Twee routes

A — chirurgisch. Een textButtonTheme met dezelfde regel als de omlijnde
knop hierboven, plus de zes aanroepplekken naar een kleur die in beide modi
leest. Volgt een beslissing die in dit bestand al genomen is, raakt verder niets,
en laat de donkere modus er verder uitzien zoals hij eruitziet.

B — bij de wortel. primary in het profiel Donker zelf licht maken (of in
fromProfile afdwingen dat scheme.primary op surface leest). Eén wijziging
die alle acht plekken tegelijk oplost, inclusief de zevende die volgende maand
wordt toegevoegd. Maar primary is niet alleen voorgrond: het is ook de vulling
van een selectievakje, een schakelaar, de focusring en de tekstselectie. Die zijn
nu bijna zwart — vermoedelijk ook fout, maar het verandert wél hoe donkere modus
eruitziet.

Aanbeveling: A nu, omdat het aantoonbaar geen bijwerkingen heeft en de
eerste-start-blokkade meteen weg is. B is de eigenlijke vraag en verdient een
eigen ronde met de andere primary-gebruiken (vullingen, focus) erbij.

Regressietest

De bestaande test/app_theme_contrast_test.dart (#606) meet losse tokens, maar
niet de kleuren die ThemeData zélf uitdeelt — precies het gat waar dit door
viel. Toe te voegen: voor elk ingebouwd AppAppearanceProfile het opgeloste
voorgrondkleur van TextButton, OutlinedButton en ElevatedButton naast
colorScheme.surface leggen en 4,5:1 eisen. Die toets staat vandaag rood op het
profiel Donker.

Plus een bronwacht in dezelfde stijl als de bestaande: colorScheme.primary niet
als color: van tekst of icoon.

Kosten

Route A: een paar uur. Geen nieuwe l10n.d('…'), geen afhankelijkheid, geen
SBOM-gevolg. Wel documentatie: CHANGELOG, en docs/ACCESSIBILITY.md als daar een
uitspraak over contrast in staat.

## Wat er gebeurt In donkere modus zijn tekstknoppen, links en een aantal iconen praktisch onzichtbaar: bijna zwart op een donkere ondergrond. Het meest zichtbaar op het **toestemmingsscherm** — de eerste keer dat iemand de app opent. De twee licentielinks ("Lees de volledige licentie", "Volledige licentie online (23 officiële taalversies)") staan er als donkere vlekken in de kaart. Wie de app voor het eerst start in donkere modus krijgt een licentie voorgelegd waarvan hij de weg naar de tekst niet kan zien. Reproduceren: Instellingen → App-thema → profiel *Donker*. Zichtbaar op het openscherm ("Gebruikershandleiding", "Instellingen"), in "Over OciDeck" ("Website van de stichting"), in de documentatielezer, en op het toestemmingsscherm. ## Waarom Het profiel *Donker* zet `primaryColor: '#111827'` — bijna zwart. Dat is prima als merk-/paneelkleur, maar het gaat in `AppTheme.fromProfile` ongewijzigd door naar `ColorScheme.primary`, en Material gebruikt `primary` als **voorgrondkleur** van een `TextButton`. Gemeten: | | op `surface` | |---|---| | `primary` `#111827` op `#1E293B` (donker) | **1,21 : 1** | | `primary` `#1C2B47` op `#FFFFFF` (licht) | 14,13 : 1 | WCAG AA vraagt 4,5:1 voor gewone tekst. 1,21:1 is geen "krap", dat is weg. Het is bovendien al eens half opgelost. `app_theme.dart` heeft precies deze constatering staan, maar alleen voor de omlijnde knop: ```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, ), ), ``` Er is géén `textButtonTheme`. Daarom is in "Over OciDeck" de knop "Alle licentieteksten tonen" (omlijnd) wél leesbaar en de link "Website van de stichting" (tekstknop) ernaast niet — twee regels uit elkaar, hetzelfde paneel. Daarnaast wordt `colorScheme.primary` op zes plekken rechtstreeks als voorgrond gebruikt en heeft daar hetzelfde probleem: - `lib/widgets/privacy_statement_content.dart:265` — de sectie-iconen op het toestemmingsscherm - `lib/widgets/reader/document_markdown_view.dart:473,477,482` — opsommingsteken, **link** en citaatbalk in de documentatielezer - `lib/widgets/shell/welcome_screen.dart:397` — het wolkje bij een op afstand opgehaald recent bestand - `lib/widgets/dialogs/consent_dialog.dart:125` — rand van het akkoordvak (30% alfa, dus minder erg, maar dezelfde bron) ## Twee routes **A — chirurgisch.** Een `textButtonTheme` met dezelfde regel als de omlijnde knop hierboven, plus de zes aanroepplekken naar een kleur die in beide modi leest. Volgt een beslissing die in dit bestand al genomen is, raakt verder niets, en laat de donkere modus er verder uitzien zoals hij eruitziet. **B — bij de wortel.** `primary` in het profiel *Donker* zelf licht maken (of in `fromProfile` afdwingen dat `scheme.primary` op `surface` leest). Eén wijziging die alle acht plekken tegelijk oplost, inclusief de zevende die volgende maand wordt toegevoegd. Maar `primary` is niet alleen voorgrond: het is ook de vulling van een selectievakje, een schakelaar, de focusring en de tekstselectie. Die zijn nu bijna zwart — vermoedelijk ook fout, maar het verandert wél hoe donkere modus eruitziet. Aanbeveling: **A nu**, omdat het aantoonbaar geen bijwerkingen heeft en de eerste-start-blokkade meteen weg is. B is de eigenlijke vraag en verdient een eigen ronde met de andere `primary`-gebruiken (vullingen, focus) erbij. ## Regressietest De bestaande `test/app_theme_contrast_test.dart` (#606) meet losse tokens, maar niet de kleuren die `ThemeData` zélf uitdeelt — precies het gat waar dit door viel. Toe te voegen: voor elk ingebouwd `AppAppearanceProfile` het opgeloste voorgrondkleur van `TextButton`, `OutlinedButton` en `ElevatedButton` naast `colorScheme.surface` leggen en 4,5:1 eisen. Die toets staat vandaag rood op het profiel *Donker*. Plus een bronwacht in dezelfde stijl als de bestaande: `colorScheme.primary` niet als `color:` van tekst of icoon. ## Kosten Route A: een paar uur. Geen nieuwe `l10n.d('…')`, geen afhankelijkheid, geen SBOM-gevolg. Wel documentatie: CHANGELOG, en `docs/ACCESSIBILITY.md` als daar een uitspraak over contrast in staat.
Author
Owner

Route A gemerged: 927c76a2 (PR #746). Het issue blijft open voor route B.

Wat er staat: dezelfde regel als de omlijnde knop nu ook op textButtonTheme, en 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.

Twee toetsen erbij in app_theme_contrast_test.dart: per ingebouwd profiel de opgeloste knopvoorgronden plus accentInk tegen 4,5:1 op het eigen oppervlak, en een bronwacht op colorScheme.primary als color:. Drie mutaties rood gezien, waaronder de gemeten {TextButton: 1.2126638…} op Donker.

Draaiend nagelopen in donkere modus: toestemmingsscherm, openscherm en de documentatielezer.

Wat nog openstaat (route B). primary in het profiel Donker is nog steeds #111827, en dat is ook de vulling van het selectievakje, de schakelaar, de focusring en de tekstselectie. Die zijn nu bijna zwart. Vermoedelijk ook fout, maar het verandert hoe donkere modus eruitziet, dus dat hoort in een eigen ronde mét die gebruiken erbij — niet meeliftend op een contrastreparatie.

**Route A gemerged**: 927c76a2 (PR #746). Het issue blijft open voor route B. Wat er staat: dezelfde regel als de omlijnde knop nu ook op `textButtonTheme`, en `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. Twee toetsen erbij in `app_theme_contrast_test.dart`: per ingebouwd profiel de opgeloste knopvoorgronden plus `accentInk` tegen 4,5:1 op het eigen oppervlak, en een bronwacht op `colorScheme.primary` als `color:`. Drie mutaties rood gezien, waaronder de gemeten `{TextButton: 1.2126638…}` op *Donker*. Draaiend nagelopen in donkere modus: toestemmingsscherm, openscherm en de documentatielezer. **Wat nog openstaat (route B).** `primary` in het profiel *Donker* is nog steeds `#111827`, en dat is ook de vulling van het selectievakje, de schakelaar, de focusring en de tekstselectie. Die zijn nu bijna zwart. Vermoedelijk ook fout, maar het verandert hoe donkere modus eruitziet, dus dat hoort in een eigen ronde mét die gebruiken erbij — niet meeliftend op een contrastreparatie.
Author
Owner

Route B opgepakt. Tak: fix/primary-donker-b-744.

Gemeten in het profiel Donker voordat ik iets aanraak:

primary #111827 op surface #1E293B 1,21:1
onPrimary #122F60 op primary #111827 1,35:1
cursorColor niet gezet → valt terug op colorScheme.primary

Dat tweede getal is erger dan waar dit issue mee begon: een aangevinkt selectievakje is een bijna-zwart vlak met een donkerblauw vinkje erop — je kunt niet zien óf het aan staat. En omdat er geen TextSelectionTheme is, is de tekstcursor in élk invoerveld #111827 op #1E293B.

De oorzaak is dat primary twee dingen tegelijk is: appBarTheme.backgroundColor gebruikt hem als balkkleur (en dáár moet hij donker blijven), terwijl Material hem als accent voor interactieve onderdelen gebruikt (en dáár moet hij licht zijn). Die twee kunnen in donkere modus niet dezelfde waarde hebben. primaryColor in het profiel simpelweg licht maken zou de bovenbalk licht maken — dus dat is niet de reparatie.

Reikwijdte: ColorScheme.primary loskoppelen van de balkkleur, een TextSelectionThemeData, en toetsen op primary/surface (3:1, WCAG 1.4.11) en onPrimary/primary (4,5:1) per ingebouwd profiel.

Route B opgepakt. Tak: `fix/primary-donker-b-744`. Gemeten in het profiel *Donker* voordat ik iets aanraak: | | | |---|---| | `primary` `#111827` op `surface` `#1E293B` | 1,21:1 | | `onPrimary` `#122F60` op `primary` `#111827` | **1,35:1** | | `cursorColor` | niet gezet → valt terug op `colorScheme.primary` | Dat tweede getal is erger dan waar dit issue mee begon: een aangevinkt selectievakje is een bijna-zwart vlak met een donkerblauw vinkje erop — je kunt niet zien óf het aan staat. En omdat er geen `TextSelectionTheme` is, is de tekstcursor in élk invoerveld `#111827` op `#1E293B`. De oorzaak is dat `primary` twee dingen tegelijk is: `appBarTheme.backgroundColor` gebruikt hem als balkkleur (en dáár moet hij donker blijven), terwijl Material hem als accent voor interactieve onderdelen gebruikt (en dáár moet hij licht zijn). Die twee kunnen in donkere modus niet dezelfde waarde hebben. `primaryColor` in het profiel simpelweg licht maken zou de bovenbalk licht maken — dus dat is niet de reparatie. Reikwijdte: `ColorScheme.primary` loskoppelen van de balkkleur, een `TextSelectionThemeData`, en toetsen op `primary`/`surface` (3:1, WCAG 1.4.11) en `onPrimary`/`primary` (4,5:1) per ingebouwd profiel.
Author
Owner

Route B gemerged: bf8e55d3 (PR #749). Daarmee is dit issue af — route A zat in #746.

ColorScheme.primary is losgekoppeld van de balkkleur: in donkere modus volgt hij het accent van het profiel, de AppBar houdt de merkkleur. Plus een TextSelectionThemeData, die er helemaal niet was.

Donker
primary op surface 1,21 → 5,75:1
onPrimary op primary (het vinkje) 1,35 → 5,16:1
cursor niet gezet → #60A5FA, expliciet

Beide lichte profielen komen er byte-identiek uit.

Vier toetsen erbij, per ingebouwd profiel — waaronder één die de verkeerde uitweg dichthoudt: een lichte primaryColor laat nu de titel in de bovenbalk vallen. Drie mutaties rood gezien. Draaiend nagelopen: schakelaar, focusrand en tekstselectie.

Wat bewust níét is gedaan. Dit dekt de drie ingebouwde profielen. Wie zelf een donker profiel maakt met een donkere accentColor krijgt hetzelfde terug. Ik heb daar geen stille correctie op gezet — een kleur die je kiest en die de app dan negeert is een ander soort verrassing. Als dat opgevangen moet worden hoort het zichtbaar in de profielbewerker, zoals de diakwaliteitscontrole dat voor het dek van de gebruiker al doet. Aparte afweging; zeg maar of je daar een issue voor wilt.

**Route B gemerged**: bf8e55d3 (PR #749). Daarmee is dit issue af — route A zat in #746. `ColorScheme.primary` is losgekoppeld van de balkkleur: in donkere modus volgt hij het accent van het profiel, de `AppBar` houdt de merkkleur. Plus een `TextSelectionThemeData`, die er helemaal niet was. | | Donker | |---|---| | `primary` op `surface` | 1,21 → **5,75:1** | | `onPrimary` op `primary` (het vinkje) | 1,35 → **5,16:1** | | cursor | niet gezet → `#60A5FA`, expliciet | Beide lichte profielen komen er byte-identiek uit. Vier toetsen erbij, per ingebouwd profiel — waaronder één die de verkeerde uitweg dichthoudt: een lichte `primaryColor` laat nu de titel in de bovenbalk vallen. Drie mutaties rood gezien. Draaiend nagelopen: schakelaar, focusrand en tekstselectie. **Wat bewust níét is gedaan.** Dit dekt de drie ingebouwde profielen. Wie zelf een donker profiel maakt met een donkere `accentColor` krijgt hetzelfde terug. Ik heb daar geen stille correctie op gezet — een kleur die je kiest en die de app dan negeert is een ander soort verrassing. Als dat opgevangen moet worden hoort het zichtbaar in de profielbewerker, zoals de diakwaliteitscontrole dat voor het dek van de gebruiker al doet. Aparte afweging; zeg maar of je daar een issue voor wilt.
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#744
No description provided.