[Feature] Waarschuw in de profielbewerker als een eigen app-thema onleesbaar wordt #750

Closed
opened 2026-07-23 14:51:34 +00:00 by brenno · 2 comments
Owner

Waar dit vandaan komt

#744 was er in twee helften: tekstknoppen en links op 1,21:1, en een
selectievakje dat zijn eigen stand niet toonde (vinkje op 1,35:1 tegen zijn
vulling). Beide zijn gerepareerd, en er staan nu toetsen op — maar die meten de
drie ingebouwde profielen.

Uiterlijk → App-thema laat de gebruiker een eigen profiel maken met acht vrije
kleuren. Zet iemand daar een donkere accentColor in een donker profiel, dan
krijgt hij precies #744 terug, en niets zegt er iets over. Dat is de kant die
open bleef.

Ik heb bewust geen stille correctie ingebouwd: een kleur die de gebruiker kiest
en die de app dan negeert is een ander soort verrassing, en moeilijker te
begrijpen dan een lelijk resultaat. Zichtbaar waarschuwen past bij wat de app
voor het dék van de gebruiker al doet.

Wat er nu staat

lib/widgets/dialogs/parts/settings_dialog_appearance.dart — acht
_appearanceColorSetting-velden en een _appearancePreview() (regel 320).
Alleen voor eigen profielen: editable = !_appearanceProfile.isBuiltIn.

Er is geen enkele contrastterugkoppeling. En het voorbeeld vleit, wat erger
is dan niets:

  • _contrastColor() (regel 403) kiest zwart of wit voor de baltitel en het
    knoplabel, op luminantie. De échte app gebruikt daar panelTextColor en een
    in AppTheme.fromProfile berekende voorgrond. Het voorbeeld laat dus een
    leesbare baltitel zien waar de app een onleesbare toont.
  • Het voorbeeld toont de rollen die in #744 goed waren (bar, zijbalk, kaart,
    gevulde knop) en precies niet de rollen die stukgingen: een selectievakje of
    schakelaar, een tekstcursor, een tekstknop of link.

Alles om het te meten ligt er al: contrastRatio, meetsWcagAa,
hexContrastRatio en blendedHexContrastRatio in lib/utils/color_contrast.dart.

Wat er nagekeken moet worden

De paren, met de lat en waar ze vandaan komen:

Paar Lat Waarom
textColor op surfaceColor en op backgroundColor 4,5:1 gewone tekst (WCAG 1.4.3)
mutedTextColor op surfaceColor 4,5:1 het is nog steeds tekst
panelTextColor op panelColor 4,5:1 de zijbalk
panelTextColor op primaryColor 4,5:1 de titel in de bovenbalk — het veld heet niet voor niets "Hoofdkleur en bovenbalk"
tekstknopvoorgrond (isDark ? textColor : primaryColor) op surfaceColor 4,5:1 route A van #744
interactiekleur (isDark ? accentColor : primaryColor) op surfaceColor 3:1 grafisch onderdeel (WCAG 1.4.11) — route B
onPrimary op die interactiekleur 4,5:1 het vinkje op zijn eigen vulling — dít was 1,35:1
voorgrond van de primaire knop op accentColor 4,5:1 wordt in fromProfile berekend, dus meebewegen

De laatste vier zijn afgeleide waarden, geen velden. Ze zijn alleen te meten
door het profiel écht door AppTheme.fromProfile te halen en het resultaat te
lezen — precies wat test/app_theme_contrast_test.dart sinds #744 voor de
ingebouwde profielen doet. Die code is te hergebruiken; laat de bewerker niet
zijn eigen tweede rekensom krijgen die uit de pas gaat lopen.

Hoe het eruit zou moeten zien

