Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
4 changed files with 147 additions and 21 deletions
Showing only changes of commit fcb117eb54 - Show all commits

View file

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

View file

@ -768,15 +768,12 @@
} }
.lg-snd.off{opacity:.55;} .lg-snd.off{opacity:.55;}
/* small digit blocks/slots for the maths widgets */ /* 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 .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;} .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 .ghost{font-size:22px; height:26px;}
.slot.sm .slot-dots{margin-bottom:6px;} .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-slots{display:flex; gap:8px; justify-content:center;}
.mt-tray{gap:8px; padding:10px 8px; min-height:96px;} .mt-tray{gap:8px; padding:10px 8px; min-height:96px;}
.mt.blocks .mt-a,.mt.blocks .mt-pad{display:none;} .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-pic img{width:80px; height:80px; object-fit:contain; border-radius:12px; vertical-align:middle;}
.lg-slots{display:flex; gap:10px;} .lg-slots{display:flex; gap:10px;}
.slot{ .slot{
--dot:9px;
width:64px; height:98px; border-radius:12px; position:relative; width:64px; height:98px; border-radius:12px; position:relative;
background:#e9eef5; border:2.5px dashed #b9c7d9; background:#e9eef5; border:2.5px dashed #b9c7d9;
display:flex; flex-direction:column; align-items:center; justify-content:flex-start; 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);} 40%{transform:translateX(7px);} 60%{transform:translateX(-5px);} 80%{transform:translateX(5px);}
} }
.blok{ .blok{
--dot:9px;
width:64px; height:98px; border-radius:12px; cursor:grab; touch-action:none; width:64px; height:98px; border-radius:12px; cursor:grab; touch-action:none;
display:flex; flex-direction:column; align-items:center; padding-top:6px; display:flex; flex-direction:column; align-items:center; padding-top:6px;
background:linear-gradient(#fdf3dd,var(--wood)); border:2.5px solid var(--wood-d); 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 .bl{font-size:34px; font-weight:900; height:44px; display:flex; align-items:center;}
.blok.vowel .bl{color:var(--red);} .blok.vowel .bl{color:var(--red);}
.blok.cons .bl{color:var(--blue);} .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.in-slot{cursor:pointer; animation:pop .3s;}
.blok.dragging{opacity:.25;} .blok.dragging{opacity:.25;}
@keyframes pop{0%{transform:scale(.6);} 60%{transform:scale(1.12);} 100%{transform:scale(1);}} @keyframes pop{0%{transform:scale(.6);} 60%{transform:scale(1.12);} 100%{transform:scale(1);}}
.slot .slot-dots{margin-top:auto; margin-bottom:9px;} .slot .slot-dots{margin-top:auto; margin-bottom:8px;}
.dots{display:grid; grid-template-columns:repeat(2,9px); grid-template-rows:repeat(3,9px); gap:3px;} /* Stippenraster (braillepatroon, zie dotGrid() in data.js). De maat komt uit
.dots i{width:9px; height:9px; border-radius:50%; background:transparent;} éé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.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);} .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.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{ .lg-tray{
display:flex; gap:12px; justify-content:center; flex-wrap:wrap; 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; 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-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-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-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-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} .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}} @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-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-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-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 .ghost{height:24px;font-size:19px}
.world-letter-slot.slot.sm .slot-dots{margin-bottom:4px} .world-letter-slot.slot.sm .slot-dots{margin-bottom:4px}
.world-letter-block.blok.sm .bl{height:26px;font-size:22px} .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-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.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-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-digit-block.blok,.clock-digit-slot.slot.sm{--dot: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{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-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} .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}} @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-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-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-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-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} .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}} @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,.mt,.dc,.np,.timer,.nlw,.sb{padding:6px}
.lg-cards{gap:3px} .lg-cards{gap:3px}
.lg-cards button,.lg-snd,.lg-mode{width:32px;height:34px} .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 .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-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-slot.slot.sm,.money-digit-slot.slot.sm,.place-digit-slot.slot.sm{width:39px}
.clock-digit-block.blok,.money-digit-block.blok,.place-digit-block.blok{width:34px;height:52px;min-height:52px} /* 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{ .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} 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} .clock-type-display,.money-type-display{min-width:0;width:100%;font-size:30px}
.world-answers{grid-template-columns:1fr} .world-answers{grid-template-columns:1fr}
.world-visual.mixed{gap:5px} .world-visual.mixed{gap:5px}

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* 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. */ 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 /* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte
achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk
zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en 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`);
});
});