diff --git a/VERSION b/VERSION index acfd48e..555d7ba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.63-beta +0.4.64-beta diff --git a/public/css/teach.css b/public/css/teach.css index cb6adbf..41041ec 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -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} diff --git a/public/js/core.js b/public/js/core.js index 2a82781..538cf11 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -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 diff --git a/test/dot-grid.test.js b/test/dot-grid.test.js new file mode 100644 index 0000000..df660d1 --- /dev/null +++ b/test/dot-grid.test.js @@ -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`); + }); +});