Twee dingen, en de tweede is belangrijker dan de eerste:

  1. Een waarschuwing bij het profiel, in de vorm die de app al kent: geen
    blokkade, wel benoemd, met de gemeten verhouding en het paar erbij ("de
    titel in de bovenbalk staat op 1,9:1"). De diakwaliteitscontrole doet dit al
    voor het dek; dit is dezelfde belofte, nu over de app zelf.
  2. Het voorbeeld eerlijk maken. Laat _contrastColor() los en gebruik de
    kleuren die de app werkelijk gebruikt, en zet er een selectievakje, een
    schakelaar en een tekstknop bij. Een voorbeeld dat de fout laat zien is meer
    waard dan een lijst met getallen eronder — en het is de enige plek waar
    iemand het ziet vóórdat hij opslaat.

Open vragen

  • Welke lat? De gebruiker kan Minimale contrastverhouding instellen
    (standaard 3,5:1). Die knop gaat over zijn dia's. Voor de chrome van de
    app zelf zou ik hem niet laten meebewegen — de app zijn eigen ondergrens laten
    verlagen is iets anders dan de gebruiker zijn eigen dek laten beoordelen. Vaste
    WCAG AA dus. Maar dat is een keuze, geen vanzelfsprekendheid.
  • Waarschuwen of tegenhouden? Waarschuwen, denk ik: het is zijn app, en de
    weg terug is één kleur. Blokkeren past bij export (waar het resultaat de deur
    uit gaat), niet bij een voorkeur.
  • Ook bij het inladen van een profiel? Een profiel dat vóór deze controle is
    gemaakt, of dat geïmporteerd wordt, komt nooit langs de bewerker.

Kosten

Schatting een dag. De rekensom bestaat al; het werk zit in het eerlijk maken van
het voorbeeld en in de teksten. Reken op drie tot vijf nieuwe l10n.d('…'),
en die kosten hier het meeste — elke string gaat langs 31 talen (make add-l10n).
Geen nieuwe afhankelijkheid, geen SBOM-gevolg. docs/ACCESSIBILITY.md noemt sinds
#744 dat de toetsen alleen de ingebouwde profielen dekken; die alinea gaat mee.

## Waar dit vandaan komt #744 was er in twee helften: tekstknoppen en links op 1,21:1, en een selectievakje dat zijn eigen stand niet toonde (vinkje op 1,35:1 tegen zijn vulling). Beide zijn gerepareerd, en er staan nu toetsen op — maar die meten de **drie ingebouwde profielen**. *Uiterlijk → App-thema* laat de gebruiker een eigen profiel maken met acht vrije kleuren. Zet iemand daar een donkere `accentColor` in een donker profiel, dan krijgt hij precies #744 terug, en niets zegt er iets over. Dat is de kant die open bleef. Ik heb bewust geen stille correctie ingebouwd: een kleur die de gebruiker kiest en die de app dan negeert is een ander soort verrassing, en moeilijker te begrijpen dan een lelijk resultaat. Zichtbaar waarschuwen past bij wat de app voor het dék van de gebruiker al doet. ## Wat er nu staat `lib/widgets/dialogs/parts/settings_dialog_appearance.dart` — acht `_appearanceColorSetting`-velden en een `_appearancePreview()` (regel 320). Alleen voor eigen profielen: `editable = !_appearanceProfile.isBuiltIn`. Er is geen enkele contrastterugkoppeling. En het voorbeeld **vleit**, wat erger is dan niets: - `_contrastColor()` (regel 403) kiest zwart of wit voor de baltitel en het knoplabel, op luminantie. De échte app gebruikt daar `panelTextColor` en een in `AppTheme.fromProfile` berekende voorgrond. Het voorbeeld laat dus een leesbare baltitel zien waar de app een onleesbare toont. - Het voorbeeld toont de rollen die in #744 *goed* waren (bar, zijbalk, kaart, gevulde knop) en precies niet de rollen die stukgingen: een selectievakje of schakelaar, een tekstcursor, een tekstknop of link. Alles om het te meten ligt er al: `contrastRatio`, `meetsWcagAa`, `hexContrastRatio` en `blendedHexContrastRatio` in `lib/utils/color_contrast.dart`. ## Wat er nagekeken moet worden De paren, met de lat en waar ze vandaan komen: | Paar | Lat | Waarom | |---|---|---| | `textColor` op `surfaceColor` en op `backgroundColor` | 4,5:1 | gewone tekst (WCAG 1.4.3) | | `mutedTextColor` op `surfaceColor` | 4,5:1 | het is nog steeds tekst | | `panelTextColor` op `panelColor` | 4,5:1 | de zijbalk | | `panelTextColor` op `primaryColor` | 4,5:1 | de titel in de bovenbalk — het veld heet niet voor niets *"Hoofdkleur en bovenbalk"* | | tekstknopvoorgrond (`isDark ? textColor : primaryColor`) op `surfaceColor` | 4,5:1 | route A van #744 | | interactiekleur (`isDark ? accentColor : primaryColor`) op `surfaceColor` | 3:1 | grafisch onderdeel (WCAG 1.4.11) — route B | | `onPrimary` op die interactiekleur | 4,5:1 | het vinkje op zijn eigen vulling — dít was 1,35:1 | | voorgrond van de primaire knop op `accentColor` | 4,5:1 | wordt in `fromProfile` berekend, dus meebewegen | De laatste vier zijn afgeleide waarden, geen velden. Ze zijn alleen te meten door het profiel écht door `AppTheme.fromProfile` te halen en het resultaat te lezen — precies wat `test/app_theme_contrast_test.dart` sinds #744 voor de ingebouwde profielen doet. Die code is te hergebruiken; laat de bewerker niet zijn eigen tweede rekensom krijgen die uit de pas gaat lopen. ## Hoe het eruit zou moeten zien Twee dingen, en de tweede is belangrijker dan de eerste: 1. **Een waarschuwing bij het profiel**, in de vorm die de app al kent: geen blokkade, wel benoemd, met de gemeten verhouding en het paar erbij ("de titel in de bovenbalk staat op 1,9:1"). De diakwaliteitscontrole doet dit al voor het dek; dit is dezelfde belofte, nu over de app zelf. 2. **Het voorbeeld eerlijk maken.** Laat `_contrastColor()` los en gebruik de kleuren die de app werkelijk gebruikt, en zet er een selectievakje, een schakelaar en een tekstknop bij. Een voorbeeld dat de fout laat zien is meer waard dan een lijst met getallen eronder — en het is de enige plek waar iemand het ziet vóórdat hij opslaat. ## Open vragen - **Welke lat?** De gebruiker kan *Minimale contrastverhouding* instellen (standaard 3,5:1). Die knop gaat over **zijn dia's**. Voor de chrome van de app zelf zou ik hem niet laten meebewegen — de app zijn eigen ondergrens laten verlagen is iets anders dan de gebruiker zijn eigen dek laten beoordelen. Vaste WCAG AA dus. Maar dat is een keuze, geen vanzelfsprekendheid. - **Waarschuwen of tegenhouden?** Waarschuwen, denk ik: het is zijn app, en de weg terug is één kleur. Blokkeren past bij export (waar het resultaat de deur uit gaat), niet bij een voorkeur. - **Ook bij het inladen van een profiel?** Een profiel dat vóór deze controle is gemaakt, of dat geïmporteerd wordt, komt nooit langs de bewerker. ## Kosten Schatting een dag. De rekensom bestaat al; het werk zit in het eerlijk maken van het voorbeeld en in de teksten. Reken op **drie tot vijf nieuwe `l10n.d('…')`**, en die kosten hier het meeste — elke string gaat langs 31 talen (`make add-l10n`). Geen nieuwe afhankelijkheid, geen SBOM-gevolg. `docs/ACCESSIBILITY.md` noemt sinds #744 dat de toetsen alleen de ingebouwde profielen dekken; die alinea gaat mee.
Author
Owner

Gewogen: bouwen — accepted. Het verhaal klopt tegen de code (de acht velden, _contrastColor() op regel 403 dat vleit, en de herbruikbare rekenroute uit app_theme_contrast_test), en het past bij wat de app al belooft: eerlijk meten in plaats van stil corrigeren. Op de drie open vragen volg ik de aanbevelingen uit het issue zelf: vaste WCAG AA-lat voor de app-chrome (de instelbare drempel gaat over het dek van de gebruiker, niet over de app), waarschuwen en niet blokkeren, en het inladen/importeren-gat als bewuste beperking benoemen in ACCESSIBILITY.md in plaats van er nu een tweede waarschuwingsoppervlak bij te bouwen — de bewerker is waar de kleur gekozen wordt. Volgorde: na het lopende #541/#570-werk.

Gewogen: **bouwen** — accepted. Het verhaal klopt tegen de code (de acht velden, _contrastColor() op regel 403 dat vleit, en de herbruikbare rekenroute uit app_theme_contrast_test), en het past bij wat de app al belooft: eerlijk meten in plaats van stil corrigeren. Op de drie open vragen volg ik de aanbevelingen uit het issue zelf: vaste WCAG AA-lat voor de app-chrome (de instelbare drempel gaat over het dek van de gebruiker, niet over de app), waarschuwen en niet blokkeren, en het inladen/importeren-gat als bewuste beperking benoemen in ACCESSIBILITY.md in plaats van er nu een tweede waarschuwingsoppervlak bij te bouwen — de bewerker is waar de kleur gekozen wordt. Volgorde: na het lopende #541/#570-werk.
Author
Owner

Gebouwd en gemerged: 159ebd9d (PR #755).

Beide helften zitten erin. De meting staat onder het voorbeeld — negen paren met de gemeten verhouding, de lat en de twee vergeleken kleuren als stippen — en het voorbeeld is eerlijk gemaakt: het bouwt nu het échte ThemeData in plaats van zijn eigen kleuren te schilderen met een zwart-of-wit-op-luminantie-hulpje, en er staan een selectievakje, een schakelaar en een tekstknop in. Met een accent van #1B2537 in een donker profiel zie je het vinkje en de schakelaar nu verdwijnen in het voorbeeld zelf, mét de twee regels eronder.

De rekensom (lib/theme/appearance_contrast.dart) meet het gebouwde thema en niet de acht velden — vier van de negen paren bestaan niet als veld — en voedt óók app_theme_contrast_test.dart, zodat er één som is.

Wat de meting meteen zelf vond: het label op de primaire knop stond in Donker op 2,54:1. De voorgrond werd gekozen met brightness == light && luminance > 0.6 ? zwart : wit, wat in donkere modus altijd wit afdwong — ook op het lichte accent #60A5FA. Dat is de opslaan-knop. Nu 8,26:1; beide lichte profielen houden hun kleur.

Op de open vragen: vaste WCAG AA, niet de instelbare Minimale contrastverhouding (die gaat over de dia's van de gebruiker). Waarschuwen, niet blokkeren. Een profiel dat nooit langs de bewerker komt is nog steeds niet gedekt — dat is bewust niet meegenomen.

Eén ding dat ik onderweg tegenkwam en niet heb aangeraakt: de +-knop schrijft een gedupliceerd profiel meteen naar de instellingen in plaats van pas bij Opslaan, dus Annuleren laat het profiel staan. Bestaand gedrag, maar het verraste me.

Gebouwd en gemerged: 159ebd9d (PR #755). Beide helften zitten erin. De meting staat onder het voorbeeld — negen paren met de gemeten verhouding, de lat en de twee vergeleken kleuren als stippen — en het **voorbeeld is eerlijk gemaakt**: het bouwt nu het échte `ThemeData` in plaats van zijn eigen kleuren te schilderen met een zwart-of-wit-op-luminantie-hulpje, en er staan een selectievakje, een schakelaar en een tekstknop in. Met een accent van `#1B2537` in een donker profiel zie je het vinkje en de schakelaar nu verdwijnen in het voorbeeld zelf, mét de twee regels eronder. De rekensom (`lib/theme/appearance_contrast.dart`) meet het gebouwde thema en niet de acht velden — vier van de negen paren bestaan niet als veld — en voedt óók `app_theme_contrast_test.dart`, zodat er één som is. **Wat de meting meteen zelf vond:** het label op de primaire knop stond in *Donker* op 2,54:1. De voorgrond werd gekozen met `brightness == light && luminance > 0.6 ? zwart : wit`, wat in donkere modus altijd wit afdwong — ook op het lichte accent `#60A5FA`. Dat is de opslaan-knop. Nu 8,26:1; beide lichte profielen houden hun kleur. Op de open vragen: **vaste WCAG AA**, niet de instelbare *Minimale contrastverhouding* (die gaat over de dia's van de gebruiker). **Waarschuwen**, niet blokkeren. Een profiel dat nooit langs de bewerker komt is nog steeds niet gedekt — dat is bewust niet meegenomen. Eén ding dat ik onderweg tegenkwam en niet heb aangeraakt: de `+`-knop schrijft een gedupliceerd profiel meteen naar de instellingen in plaats van pas bij Opslaan, dus *Annuleren* laat het profiel staan. Bestaand gedrag, maar het verraste me.
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#750
No description provided.