feat(keuzemenu): drie indelingen, categorieen en een uitleg per blok (#1162) #1549

Merged
brenno merged 19 commits from menu-indelingen-en-categorieen into main 2026-08-18 08:37:01 +00:00
Owner

Wat dit toevoegt

Het keuzemenu-diatype (#1162) kon één ding: een raster van blokken met een label. Deze tak maakt er een echt menu van.

  • Drie indelingen per dia: Raster, Onder elkaar en In een cirkel. Presentatiekeuze, geen inhoud — omschakelen herschikt en verandert niets aan wat de auteur typte.
  • Categorieën: een lang menu deel je op met dezelfde tussenkoppen die opsommingsdia's al gebruiken. Tijdens het presenteren staat er een balk met pillen boven de blokken; het beamervenster volgt via dezelfde sync als de rich-text-pagina.
  • Uitleg per blok, één regel onder het label.
  • De blokafbeelding staat nu klein náást de tekst in plaats van eroverheen.

Het bestandsformaat

Alles rijdt mee op wat er al was; een bestaand menu verandert geen byte.

<!-- _class: menu menu-list -->
# Waar wil je heen?

- ␀Producten
- [Prijzen](#prijzen) — Wat het kost, per maand ![](mem:9f2a1c)
- ␀Over ons
- [Team](#team)

De indeling is een extra _class-token (menu-list/menu-circle; het raster schrijft er géén, dus oude bestanden blijven identiek). De uitleg staat achter een gedachtestreepje in dezelfde link-opsomming. Categorieën zijn de bestaande tussenkop-bullet ( = U+E010).

Afweging (bewaker)

De bewaker keurde de vier formaatkeuzes goed, met twee kanttekeningen die in de tekst staan en één die hier hoort:

  • De PUA-tussenkop voor categorieën is hergebruik van een bestaand mechanisme, en dat weegt hier op tegen een tweede lijst (twee dingen synchroon houden is de val) — ## was geen optie, want dat is de ondertitel. Voorwaarde om terug te komen op dat oordeel: zodra een vierde mechanisme aan dat teken gaat hangen, of zodra het in een export of een schermlezerlabel terechtkomt. Dan is het geen bullet-conventie meer maar een privéformaat.
  • Buiten OciDeck is dit een leesbaar menu, geen werkend menu: #prijzen is het ocideck_slide_anchor, en geen andere Marp-implementatie zet daar een id op. Dat is inherent; het staat nu in de USER_GUIDE zodat de lezer het niet zelf hoeft te ontdekken.

Onderweg gevonden en meegenomen

Een tabeldia verloor zijn typetoken bij het opslaan. table-overdue ontbrak in de woordenlijst van de structuurcontrole, dus waarschuwde OciDeck over een dia die het zelf had geschreven. Dat token toevoegen dempte de waarschuwing — en de bewaker wees erop dat er iets ergers achter zat: de lézer filterde het ook niet weg, dus belandde het in de vrije klasse van de dia, en die vervángt het typetoken bij het schrijven. Gemeten over drie generaties: table table-overduetable-overdue table-overdue → drie tokens, en table was weg; de dia las niet meer terug als tabel. Lezer en schrijver delen nu één lijst, en een proef slaat drie keer achter elkaar op en eist dat elke _class-regel identiek terugkomt. image-title-above ontbrak op dezelfde manier.

De plaatshouder voor een ontbrekende afbeelding volgt het thema. Alle drie de plaatshouders stonden op vaste lichtgrijze kleuren — gewogen tegen een witte dia, maar op een donker thema een fel blok midden in beeld. Raakt elk diatype met een afbeelding.

Een sprong die nergens uitkomt wordt gemeld. Wijst een menublok of een sprong-uit naar een verwijderde dia, dan viel de presentator stil terug op de gewone volgorde: een knop die niets doet, pas merkbaar op het podium.

Verder: de gedeelde afbeeldingsplaatshouder liep over in een vak van 48–60 px, en markdown_service_parse.dart is gesplitst omdat het tegen het harde plafond van 1000 regels liep.

Hoe het is getoetst

Drie beeldkeuringen op de draaiende renderer (alle vier de oppervlakken, beide thema's, de HTML-export). Die vonden wat groene tests niet zien, want het waren geen overlopen:

  • tekst die stil werd weggeknipt in plaats van af te breken met een ellips;
  • een dia die door de FittedBox tot 27% van de breedte kromp;
  • een scheve ring (het blokkenvlak was zo breed als de dia, breder dan de kolom eromheen);
  • uitleg onder de contrastvloer (3,97:1);
  • bij zestien blokken een dia die heel maar onleesbaar was — 3,7 px letter.

Daar zijn proeven bij gekomen die precies dát meten: de titel van dezelfde dia met 16 blokken tegen die met 3 (vangt het krimpen), rechthoeken van label en uitleg die elkaar niet mogen overlappen (vangt het botsen), en een leesbaarheidsvloer op de werkelijk getekende lettergrootte (vangt het onleesbare). Elke reparatie is één keer rood gezien tegen de ongerepareerde code.

make check groen (9700+ tests, dekkingsvloer en per-bestandsvloer), make check-secrets groen, make sast groen (0 findings). DAST niet gedraaid — deze wijziging raakt geen geserveerd oppervlak.

Wat hier bewust niet in zit

De menublokken zijn niet met het toetsenbord bedienbaar (GestureDetector zonder focus of knoprol). Dat is een ontwerpvraag op zichzelf — de presentator vangt cijfers al af voor dianummers en pijltjes voor de volgorde, en een focusring moet van veraf zichtbaar zijn zonder de dia te ontsieren. Het staat als bekend gat in docs/ACCESSIBILITY.md.

## Wat dit toevoegt Het keuzemenu-diatype (#1162) kon één ding: een raster van blokken met een label. Deze tak maakt er een echt menu van. - **Drie indelingen** per dia: *Raster*, *Onder elkaar* en *In een cirkel*. Presentatiekeuze, geen inhoud — omschakelen herschikt en verandert niets aan wat de auteur typte. - **Categorieën**: een lang menu deel je op met dezelfde tussenkoppen die opsommingsdia's al gebruiken. Tijdens het presenteren staat er een balk met pillen boven de blokken; het beamervenster volgt via dezelfde sync als de rich-text-pagina. - **Uitleg per blok**, één regel onder het label. - **De blokafbeelding staat nu klein náást de tekst** in plaats van eroverheen. ## Het bestandsformaat Alles rijdt mee op wat er al was; **een bestaand menu verandert geen byte**. ```markdown <!-- _class: menu menu-list --> # Waar wil je heen? - ␀Producten - [Prijzen](#prijzen) — Wat het kost, per maand ![](mem:9f2a1c) - ␀Over ons - [Team](#team) ``` De indeling is een extra `_class`-token (`menu-list`/`menu-circle`; het raster schrijft er géén, dus oude bestanden blijven identiek). De uitleg staat achter een gedachtestreepje in dezelfde link-opsomming. Categorieën zijn de bestaande tussenkop-bullet (`␀` = `U+E010`). ### Afweging (bewaker) De bewaker keurde de vier formaatkeuzes goed, met twee kanttekeningen die in de tekst staan en één die hier hoort: - **De PUA-tussenkop voor categorieën** is hergebruik van een bestaand mechanisme, en dat weegt hier op tegen een tweede lijst (twee dingen synchroon houden is de val) — `##` was geen optie, want dat is de ondertitel. **Voorwaarde om terug te komen op dat oordeel:** zodra een vierde mechanisme aan dat teken gaat hangen, of zodra het in een export of een schermlezerlabel terechtkomt. Dan is het geen bullet-conventie meer maar een privéformaat. - **Buiten OciDeck is dit een *leesbaar* menu, geen *werkend* menu**: `#prijzen` is het `ocideck_slide_anchor`, en geen andere Marp-implementatie zet daar een `id` op. Dat is inherent; het staat nu in de USER_GUIDE zodat de lezer het niet zelf hoeft te ontdekken. ## Onderweg gevonden en meegenomen **Een tabeldia verloor zijn typetoken bij het opslaan.** `table-overdue` ontbrak in de woordenlijst van de structuurcontrole, dus waarschuwde OciDeck over een dia die het zelf had geschreven. Dat token toevoegen dempte de waarschuwing — en de bewaker wees erop dat er iets ergers achter zat: de lézer filterde het ook niet weg, dus belandde het in de vrije klasse van de dia, en die vervángt het typetoken bij het schrijven. Gemeten over drie generaties: `table table-overdue` → `table-overdue table-overdue` → drie tokens, en `table` was weg; de dia las niet meer terug als tabel. Lezer en schrijver delen nu één lijst, en een proef slaat drie keer achter elkaar op en eist dat elke `_class`-regel identiek terugkomt. `image-title-above` ontbrak op dezelfde manier. **De plaatshouder voor een ontbrekende afbeelding volgt het thema.** Alle drie de plaatshouders stonden op vaste lichtgrijze kleuren — gewogen tegen een witte dia, maar op een donker thema een fel blok midden in beeld. Raakt elk diatype met een afbeelding. **Een sprong die nergens uitkomt wordt gemeld.** Wijst een menublok of een sprong-uit naar een verwijderde dia, dan viel de presentator stil terug op de gewone volgorde: een knop die niets doet, pas merkbaar op het podium. Verder: de gedeelde afbeeldingsplaatshouder liep over in een vak van 48–60 px, en `markdown_service_parse.dart` is gesplitst omdat het tegen het harde plafond van 1000 regels liep. ## Hoe het is getoetst Drie beeldkeuringen op de draaiende renderer (alle vier de oppervlakken, beide thema's, de HTML-export). Die vonden wat groene tests niet zien, want het waren geen overlopen: - tekst die stil werd **weggeknipt** in plaats van af te breken met een ellips; - een dia die door de `FittedBox` tot **27% van de breedte** kromp; - een **scheve ring** (het blokkenvlak was zo breed als de dia, breder dan de kolom eromheen); - uitleg onder de **contrastvloer** (3,97:1); - bij zestien blokken een dia die **heel maar onleesbaar** was — 3,7 px letter. Daar zijn proeven bij gekomen die precies dát meten: de titel van dezelfde dia met 16 blokken tegen die met 3 (vangt het krimpen), rechthoeken van label en uitleg die elkaar niet mogen overlappen (vangt het botsen), en een leesbaarheidsvloer op de werkelijk getekende lettergrootte (vangt het onleesbare). Elke reparatie is één keer rood gezien tegen de ongerepareerde code. `make check` groen (9700+ tests, dekkingsvloer en per-bestandsvloer), `make check-secrets` groen, `make sast` groen (0 findings). DAST niet gedraaid — deze wijziging raakt geen geserveerd oppervlak. ## Wat hier bewust niet in zit De menublokken zijn **niet met het toetsenbord bedienbaar** (`GestureDetector` zonder focus of knoprol). Dat is een ontwerpvraag op zichzelf — de presentator vangt cijfers al af voor dianummers en pijltjes voor de volgorde, en een focusring moet van veraf zichtbaar zijn zonder de dia te ontsieren. Het staat als bekend gat in `docs/ACCESSIBILITY.md`.
Een keuzemenudia kon tot nu toe één ding: een raster van blokken met een
label. Deze wijziging maakt er een echt menu van.

- Indeling per dia: raster, onder elkaar of in een cirkel. Rijdt als
  `_class`-token mee (`menu-list`/`menu-circle`); raster is de standaard en
  schrijft niets, dus bestaande decks veranderen geen byte.
- Categorieën: de bestaande tussenkop-bullet groepeert de blokken. Tijdens
  het presenteren wissel je met een balk bovenaan tussen de groepen, en het
  beamervenster volgt via dezelfde sync als de rich-text-pagina.
- Uitleg per blok, achter een gedachtestreepje in dezelfde bullet — leesbaar
  in elke Markdown-lezer.
- De blokafbeelding staat nu klein náást de tekst in plaats van eroverheen.

Onderweg gevonden: de gedeelde afbeeldingsplaatshouder liep over in een vak
van 48 tot 60 pixels — het bijschrift bindt nu in.
Rondgang door het bestand, de editor, de presentator en de HTML-export, en
de overloopproef op alle drie de indelingen bij zestien volle blokken —
labels, uitleg, afbeeldingen en categorieën tegelijk, presentatiegroot én
slidestrook-klein.
Zeven bronstrings naar 31 talen: de drie indelingsnamen, de categorie-
teksten, de uitleg per blok en de herschreven hulptekst bij het diatype.
De oude hulptekst is als wees uit alle taalbestanden verwijderd.
Het bestand liep met het menu-indelingstoken erbij tegen het harde
plafond van 1000 regels. Het doorlopen van de body-regels en het afleiden
van het diatype daaruit verhuizen verbatim naar een part-bestand: dezelfde
library, dezelfde leden, geen gedragswijziging.

De ratchets die dit veld raakt gaan mee omhoog, elk met de reden erbij.
De kolomtrap van het raster stond twee keer uitgeschreven — in de preview
en in de HTML-export. Nu één keer, in menu_blocks.dart, want ze horen per
definitie gelijk te lopen.

En de indelingschip leende de bronstring `Cirkel` van het cirkeldiagram:
in het Engels stond er daardoor "Pie" boven een menu. Eigen bronstring.
Het geraden trapje liet de schijven vanaf een stuk of acht blokken tegen
elkaar aan lopen. De maat volgt nu uit de koorde tussen twee buren, één
keer uitgerekend en gedeeld met de HTML-export, met een proef die van twee
tot dertig blokken narekent dat ze elkaar niet raken.
De eigen bronstring voor de cirkelindeling, met overal het meetkundige
cirkelwoord — niet het taartwoord van het cirkeldiagram.
De structuurcontrole klaagde over `table-overdue` — een klassetoken dat
OciDeck zélf wegschrijft en zelf terugleest, maar dat alleen de woordenlijst
niet kende. Eén losse proef per token vangt zoiets pas als iemand eraan
denkt, dus de nieuwe proef laat de serialiser de lijst opleveren: elk
diatype, elke tijdlijn- en menu-indeling en elke losse vlag erdoorheen, en
geen enkele mag 'onbekende class' opleveren.

Die proef vond meteen een tweede: `image-title-above` (#1407) ontbrak ook.

Gevonden door de documentatieplicht bij het naslaan van FILE_FORMAT §10.
FILE_FORMAT krijgt een eigen Menu-blok — dat ontbrak — met de bulletvorm,
de categorie-tussenkop, de uitleg en de drie indelingstokens. USER_GUIDE
beschrijft het maken ervan, API_DOCUMENTATION het model, SOURCE_MAP de
nieuwe bestanden, en ACCESSIBILITY vermeldt eerlijk dat de blokken nog geen
toetsenbordroute hebben.

Onderweg drie beweringen rechtgezet die niet meer klopten: de lijst bekende
klassetokens miste er tien, het aantal Slide-velden stond op zestig (het
zijn er 76), en de scheidingsregel voor de uitleg stond te ruim.
De enum-tellingpoort kent nu ook MenuLayout, zodat het aantal waarden in
API_DOCUMENTATION niet stil kan verrotten.

De ankerpoort las voorbeelden in codeblokken als echte verwijzingen en
sloeg aan op `[Prijzen](#prijzen)` uit het nieuwe FILE_FORMAT-voorbeeld.
Codeblokken op kolom 0 tellen niet meer mee — anders dwingt de poort de
documentatie om de syntaxis te omschrijven in plaats van te tonen. Zes
voorbeelden vallen weg, 295 echte links blijven bewaakt.
De keuring liet zien dat de tests groen stonden om de verkeerde reden: er
viel geen overloop, maar de tekst werd wéggeknipt. In de indeling 'onder
elkaar' was van élk label de bovenste helft eraf, zonder ellips.

- Lettergrootte en regelbudget volgen nu uit de hoogte die er is
  (`menuTextFit`, met een proef die van 2 tot 400 px narekent dat wat wordt
  toegewezen ook past). Krimpt de uitleg tot een grijze veeg, dan valt hij
  weg en gaat de ruimte naar het label.
- De regels van 'onder elkaar' verdelen de hoogte weer, nu de tekst meekrimpt
  — geen dia meer die tot 30% ineenschrompelt in de linkerbovenhoek.
- Het blokkenvlak was zo breed als de dia en dus breder dan de kolom
  eromheen: de ring hing scheef. Nu de kolombreedte.
- De ringlijn is weg; hij liep zichtbaar door de labels heen.
- De uitleg stond op 0,85 in plaats van 0,7 alfa — 0,7 haalde de
  contrastvloer niet op een lichte kaart.
- In de ring: uitleg verdween niet langer stilzwijgend zodra er een
  afbeelding bij zat, en een springend blok heeft nu een zwaardere rand,
  want voor een pijl is in een schijf geen plek.
- Kaarten lijnen hun tekst altijd links uit; één gecentreerde kaart tussen
  linkse buren maakte de rij rafelig.
- HTML-export: de ring kreeg per categorie de volle maat en werd vijf
  schermen hoog — nu delen ze de hoogte. Een raster van meer dan negen
  blokken wordt dichter gezet, en de schijven tonen er ook hun uitleg.
- Editor: geen hint meer op 'Uitleg' (leeg zag eruit als een ander soort
  veld), en blokken springen in onder hun categoriekop.
Het veld 'Uitleg' verloor zijn hint (leeg zag hij eruit als een ander soort
veld); de vertaling ervan bleef in 32 tabellen staan. De weespoort hoort op
nul te blijven.
Gevonden door de bewaker, achter de woordenlijstfix van c509dd6c: dat
token toevoegen dempte een waarschuwing die ergens over ging.

`table-overdue` stond niet in het klassefilter van de lezer, dus belandde
het in de vrije klasse van de dia — en die vervangt het typetoken bij het
schrijven. Gemeten over drie generaties: `table table-overdue` wordt
`table-overdue table-overdue`, dan drie tokens, en `table` is weg. De dia
las daarna niet meer terug als tabel, en sinds de woordenlijstfix zweeg de
structuurcontrole erover.

Lezer en schrijver delen nu één lijst (`isOcideckWrittenClassToken`) in
plaats van twee die uit de pas kunnen lopen. De nieuwe proef slaat drie
keer achter elkaar op en eist dat elke `_class`-regel identiek terugkomt —
één keer rood gezien tegen de ongerepareerde code. De vorige proef vroeg
alleen of de contrôle het token kénde, en dat is precies het gat waar dit
doorheen viel.

Verder de andere bevindingen van de bewaker: FILE_FORMAT beweerde dat het
token 'gewoon werkt' en miste twee tokens in zijn lijst, de belofte
'schrijft identiek terug' gold niet voor een blok mét link, en dat
`menu-grid` er bij de volgende opslag weer uit gaat stond er niet bij.

De ankerpoort sloeg aan op `[Prijzen](#prijzen)` in een lopende zin: een
link tussen backticks is een voorbeeld, geen verwijzing — Markdown maakt er
zelf ook geen link van.
De vorige ronde loste er vijf op; deze vier bleven staan of kwamen ervoor
in de plaats.

- 'Onder elkaar' kreeg `maxHeight: infinity` om te mogen doorgroeien — maar
  dan ziet zijn eigen LayoutBuilder een oneindige hoogte, verdeelt hij niets,
  en schaalt de stellage de héle dia terug tot een postzegel in de
  linkerbovenhoek. Al vanaf vier blokken. Nu een begrensde hoogte, zoals de
  andere twee.
- In de schijf werd `menuTextFit` wél berekend maar niet toegepast: een
  hardgecodeerde `maxLines: 2` en twee gelijke `Flexible`s deelden de ruimte
  fiftyfifty, dus liepen label en uitleg over elkaar heen. Elk stuk tekst
  krijgt nu precies het budget dat is uitgerekend, met ruimte ertussen. En de
  rand telt mee: die zit binnen de schijf.
- De ontsnappingsroute voor 'uitleg naast een afbeelding' (`diameter >
  w * 0.22`) kon nooit waar worden — een schijf haalt hoogstens 0,16·w — dus
  viel de uitleg daar nog altijd weg. Voorwaarde geschrapt; menuTextFit weet
  zelf of het past.
- HTML-export: de dichtheid werd per categorie bepaald, dus twee categorieën
  van acht ontsnapten er allebei aan en de dia werd twee schermen hoog. Nu per
  dia, met een rijhoogte uit het hoogtebudget. De schijflabels stonden op een
  vaste 20 px in een schijf die met de ring meeschaalt en werden links én
  rechts weggesneden; de lettermaat volgt nu de schijf. En het sprongteken
  (zwaardere rand) doet de export nu ook mee.
- Editor: de inspringing stond in de code maar niet in beeld — de blokkaart
  stak links uit onder de kop waar hij bij hoort.

Twee proeven erbij voor wat 'geen overloop' nooit ziet: een dia die als
geheel wordt teruggeschaald (gemeten tegen dezelfde dia met drie blokken), en
tekst die over andere tekst valt (rechthoeken die elkaar overlappen). Allebei
één keer rood gezien tegen de ongerepareerde code.
Bij zestien blokken onder elkaar bleef de dia keurig binnen zijn vak met een
letter van 3,7 px: geen overloop, geen enkele test die klaagde, en toch een
rij streepjes in plaats van een menu. Een dia die onleesbaar is, is niet
minder stuk dan een dia die overloopt.

Elke indeling houdt nu een ondergrens aan de lettergrootte. Wat daarboven
niet meer past, maakt plaats voor een telblok '+n' — een zichtbaar tekort in
plaats van een stilzwijgend tekort, en meteen het teken dat het menu te vol
is voor deze vorm (het raster kan er de meeste kwijt, categorieën delen een
lang menu op). In het bestand verandert er niets.

Hoeveel er passen wordt afgeteld, niet geschat: de indeling roept dezelfde
functie aan die de kaart straks tekent. Daarvoor zijn de maten van kaart en
schijf — rand, marge, labelverhouding — naar menu_blocks.dart verhuisd. Een
tweede rekensom die de eerste voorspelt, loopt er stil uit.

De HTML-export doet hetzelfde. Daar won de ondergrens per rij het van het
hoogtebudget: zestien rijen van 56 px in een vak van 560, en de dia werd twee
schermen hoog.

Onderweg: de vloer waaronder de uitleg wegvalt lag te hoog (11,6 px op een
1280-dia is prima leesbaar), waardoor de uitleg tussen vijf en zes blokken
verdween op een verschil van een halve procent. En lange woorden in een
exportschijf braken niet af.

De proef meet de werkelijk getekende lettergrootte, want 'loopt niet over',
'titel even groot' en 'rechthoeken overlappen niet' zien dit geen van drieën.
Een keuzemenublok — of de sprong-uit van een dia — wijst naar het anker van
een andere dia. Verwijder of hernoem je die dia, dan valt de presentator
stil terug op de gewone volgorde: de knop doet niets, en dat merk je pas op
het podium. De kwaliteitscontrole zweeg erover.

Deckbreed, naast de bestaande split-run-controle en om dezelfde reden buiten
de per-dia-memo: of een sprong ergens uitkomt hangt af van de ándere dia's.
De melding noemt het label van het blok, want dat is wat de auteur op zijn
dia ziet staan.

Het hoofdbestand liep hierdoor tegen het harde plafond van 1000 regels; de
losse inhoudscontroles verhuizen verbatim naar een part-bestand.
Alle drie de plaatshouders in de renderer — geen afbeelding, bestand niet
gevonden, online media geblokkeerd — stonden op vaste lichtgrijze kleuren.
Keurig gewogen tegen een wítte dia (#780), maar op een donker thema een fel
lichtgrijs blok midden in het beeld. De zesde ontsnappingsroute langs de
themaregel, en hij raakt élk diatype met een afbeelding.

Vlak, tekst en pictogram mengen nu uit de tekst- en achtergrondkleur van de
dia, met dezelfde verhoudingen als voorheen: op de oude witte referentie
komt er bijna exact hetzelfde uit, en op een donkere dia halen ze dus
dezelfde contrastvloer.

De kleuren reizen mee in de link-scope en niet als parameter, om de reden
die daar al bij twee andere velden staat: er zijn negen aanroepplekken van
`_resolvedImage`, en de tiende vergeet hem.
docs: leesbaarheidsvloer, sprongwaarschuwing en de themavolgende plaatshouder
All checks were successful
scans / scans (pull_request) Successful in 1m56s
static-gate / static-gate (pull_request) Successful in 4m46s
17e6fced60
De USER_GUIDE legt uit wat een '+n'-blokje betekent en wat je eraan doet
(raster kiezen of categorieën gebruiken), en zegt er nu bij dat een sprong
naar een verwijderde dia wordt gemeld in plaats van stil te blijven.
brenno merged commit dd673d41cf into main 2026-08-18 08:37:01 +00:00
Sign in to join this conversation.
No description provided.