fix: lijn het stippenpatroon uit binnen elk blokformaat (v0.4.64-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 50s

Gewijzigde functionaliteiten:

- De open stippen in de doelvakken stonden links-boven in hun rastercel in
  plaats van gecentreerd, waardoor het patroon scheef oogde; ze staan nu op
  dezelfde as als de gevulde stippen.

- De stippen van de geldblokken en van alle cijferblokken in een smalgemaakte
  widget staken onder het blok uit. De stipmaat komt nu uit een variabele die
  elk bloktype naast zijn eigen hoogte zet, in plaats van uit losse
  hardgecodeerde regels per widget.

- De smalle-widget-weergave verkleint alleen nog de breedte; blokhoogtes
  blijven gelijk, want juist het verkleinen van de hoogte duwde het raster naar
  buiten. Letterblokken krijgen daar een passend kleiner cijfer/letterformaat.

- Nieuw vangnet: het raster houdt altijd zijn maat en het label geeft mee als
  een blokformaat krap is, zodat een nieuw blokformaat dit niet opnieuw kan
  veroorzaken.

Tests: 196 geslaagd (4 nieuw in test/dot-grid.test.js; die vier vallen ook
daadwerkelijk om op de oude CSS). Daarnaast in de browser opgemeten voor alle
zeven blokcontexten, bij normale en smalle widgetbreedte, plus screenshots en
een controle in de echte leerlingweergave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126hjBPD2ggZ2LEoDYZSW7S
This commit is contained in:
Ramon 2026-07-26 08:13:02 +02:00
parent 50b9f32e91
commit fcb117eb54
4 changed files with 147 additions and 21 deletions

View file

@ -1 +1 @@
0.4.63-beta
0.4.64-beta

View file

@ -768,15 +768,12 @@
}
.lg-snd.off{opacity:.55;}
/* small digit blocks/slots for the maths widgets */
.blok.sm{width:46px; height:74px; border-radius:10px; padding-top:4px;}
.blok.sm{--dot:7px; width:46px; height:74px; border-radius:10px; padding-top:4px;}
.blok.sm .bl{font-size:24px; height:30px; color:var(--purple);}
.blok.sm .dots,.slot.sm .dots{grid-template-columns:repeat(2,7px); grid-template-rows:repeat(3,7px); gap:2.5px;}
.blok.sm .dots i,.slot.sm .dots i{width:7px; height:7px;}
.blok.sm .blok-dots{margin-bottom:7px;}
.slot.sm{width:46px; height:74px; padding-top:4px;}
.slot.sm{--dot:7px; width:46px; height:74px; padding-top:4px;}
.slot.sm .ghost{font-size:22px; height:26px;}
.slot.sm .slot-dots{margin-bottom:6px;}
.slot.sm .dots i.off{width:6px; height:6px;}
.mt-slots{display:flex; gap:8px; justify-content:center;}
.mt-tray{gap:8px; padding:10px 8px; min-height:96px;}
.mt.blocks .mt-a,.mt.blocks .mt-pad{display:none;}
@ -827,6 +824,7 @@
.lg-pic img{width:80px; height:80px; object-fit:contain; border-radius:12px; vertical-align:middle;}
.lg-slots{display:flex; gap:10px;}
.slot{
--dot:9px;
width:64px; height:98px; border-radius:12px; position:relative;
background:#e9eef5; border:2.5px dashed #b9c7d9;
display:flex; flex-direction:column; align-items:center; justify-content:flex-start;
@ -842,6 +840,7 @@
40%{transform:translateX(7px);} 60%{transform:translateX(-5px);} 80%{transform:translateX(5px);}
}
.blok{
--dot:9px;
width:64px; height:98px; border-radius:12px; cursor:grab; touch-action:none;
display:flex; flex-direction:column; align-items:center; padding-top:6px;
background:linear-gradient(#fdf3dd,var(--wood)); border:2.5px solid var(--wood-d);
@ -852,17 +851,30 @@
.blok .bl{font-size:34px; font-weight:900; height:44px; display:flex; align-items:center;}
.blok.vowel .bl{color:var(--red);}
.blok.cons .bl{color:var(--blue);}
.blok .blok-dots{margin-top:auto; margin-bottom:10px;}
.blok .blok-dots{margin-top:auto; margin-bottom:8px;}
/* Vangnet: het stippenraster houdt altijd zijn maat, het label geeft mee als
een blokformaat krap is. Zonder min-height:0 kan een kolom-flexitem niet
onder zijn inhoud krimpen en werd het raster het blok uit geduwd. */
.blok .bl,.slot .ghost{min-height:0;}
.blok .blok-dots,.slot .slot-dots{flex:none;}
.blok.in-slot{cursor:pointer; animation:pop .3s;}
.blok.dragging{opacity:.25;}
@keyframes pop{0%{transform:scale(.6);} 60%{transform:scale(1.12);} 100%{transform:scale(1);}}
.slot .slot-dots{margin-top:auto; margin-bottom:9px;}
.dots{display:grid; grid-template-columns:repeat(2,9px); grid-template-rows:repeat(3,9px); gap:3px;}
.dots i{width:9px; height:9px; border-radius:50%; background:transparent;}
.slot .slot-dots{margin-top:auto; margin-bottom:8px;}
/* Stippenraster (braillepatroon, zie dotGrid() in data.js). De maat komt uit
één variabele --dot die elk bloktype naast zijn eigen hoogte zet; zo kan een
nieuw blokformaat niet meer per ongeluk het raster van een ander formaat
erven (dat gebeurde bij de geldblokken en in de smalle-widget-query, waar de
stippen onder het blok uit staken). place-self houdt de kleinere "off"-stip
gecentreerd in zijn cel - anders staat die links-boven en oogt het patroon
scheef. Vuistregel: --dot nooit onder 6px (leesbaarheid); past het niet,
dan geeft de labelhoogte of de blokhoogte mee, niet de stip. */
.dots{display:grid; grid-template-columns:repeat(2,var(--dot)); grid-template-rows:repeat(3,var(--dot)); gap:calc(var(--dot)/3);}
.dots i{width:var(--dot); height:var(--dot); border-radius:50%; background:transparent; place-self:center;}
.blok .dots i.on{background:#b98a3c; box-shadow:inset 0 -1.5px 2px rgba(0,0,0,.35), 0 1px 0 rgba(255,255,255,.5);}
.blok .dots i.off{background:rgba(0,0,0,.05);}
.slot .dots i.on{background:#aebcce; box-shadow:inset 0 2px 3px rgba(30,50,80,.4);}
.slot .dots i.off{background:transparent; border:1px dotted #ccd7e4; width:7px; height:7px;}
.slot .dots i.off{background:transparent; border:1px dotted #ccd7e4; width:calc(var(--dot) - 2px); height:calc(var(--dot) - 2px);}
.lg-tray{
display:flex; gap:12px; justify-content:center; flex-wrap:wrap;
background:var(--surface-2); border-radius:16px; padding:14px 12px; min-height:130px; flex:none;
@ -1885,7 +1897,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.money-tray-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}.money-tray-wrap>b{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.4px}.money-cash-tray{width:100%;display:flex;align-items:center;justify-content:center;gap:8px;flex-wrap:wrap;padding:11px;border:1px solid var(--line);border-radius:16px;background:var(--surface-2)}
.money-count-task{width:100%;min-height:128px;display:grid;place-items:center;padding:14px;border-radius:17px;background:linear-gradient(145deg,#eef7e9,var(--surface-2));border:1px solid var(--line)}.money-count-task .money-piece{transform:rotate(var(--money-tilt,0deg))}
.money-shop-task{width:100%;display:grid;grid-template-columns:minmax(0,1fr) minmax(150px,.8fr);gap:12px}.money-product,.money-paid{min-height:150px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:12px;border:1px solid var(--line);border-radius:17px;background:var(--surface-2)}.money-product-icon{font-size:clamp(52px,8vw,70px);line-height:1;filter:drop-shadow(0 5px 5px rgba(30,45,70,.2))}.money-product-name{max-width:100%;font-size:17px;font-weight:950;text-align:center}.money-product>b,.money-paid>b{color:var(--muted);font-size:12px;text-transform:uppercase}.money-product .money-task-amount{font-size:37px}.money-product .money-task-words{font-size:19px}.money-paid>output{font-size:31px;font-weight:950;color:var(--blue)}
.money-digit-answer,.money-type-answer{width:min(100%,300px);display:flex;flex-direction:column;align-items:center;gap:10px}.money-digit-answer>p,.money-type-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.money-answer-slots{display:flex;align-items:center;justify-content:center;gap:6px}.money-answer-slots>strong{font-size:29px}.money-digit-slot.slot.sm{width:43px;height:64px;padding-top:1px}.money-digit-slot.slot.sm .ghost{height:24px;font-size:20px}.money-digit-slot.slot.sm .slot-dots{margin-bottom:3px}.money-digit-slot.filled{padding:0}.money-digit-slot.fixed{border-style:solid;background:var(--surface-2);opacity:.72}.money-digit-block.blok{width:39px;height:58px;min-height:58px;padding-top:2px;cursor:default}.money-digit-block.blok .bl{height:28px;font-size:25px;color:var(--purple)}.money-digit-block.blok .blok-dots{margin-bottom:4px}.money-digit-tray.lg-tray{display:grid;grid-template-columns:repeat(5,39px);gap:7px;min-height:0;padding:11px 14px}.money-digit-tray .money-drag-digit{cursor:grab}.money-digit-tray .money-drag-digit:active{cursor:grabbing}.money-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
.money-digit-answer,.money-type-answer{width:min(100%,300px);display:flex;flex-direction:column;align-items:center;gap:10px}.money-digit-answer>p,.money-type-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.money-answer-slots{display:flex;align-items:center;justify-content:center;gap:6px}.money-answer-slots>strong{font-size:29px}.money-digit-slot.slot.sm{width:43px;height:64px;padding-top:1px}.money-digit-slot.slot.sm .ghost{height:24px;font-size:20px}.money-digit-slot.slot.sm .slot-dots{margin-bottom:3px}.money-digit-slot.filled{padding:0}.money-digit-slot.fixed{border-style:solid;background:var(--surface-2);opacity:.72}.money-digit-block.blok,.money-digit-slot.slot.sm{--dot:6px}.money-digit-block.blok{width:39px;height:62px;min-height:62px;padding-top:2px;cursor:default}.money-digit-block.blok .bl{height:26px;font-size:25px;color:var(--purple)}.money-digit-block.blok .blok-dots{margin-bottom:3px}.money-digit-tray.lg-tray{display:grid;grid-template-columns:repeat(5,39px);gap:7px;min-height:0;padding:11px 14px}.money-digit-tray .money-drag-digit{cursor:grab}.money-digit-tray .money-drag-digit:active{cursor:grabbing}.money-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
.money-type-display{min-width:190px;padding:9px 14px;border:3px dashed var(--line);border-radius:14px;background:var(--surface-2);font-size:34px;font-weight:950;letter-spacing:2px;color:var(--green)}.money-keypad{width:100%;display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.money-keypad .tbtn{min-width:0;padding:0;font-size:20px}
.money-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.money-feedback.ok{color:var(--green)}.money-feedback.bad{color:var(--red)}.money-actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.money-actions .tbtn:not(.ghost){background:var(--green);color:#fff}
@media(max-width:520px){.money-task-card{gap:9px;padding:10px}.money-level-control>span{display:none}.money-top{top:-10px;gap:5px}.money-shop-task{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:7px}.money-product,.money-paid{min-height:112px;padding:8px 6px;gap:4px}.money-product-icon{font-size:46px}.money-product-name{font-size:14px}.money-product .money-task-amount{font-size:32px}.money-product .money-task-words{font-size:16px}.money-paid>output{font-size:26px}.money-cash-tray{gap:6px;padding:9px}}
@ -1972,7 +1984,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.world-spell-answer{width:100%;display:flex;flex-direction:column;align-items:center;gap:12px}
.world-spell-word{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;max-width:100%}
.world-spell-gap{width:14px;flex:none}
.world-letter-slot.slot.sm,.world-letter-block.blok.sm{width:40px;height:60px;padding-top:3px}
.world-letter-slot.slot.sm,.world-letter-block.blok.sm{--dot:6px;width:40px;height:62px;padding-top:3px}
.world-letter-slot.slot.sm .ghost{height:24px;font-size:19px}
.world-letter-slot.slot.sm .slot-dots{margin-bottom:4px}
.world-letter-block.blok.sm .bl{height:26px;font-size:22px}
@ -2000,8 +2012,9 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo
.clock-hand{position:absolute;z-index:4;left:calc(50% - 5px);bottom:50%;width:10px;border-radius:9px;transform-origin:50% 100%;background:var(--ink);cursor:grab;touch-action:none}.clock-hand:active{cursor:grabbing}.clock-hour{height:28%}.clock-min{left:calc(50% - 4px);width:8px;height:39%;background:var(--pink)}.clock-pin{position:absolute;z-index:5;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;border:3px solid var(--surface);border-radius:50%;background:var(--pink);box-shadow:0 1px 4px rgba(0,0,0,.28)}
.clock.interactive{touch-action:none;user-select:none;outline:4px solid transparent;transition:outline-color .15s,transform .15s}.clock.interactive:hover,.clock.interactive:focus-within{outline-color:var(--accent-soft)}
.clock-time-blocks{display:flex;align-items:center;justify-content:center;gap:5px;margin:0;padding:6px;border:0;border-radius:14px;background:transparent;color:var(--ink);font:inherit}.clock-time-blocks:is(button){cursor:pointer}.clock-time-blocks:is(button):hover,.clock-time-blocks:is(button):focus-visible{background:var(--accent-soft);outline:2px solid var(--accent)}.clock-time-blocks>strong{font-size:30px}
.clock-digit-block.blok{width:42px;height:62px;min-height:62px;padding-top:3px;cursor:default}.clock-digit-block.blok .bl{height:31px;font-size:27px;color:var(--purple)}.clock-digit-block.blok .blok-dots{margin-bottom:5px}.clock-digit-block.blok .dots,.clock-digit-slot .dots{grid-template-columns:repeat(2,6px);grid-template-rows:repeat(3,6px);gap:2px}.clock-digit-block.blok .dots i,.clock-digit-slot .dots i{width:6px;height:6px}
.clock-live-answer{padding:6px 12px;border:2px dashed var(--line);border-radius:16px;background:var(--surface-2)}.clock-read-layout{width:100%;display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap}.clock-read-layout>.clock-assignment{flex:1;min-width:190px}.clock-read-layout .clock{--clock-size:180px}.clock-block-answer{width:min(100%,250px);display:flex;flex-direction:column;align-items:center;gap:10px}.clock-block-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.clock-answer-slots{display:flex;align-items:center;justify-content:center;gap:7px}.clock-digit-slot.slot.sm{width:46px;height:66px;padding-top:2px}.clock-digit-slot.slot.sm .ghost{height:25px;font-size:20px}.clock-digit-slot.slot.sm .slot-dots{margin-bottom:4px}.clock-digit-slot.filled{padding:0}.clock-slot-colon{font-size:30px;margin:0 2px}.clock-block-tray.lg-tray{display:grid;grid-template-columns:repeat(5,38px);gap:7px;min-height:0;padding:11px 14px}.clock-block-tray .clock-drag-block{width:38px;height:56px;min-height:56px;cursor:grab}.clock-block-tray .clock-drag-block:active{cursor:grabbing}.clock-block-tray .clock-drag-block:focus-visible{outline:3px solid var(--accent);outline-offset:2px}.clock-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
.clock-digit-block.blok,.clock-digit-slot.slot.sm{--dot:6px}
.clock-digit-block.blok{width:42px;height:66px;min-height:66px;padding-top:3px;cursor:default}.clock-digit-block.blok .bl{height:29px;font-size:27px;color:var(--purple)}.clock-digit-block.blok .blok-dots{margin-bottom:4px}
.clock-live-answer{padding:6px 12px;border:2px dashed var(--line);border-radius:16px;background:var(--surface-2)}.clock-read-layout{width:100%;display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap}.clock-read-layout>.clock-assignment{flex:1;min-width:190px}.clock-read-layout .clock{--clock-size:180px}.clock-block-answer{width:min(100%,250px);display:flex;flex-direction:column;align-items:center;gap:10px}.clock-block-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.clock-answer-slots{display:flex;align-items:center;justify-content:center;gap:7px}.clock-digit-slot.slot.sm{width:46px;height:66px;padding-top:2px}.clock-digit-slot.slot.sm .ghost{height:25px;font-size:20px}.clock-digit-slot.slot.sm .slot-dots{margin-bottom:4px}.clock-digit-slot.filled{padding:0}.clock-slot-colon{font-size:30px;margin:0 2px}.clock-block-tray.lg-tray{display:grid;grid-template-columns:repeat(5,38px);gap:7px;min-height:0;padding:11px 14px}.clock-block-tray .clock-drag-block{width:38px;height:66px;min-height:66px;cursor:grab}.clock-block-tray .clock-drag-block:active{cursor:grabbing}.clock-block-tray .clock-drag-block:focus-visible{outline:3px solid var(--accent);outline-offset:2px}.clock-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
.clock-choices{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}.clock-choices.wood .clock-time-blocks{border:1px solid var(--line);background:var(--surface-2)}.clock-word-choice{min-height:48px;border:2px solid var(--wood-d)!important;background:linear-gradient(#fdf3dd,var(--wood))!important;color:var(--ink)!important;box-shadow:0 3px 0 var(--wood-d)}.clock-digital-choice{min-width:118px;min-height:58px;font-size:28px!important;color:var(--blue)!important}.clock-type-answer{width:min(100%,250px);display:flex;flex-direction:column;align-items:center;gap:9px}.clock-type-answer>p{margin:0;color:var(--muted);font-weight:800;text-align:center}.clock-type-display{min-width:180px;padding:9px 14px;border:3px dashed var(--line);border-radius:14px;background:var(--surface-2);font-size:34px;font-weight:950;letter-spacing:3px;color:var(--blue)}.clock-keypad{width:100%;display:grid;grid-template-columns:repeat(4,1fr);gap:7px}.clock-keypad .tbtn{min-width:0;padding:0;font-size:20px}.clock-keypad .ok{background:var(--green);color:#fff}
.clock-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.clock-feedback.ok{color:var(--green)}.clock-feedback.bad{color:var(--red)}.clock-actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.clock-actions .tbtn:not(.ghost){background:var(--green);color:#fff}.clock-drag-hint{color:var(--muted);font-weight:800;text-align:center}
@media(max-width:480px){.clock-task-card{padding:12px}.clock-level-control>span{display:none}.clock-top{gap:5px}.clock{--clock-size:min(210px,68vw)}.clock-target{min-height:72px}.clock-block-tray.lg-tray{gap:7px;padding:9px}}
@ -2024,7 +2037,8 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo
.place-digit-slot.slot.sm{width:46px;height:66px;padding-top:2px}.place-digit-slot.slot.sm .ghost{height:25px;font-size:20px}.place-digit-slot.slot.sm .slot-dots{margin-bottom:4px}.place-digit-slot.filled{padding:0}
.place-digit-tray.lg-tray{display:grid;grid-template-columns:repeat(5,38px);gap:7px;min-height:0;padding:11px 14px}.place-digit-tray .place-drag-block{width:38px;height:56px;min-height:56px;cursor:grab}.place-digit-tray .place-drag-block:active{cursor:grabbing}.place-digit-tray .place-drag-block:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.place-digit-slot.droptarget{border-color:var(--green);background:#e3f6ea;transform:scale(1.06)}
.place-digit-block.blok{width:42px;height:62px;min-height:62px;padding-top:3px;cursor:default}.place-digit-block.blok .bl{height:31px;font-size:27px;color:var(--purple)}.place-digit-block.blok .blok-dots{margin-bottom:5px}.place-digit-block.blok .dots,.place-digit-slot .dots{grid-template-columns:repeat(2,6px);grid-template-rows:repeat(3,6px);gap:2px}.place-digit-block.blok .dots i,.place-digit-slot .dots i{width:6px;height:6px}
.place-digit-block.blok,.place-digit-slot.slot.sm{--dot:6px}
.place-digit-block.blok{width:42px;height:66px;min-height:66px;padding-top:3px;cursor:default}.place-digit-block.blok .bl{height:29px;font-size:27px;color:var(--purple)}.place-digit-block.blok .blok-dots{margin-bottom:4px}
.place-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.place-feedback.ok{color:var(--green)}.place-feedback.bad{color:var(--red)}
.place-actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.place-actions .tbtn:not(.ghost){background:var(--green);color:#fff}
@media(max-width:480px){.place-task-card{padding:12px}.place-level-control>span{display:none}.place-top{gap:5px}.place-digit-tray.lg-tray{gap:7px;padding:9px}}
@ -2239,14 +2253,20 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo
.lg,.mt,.dc,.np,.timer,.nlw,.sb{padding:6px}
.lg-cards{gap:3px}
.lg-cards button,.lg-snd,.lg-mode{width:32px;height:34px}
/* smallere blokken: label én stipmaat moeten mee, anders steekt het raster
onder het blok uit (dat gebeurde hier met de cijferblokken) */
.lg .slot:not(.clock-digit-slot):not(.money-digit-slot):not(.place-digit-slot),
.lg .blok:not(.clock-digit-block):not(.money-digit-block):not(.place-digit-block){width:52px;height:82px}
.lg .blok:not(.clock-digit-block):not(.money-digit-block):not(.place-digit-block){--dot:7px;width:52px;height:82px}
.lg .blok:not(.clock-digit-block):not(.money-digit-block):not(.place-digit-block) .bl{font-size:28px;height:36px}
.lg .slot:not(.clock-digit-slot):not(.money-digit-slot):not(.place-digit-slot) .ghost{font-size:24px;height:32px}
.clock-answer-slots,.money-answer-slots,.place-answer-slots{gap:4px}
.clock-digit-slot.slot.sm,.money-digit-slot.slot.sm,.place-digit-slot.slot.sm{width:39px;height:60px}
.clock-digit-block.blok,.money-digit-block.blok,.place-digit-block.blok{width:34px;height:52px;min-height:52px}
.clock-digit-slot.slot.sm,.money-digit-slot.slot.sm,.place-digit-slot.slot.sm{width:39px}
/* de blokhoogte geeft mee (de widget is te smal, niet te laag), zodat de
cijfers leesbaar blijven en het raster er alsnog binnen past */
.clock-digit-block.blok,.money-digit-block.blok,.place-digit-block.blok{width:34px}
.clock-block-tray.lg-tray,.money-digit-tray.lg-tray,.place-digit-tray.lg-tray{
grid-template-columns:repeat(5,34px);gap:4px;padding:7px}
.clock-block-tray .clock-drag-block,.place-digit-tray .place-drag-block{width:34px;height:52px;min-height:52px}
.clock-block-tray .clock-drag-block,.place-digit-tray .place-drag-block{width:34px}
.clock-type-display,.money-type-display{min-width:0;width:100%;font-size:30px}
.world-answers{grid-template-columns:1fr}
.world-visual.mixed{gap:5px}

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.63-beta";
const VERSION = "0.4.64-beta";
/* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte
achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk
zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en

106
test/dot-grid.test.js Normal file
View file

@ -0,0 +1,106 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
/* Stippenraster (braillepatroon uit dotGrid(), data.js). Het ging mis doordat de
stipmaat per bloktype hardgecodeerd was terwijl blokhoogte, labelhoogte en
marges óók per type verschillen: de geldblokken erfden de maat van een ander
formaat en de smalle-widget-query verkleinde alleen de blokken. Gevolg: de
stippen staken onder het blok uit. Deze test bewaakt de regels die dat
voorkomen; de volledige geometrie is daarnaast in de browser opgemeten. */
const CSS = await readFile('public/css/teach.css', 'utf8');
/* alle regels als [selector, declaraties]; commentaar eruit want daar staan
voorbeelden in die anders als declaratie meetellen */
function rules(css) {
const clean = css.replace(/\/\*[\s\S]*?\*\//g, '');
const out = [];
for (const chunk of clean.split('}')) {
const i = chunk.lastIndexOf('{');
if (i < 0) continue;
const selector = chunk.slice(0, i).split('{').pop().trim();
if (!selector || selector.startsWith('@')) continue;
out.push([selector, chunk.slice(i + 1)]);
}
return out;
}
const RULES = rules(CSS);
const declares = (decls, prop) => new RegExp(`(^|;|\\s)${prop}\\s*:`).test(decls);
/* het bloktype in een selector: .money-digit-block, .blok.sm, .blok, ... */
function contextTokens(selector) {
return selector.split(',')
.map((part) => part.trim())
.filter((part) => /\.(blok|slot)\b/.test(part) && !/\s\.(bl|ghost|dots|blok-dots|slot-dots)\b/.test(part))
.map((part) => (part.match(/\.[a-z0-9-]*(?:block|slot|blok)\b/g) || []).join(''));
}
test('elk bloktype dat een eigen hoogte krijgt, zet ook zijn eigen stipmaat', () => {
const withDot = new Set();
RULES.forEach(([selector, decls]) => {
if (declares(decls, '--dot')) contextTokens(selector).forEach((t) => withDot.add(t));
});
const missing = [];
RULES.forEach(([selector, decls]) => {
if (!declares(decls, 'height')) return;
contextTokens(selector).forEach((token) => {
if (!token) return;
// een variant erft de maat van zijn basis (.blok/.slot) als die het zet
const base = token.includes('slot') ? '.slot' : '.blok';
if (!withDot.has(token) && !withDot.has(base)) missing.push(`${selector} (${token})`);
});
});
assert.deepEqual(missing, [], 'deze regels zetten een blokhoogte zonder dat er ergens een --dot voor dat type bestaat');
assert.ok(withDot.has('.blok') && withDot.has('.slot'), 'basisblok en -slot zetten --dot');
});
test('de stippen zijn nooit kleiner dan 6px: het patroon moet leesbaar blijven', () => {
const values = [...CSS.matchAll(/--dot\s*:\s*([\d.]+)px/g)].map((m) => Number(m[1]));
assert.ok(values.length >= 5, `verwacht meerdere stipmaten, gevonden: ${values.length}`);
values.forEach((v) => assert.ok(v >= 6, `stipmaat ${v}px is onder de leesbaarheidsgrens`));
});
test('geen enkel bloktype heeft nog een eigen hardgecodeerd stippenraster', () => {
const hardcoded = RULES.filter(([selector, decls]) =>
/\.dots\b/.test(selector)
&& /(grid-template-(columns|rows)\s*:[^;]*\d+px|(?:^|;|\s)(width|height)\s*:\s*\d+px)/.test(decls));
assert.deepEqual(hardcoded.map(([s]) => s), [],
'stipmaten horen via var(--dot) te lopen, anders loopt een nieuw blokformaat weer uit de pas');
});
/* Narekening van de verticale begroting per bloktype. De blokrand telt mee
(border-box), dus: rand + padding-top + labelhoogte + rasterhoogte +
ondermarge moet binnen de blokhoogte passen. Rasterhoogte = 3 stippen + 2
tussenruimtes van --dot/3. */
const CONTEXTS = [
{ naam: 'letterblok', maat: '.blok', label: '.blok .bl', marge: '.blok .blok-dots', dot: '.blok' },
{ naam: 'letterslot', maat: '.slot', label: '.slot .ghost', marge: '.slot .slot-dots', dot: '.slot' },
{ naam: 'rekenblok', maat: '.blok.sm', label: '.blok.sm .bl', marge: '.blok.sm .blok-dots', dot: '.blok.sm' },
{ naam: 'rekenslot', maat: '.slot.sm', label: '.slot.sm .ghost', marge: '.slot.sm .slot-dots', dot: '.slot.sm' },
];
function value(selectorPart, prop) {
let found = null;
RULES.forEach(([selector, decls]) => {
if (!selector.split(',').some((part) => part.trim() === selectorPart)) return;
const m = decls.match(new RegExp(`(?:^|;|\\s)${prop}\\s*:\\s*([\\d.]+)px`));
if (m) found = Number(m[1]);
});
return found;
}
test('label, stippenraster en marges passen samen binnen elk basisblok', () => {
CONTEXTS.forEach((ctx) => {
const hoogte = value(ctx.maat, 'height');
const padding = value(ctx.maat, 'padding-top') ?? 0;
const rand = (value(ctx.maat, 'border') ?? 2.5) * 2;
const label = value(ctx.label, 'height');
const marge = value(ctx.marge, 'margin-bottom');
const dot = value(ctx.dot, '--dot');
assert.ok(hoogte && label && marge && dot, `${ctx.naam}: maten niet gevonden`);
const raster = 3 * dot + 2 * (dot / 3);
const nodig = padding + label + raster + marge + rand;
assert.ok(nodig <= hoogte,
`${ctx.naam}: ${nodig.toFixed(1)}px nodig in een blok van ${hoogte}px - de stippen steken eruit`);
});
});