diff --git a/VERSION b/VERSION index 6242a31..5c6f58f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.76-beta +0.3.77-beta diff --git a/public/css/teach.css b/public/css/teach.css index dfc3625..4000333 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -870,7 +870,8 @@ .ed-row.base{opacity:.65;} /* =========== Notes widget =========== */ .notes{flex:1; width:100%; min-height:0; border:none; resize:none; font:inherit; font-size:16px; - padding:14px; outline:none; background:#fffdf2; color:var(--ink);} + padding:16px; outline:none; background:transparent; color:#26354a; line-height:1.55;} + .notes.large{font-size:22px;} /* =========== Writing pad widget =========== */ .wp{flex:1; min-height:0; display:flex; flex-direction:column;} .wp-bar{ @@ -1134,16 +1135,17 @@ .die i.on{visibility:visible;} .dc-sum{font-size:22px; font-weight:900; min-height:28px;} /* =========== Traffic light widget =========== */ - .tl{flex:1; min-height:0; display:flex; align-items:center; justify-content:center; padding:14px;} - .tl-house{background:#22272f; border-radius:26px; padding:20px 18px; display:flex; flex-direction:column; gap:16px; box-shadow:0 10px 26px rgba(0,0,0,.35);} + .tl{flex:1; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; padding:14px;} + .tl-house{width:min(100%,390px); background:#22272f; border-radius:26px; padding:14px; display:flex; flex-direction:column; gap:9px; box-shadow:0 10px 26px rgba(0,0,0,.35);} .tl-lamp{ - width:100px; height:100px; border-radius:50%; border:none; cursor:pointer; + width:70px; height:70px; flex:none; border-radius:50%; border:4px solid rgba(255,255,255,.13); cursor:pointer; opacity:.22; transition:all .2s; padding:0; } .tl-lamp.red{background:#e33; color:#e33;} .tl-lamp.orange{background:#f5a623; color:#f5a623;} .tl-lamp.green{background:#2ecc71; color:#2ecc71;} .tl-lamp.on{opacity:1; box-shadow:0 0 46px currentColor;} + .tl-option{display:flex;align-items:center;gap:15px;color:#fff}.tl-option>span{display:flex;flex-direction:column;gap:2px}.tl-option strong{font-size:18px}.tl-option small{color:#cbd3df;font-size:13px;font-weight:700}.tl-status{min-height:25px;color:var(--ink);font-size:17px;font-weight:900;text-align:center} .tl-mini{padding:12px 10px; gap:10px; border-radius:20px;} .tl-mini .tl-lamp{width:62px; height:62px;} /* =========== Name picker widget =========== */ @@ -1164,6 +1166,7 @@ @keyframes npspin{0%,100%{transform:scale(1) rotate(-1deg);} 50%{transform:scale(1.04) rotate(1deg);}} .np-row{display:flex; gap:10px; align-items:center; flex:none;} .np-row label{font-size:13px; font-weight:700; color:var(--muted); display:flex; gap:5px; align-items:center; margin-left:auto;} + .np-progress{margin:-3px 0;color:var(--muted);font-size:12px;font-weight:900;text-align:center} /* gallery categories */ .gcat-title{font-size:13.5px; font-weight:800; color:var(--muted); margin:16px 0 8px; text-transform:uppercase; letter-spacing:.5px;} .gcat-title:first-child{margin-top:0;} @@ -1554,7 +1557,7 @@ body.hc #penbar .pen-current,body.hc #penbar .pen-popover{border:2px solid #000; .nlw-settings{display:flex; align-items:end; gap:8px; flex-wrap:wrap;} .nlw-settings label{font-size:11px; font-weight:800; color:var(--muted); display:flex; flex-direction:column; gap:3px;} .nlw-settings input,.nlw-settings select{width:72px; height:34px; border:1px solid var(--line); border-radius:9px; padding:0 8px; background:var(--surface); color:var(--ink); font:inherit;} -.nlw-new{margin-left:auto; min-height:34px;}.nlw-task{text-align:center; font-size:clamp(18px,3vw,27px); font-weight:900; color:var(--blue);} +.nlw-new{margin-left:auto; min-height:34px;}.nlw-playtop{display:flex;align-items:center;justify-content:center;gap:9px;flex-wrap:wrap}.nlw-task{text-align:center; font-size:clamp(18px,3vw,27px); font-weight:900; color:var(--blue);}.nlw-score{padding:5px 9px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:11px;font-weight:900} .nlw-scroll{flex:1; min-height:115px; overflow-x:auto; overflow-y:hidden; border-radius:14px; background:var(--surface-2); padding:20px 18px 8px;} .nlw-line{position:relative; min-width:max-content; height:88px; display:flex; align-items:center; padding:0 18px;} .nlw-line::before{content:""; position:absolute; height:4px; left:18px; right:18px; top:38px; border-radius:4px; background:var(--ink);} @@ -1578,7 +1581,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2 /* Educatieve widgets en compacte widgetbibliotheek */ .gallery-head,.gallery-tools,.gallery-data{display:flex;align-items:center;gap:8px}.gallery-head h2{flex:1}.gallery-tools{position:sticky;top:-24px;z-index:2;background:var(--surface);padding:6px 0 10px}.gallery-tools input{flex:1;height:42px;border:1px solid var(--line);border-radius:12px;padding:0 13px;background:var(--surface);color:var(--ink);font:inherit}.gallery-data{margin:8px 0 14px;flex-wrap:wrap}#galleryTemplates{display:flex;gap:7px;overflow-x:auto;padding:2px 0 8px}.gcat-title{margin:18px 0 8px;font-size:13px;font-weight:900;color:var(--muted);text-transform:uppercase}.gcard{position:relative}.gstar{position:absolute;right:7px;top:7px;border:0;background:transparent;color:var(--orange);font-size:22px;cursor:pointer}.gallery-empty,.edu-empty{text-align:center;color:var(--muted);padding:28px}.widget.locked .widget-rs{display:none}.widget.locked{outline:2px solid var(--accent-soft)}.widget.preview .widget-head{position:absolute;z-index:4;width:100%;opacity:0}.widget.preview:hover .widget-head,.widget.preview:focus-within .widget-head{opacity:1}.widget.preview .widget-rs{display:none}.widget.preview{border-color:transparent;box-shadow:none}.wpreview.on{background:var(--accent-soft);color:var(--accent-ink)} .edu{height:100%;display:flex;flex-direction:column;background:var(--surface);color:var(--ink)}.edu-settings{flex:none;background:var(--surface-2);border-bottom:1px solid var(--line)}.edu-settings summary{cursor:pointer;padding:9px 12px;font-weight:800;font-size:13px}.edu-controls{display:flex;align-items:end;gap:8px;flex-wrap:wrap;padding:0 12px 10px}.edu-controls label{display:flex;flex-direction:column;gap:3px;font-size:11px;font-weight:800;color:var(--muted)}.edu-controls input,.edu-controls select,.edu-controls textarea{min-height:34px;border:1px solid var(--line);border-radius:9px;padding:5px 8px;background:var(--surface);color:var(--ink);font:inherit}.edu-controls textarea{height:65px;resize:vertical}.edu-stage{flex:1;min-height:0;overflow:auto;padding:16px;display:flex;align-items:center;justify-content:center} -.fraction-wall{width:100%;display:flex;flex-direction:column;gap:5px}.fraction-wall>div{display:flex;gap:4px}.fraction-piece{flex:1;min-height:34px;border:2px solid hsl(var(--fraction-hue) 55% 43%);border-radius:5px;background:hsl(var(--fraction-hue) 75% 90%);font-weight:800;color:#20304a}.fraction-piece.selected{background:hsl(var(--fraction-hue) 70% 55%);color:#fff} +.fraction-game{width:min(100%,620px);display:flex;flex-direction:column;align-items:center;gap:9px}.fraction-summary{min-height:34px;padding:5px 13px;border-radius:999px;background:var(--surface-2);color:var(--purple);font-size:18px;font-weight:950}.fraction-wall{width:100%;display:flex;flex-direction:column;gap:5px}.fraction-wall>div{display:flex;gap:4px}.fraction-piece{flex:1;min-height:38px;border:2px solid hsl(var(--fraction-hue) 55% 43%);border-radius:7px;background:hsl(var(--fraction-hue) 75% 90%);font-weight:800;color:#20304a;cursor:pointer;transition:transform .12s,filter .12s}.fraction-piece:hover{filter:brightness(.96);transform:translateY(-1px)}.fraction-piece.selected{background:hsl(var(--fraction-hue) 70% 55%);color:#fff}.fraction-clear{align-self:flex-end;min-height:34px} .place-grid{display:grid;grid-template-columns:repeat(6,1fr);width:100%;border:1px solid var(--line);border-radius:14px;overflow:hidden}.place-grid div{text-align:center;border-right:1px solid var(--line)}.place-grid b{display:block;padding:8px;background:var(--surface-2);font-size:12px}.place-grid output{display:block;padding:18px 4px;font-size:clamp(25px,5vw,48px);font-weight:900;color:var(--blue)} .money-app{width:100%;min-height:100%;align-self:flex-start;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)} .money-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.money-level-control{display:flex;align-items:center;gap:6px}.money-level-control>span{color:var(--muted);font-size:12px;font-weight:900}.money-levels button{min-width:31px;padding:6px 9px;white-space:nowrap}.money-mode{width:36px;padding:0;font-size:18px}.money-score{margin-left:auto;padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900} @@ -1597,7 +1600,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2 .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}} .wordtypes{display:flex;gap:9px;flex-wrap:wrap;justify-content:center}.wordtypes button{border:0;border-radius:10px;padding:10px 13px;font:inherit;font-weight:900;background:var(--surface-2)}.wordtypes button[data-type=noun]{background:#dce9ff;color:#174c9b}.wordtypes button[data-type=verb]{background:#dcf7e7;color:#176b3a}.wordtypes button[data-type=adjective]{background:#fff0cf;color:#855b00}.wordtypes p{flex-basis:100%;text-align:center;color:var(--muted);font-size:12px} -.schedule{width:min(100%,450px);display:flex;flex-direction:column;gap:7px}.schedule>div{display:flex;align-items:center;gap:12px;padding:9px;background:var(--surface-2);border-radius:10px}.schedule time{font-weight:900;color:var(--blue)}.schedule span{flex:1;font-weight:800}.schedule .tbtn{height:30px;min-height:30px;padding:0 9px}.mood{display:flex;gap:8px;flex-wrap:wrap}.mood .tbtn{font-size:36px;height:auto;padding:12px}.mood span{display:block;font-size:14px}.poll{width:min(100%,440px);display:flex;flex-direction:column;gap:8px}.poll h3{text-align:center}.poll .tbtn{position:relative;overflow:hidden;text-align:left}.poll .tbtn i{position:absolute;inset:0 auto 0 0;background:var(--accent-soft);z-index:0}.poll .tbtn span{position:relative;float:right;font-weight:900}.poll .tbtn{isolation:isolate} +.schedule{width:min(100%,450px);display:flex;flex-direction:column;gap:7px}.schedule>div{display:flex;align-items:center;gap:12px;padding:9px;background:var(--surface-2);border-radius:12px;border-left:4px solid var(--widget-accent)}.schedule-icon{width:33px;height:33px;display:grid;place-items:center;flex:none;border-radius:10px;background:var(--surface);font-size:20px;font-style:normal}.schedule time{font-weight:900;color:var(--blue)}.schedule span{flex:1;font-weight:800}.schedule .tbtn{height:30px;min-height:30px;padding:0 9px}.class-total{flex-basis:100%;width:100%;margin:0 0 3px;color:var(--muted);font-size:12px;font-weight:900;text-align:center}.mood{display:flex;gap:8px;flex-wrap:wrap}.mood .tbtn{flex:1;min-width:62px;font-size:36px;height:auto;padding:12px;background:var(--surface);border:2px solid var(--line);color:var(--ink)}.mood span{display:block;font-size:14px}.poll{width:min(100%,440px);display:flex;flex-direction:column;gap:8px}.poll h3{margin-bottom:0;text-align:center}.poll .tbtn{position:relative;overflow:hidden;text-align:left}.poll .tbtn i{position:absolute;inset:0 auto 0 0;background:color-mix(in srgb,var(--widget-accent) 22%,var(--surface));z-index:0;transition:width .25s}.poll .tbtn span{position:relative;float:right;font-weight:900}.poll .tbtn{isolation:isolate} @media(max-width:600px){.gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.gcard{min-height:120px;padding:13px 8px}.gcard .gd{display:none}.widget .wdup,.widget .wpreview{display:none}.edu-stage{padding:10px}.place-grid b{font-size:9px}.place-grid output{padding:12px 2px}} /* Wereldoriëntatie: natuurherkenning en topografie */ @@ -1638,6 +1641,30 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo .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}} +/* ============================================================ + Kindvriendelijke spel-laag + Eén accent, duidelijke missies en tastbare bediening voor elk widgettype. + ============================================================ */ +.widget{--widget-accent:var(--teal);--widget-soft:color-mix(in srgb,var(--widget-accent) 12%,var(--surface))} +.widget:is([data-widget="anchor"],[data-widget="letters"],[data-widget="flits"],[data-widget="hak"],[data-widget="ball"],[data-widget="sentence"],[data-widget="wordtypes"]){--widget-accent:var(--purple)} +.widget:is([data-widget="madd"],[data-widget="msub"],[data-widget="mmul"],[data-widget="mdiv"],[data-widget="mpct"],[data-widget="numberline"],[data-widget="fractions"],[data-widget="clock"],[data-widget="placevalue"],[data-widget="money"]){--widget-accent:var(--blue)} +.widget:is([data-widget="trees"],[data-widget="birds"],[data-widget="plants"],[data-widget="topography"]){--widget-accent:var(--green)} +.widget:is([data-widget="birthday"],[data-widget="schedule"],[data-widget="light"],[data-widget="mood"],[data-widget="poll"]){--widget-accent:var(--pink)} +.widget[data-widget="media"]{--widget-accent:var(--orange)} +.widget:not(.bare){border-top:4px solid var(--widget-accent)} +.widget:not(.bare) .widget-head .wicon{width:34px;height:34px;display:grid;place-items:center;flex:none;border-radius:11px;background:var(--widget-soft);font-size:20px;transform:rotate(-3deg)} +.widget:not(.bare) .widget-head .wtitle{color:color-mix(in srgb,var(--widget-accent) 74%,var(--ink))} +.widget-body{background-image:radial-gradient(circle at 96% 4%,color-mix(in srgb,var(--widget-accent) 8%,transparent) 0 54px,transparent 55px)} +.widget-body .tbtn{transition:transform .12s,filter .12s,box-shadow .12s}.widget-body .tbtn:not(.ghost){background:var(--widget-accent);box-shadow:0 3px 0 color-mix(in srgb,var(--widget-accent) 72%,#18233a)}.widget-body .tbtn:active{transform:translateY(2px) scale(.985);box-shadow:none}.widget-body :is(button,input,select,textarea):focus-visible{outline:3px solid color-mix(in srgb,var(--widget-accent) 72%,#fff);outline-offset:2px} +.play-mission{width:min(100%,560px);margin:0 auto;padding:8px 12px;border:1px solid color-mix(in srgb,var(--widget-accent) 25%,var(--line));border-radius:13px;background:var(--widget-soft);color:color-mix(in srgb,var(--widget-accent) 68%,var(--ink));font-size:13px;font-weight:850;line-height:1.35;text-align:center} +.fl-reflect{width:100%;padding:8px 10px;border-radius:14px;background:var(--widget-soft);text-align:center}.fl-reflect p{margin:0 0 7px;font-weight:900}.fl-reflect>div{display:flex;justify-content:center;gap:7px;flex-wrap:wrap}.fl-reflect[hidden]{display:none} +.dc-history{min-height:28px;display:flex;align-items:center;justify-content:center;gap:5px;flex-wrap:wrap}.dc-history span{margin-right:3px;color:var(--muted);font-size:11px;font-weight:850}.dc-history b{min-width:28px;height:28px;display:grid;place-items:center;border-radius:9px;background:var(--widget-soft);color:var(--widget-accent)} +.notes-app{height:100%;display:flex;flex-direction:column;background:#fff8cf;transition:background .15s}.notes-app[data-theme="mint"]{background:#e5f7df}.notes-app[data-theme="sky"]{background:#e2f3ff}.notes-app[data-theme="lilac"]{background:#f0e8ff}.notes-tools{min-height:42px;display:flex;align-items:center;gap:7px;padding:6px 8px;border-bottom:1px solid rgba(29,52,97,.12);color:#52627b;font-size:11px;font-weight:900}.notes-swatches{display:flex;gap:4px}.notes-swatches button{width:24px;height:24px;padding:0;border:2px solid rgba(29,52,97,.16);border-radius:50%;cursor:pointer}.notes-swatches button[data-theme="sun"]{background:#fff0a8}.notes-swatches button[data-theme="mint"]{background:#cdeebe}.notes-swatches button[data-theme="sky"]{background:#c6e9ff}.notes-swatches button[data-theme="lilac"]{background:#dfd0ff}.notes-swatches button.on{outline:2px solid var(--ink);outline-offset:1px}.notes-size{height:28px;padding:0 7px;border:1px solid rgba(29,52,97,.18);border-radius:8px;background:rgba(255,255,255,.5);color:#33445f;font-weight:900;cursor:pointer}.notes-size.on{background:#fff}.notes-count{margin-left:auto;white-space:nowrap} +.mt-card,.lg-card,.money-task-card,.clock-task-card,.world-task-card,.sb-sentence,.nlw-scroll{box-shadow:0 7px 20px rgba(29,52,97,.09)} +@media(pointer:coarse){.widget-body :is(.tbtn,.sb-chip,.nlw-tick,.fraction-piece,.tl-lamp){min-height:44px}} +@media(prefers-reduced-motion:reduce){.widget-body *,.widget-body *::before,.widget-body *::after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}} +@container widget (max-width:520px){.play-mission{padding:6px 9px;font-size:12px}.tl{padding:8px}.tl-house{padding:9px}.tl-lamp{width:58px;height:58px}.tl-option{gap:11px}.tl-option strong{font-size:16px}.tl-option small{font-size:11px}.notes-color-label{display:none}.notes-tools{gap:5px}.notes-count{font-size:10px}.fraction-game{gap:6px}.fraction-piece{min-height:34px;font-size:11px}.dc-history span{flex-basis:100%;text-align:center}.sb-surprise{flex-basis:100%}} + /* ============================================================ Widgetinhoud op telefoonformaat De widget-body is de container: dit werkt zowel op het bord als diff --git a/public/js/core.js b/public/js/core.js index f7fad1a..26d11c6 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.3.76-beta"; +const VERSION = "0.3.77-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; @@ -191,7 +191,7 @@ const I18N = { imgQuotaMb:"limiet in MB", imgQuotaInvalid:"Vul een geldige limiet in.", imgSchools:"Scholen", imgUsers:"Gebruikers", imgSearchUsers:"Zoek gebruiker…", cats:["Dieren","Eten","Natuur","Vervoer","Spullen","Lichaam"], - notesPh:"Schrijf hier…", + notesPh:"Schrijf hier…", notesColor:"Kleur briefje", notesLarge:"Grote letters", notesCount:"{count} tekens", start:"Start", pause:"Pauze", reset:"Reset", min:"min", tmDigits:"Cijfers", tmVisual:"Kijkklok", tmBoth:"Allebei", tmSound:"Geluid aan/uit", tmHint:"Sleep aan de rode schijf om de tijd in te stellen.", diff --git a/public/js/widgets/dice.js b/public/js/widgets/dice.js index f563868..02d34f5 100644 --- a/public/js/widgets/dice.js +++ b/public/js/widgets/dice.js @@ -3,7 +3,7 @@ Dice widget ==========================================================*/ function mountDice(root, initState){ - let count = (initState && initState.count) || 2, rolling = false; + let count = (initState && initState.count) || 2, rolling = false, history=Array.isArray(initState?.history)?initState.history.slice(-5):[]; const PIPS = {1:[4],2:[0,8],3:[0,4,8],4:[0,2,6,8],5:[0,2,4,6,8],6:[0,2,3,5,6,8]}; root.innerHTML = `
@@ -13,8 +13,10 @@ function mountDice(root, initState){
+

-
+
+
`; const row = root.querySelector(".dc-row"), @@ -50,10 +52,12 @@ function mountDice(root, initState){ dice.forEach(d=>d.classList.remove("rolling")); const total = dice.reduce((s,d)=>s + +d.dataset.v, 0); if(count>1) sumEl.textContent = `${T("diceSum")}: ${total}`; + history.push(total);history=history.slice(-5);renderHistory(); rolling = false; } }, 90); } + function renderHistory(){const box=root.querySelector('.dc-history');box.innerHTML='';if(!history.length)return;const label=document.createElement('span');label.textContent=T('diceHistory');box.append(label,...history.map(value=>{const chip=document.createElement('b');chip.textContent=value;return chip}))} row.addEventListener("click", roll); rollBtn.addEventListener("click", roll); root.querySelectorAll(".dc-seg button").forEach(b=>{ @@ -65,10 +69,11 @@ function mountDice(root, initState){ }); function labels(){ rollBtn.textContent = T("diceRoll"); + root.querySelector('.dc-mission').textContent=T('diceMission'); root.querySelectorAll(".dc-seg button").forEach(x=>x.classList.toggle("on", +x.dataset.n===count)); + renderHistory(); } document.addEventListener("langchange", labels); labels(); build(); - return { getState: ()=>({count}) }; + return { getState: ()=>({count,history:[...history]}) }; } - diff --git a/public/js/widgets/flits.js b/public/js/widgets/flits.js index 11c7654..c182491 100644 --- a/public/js/widgets/flits.js +++ b/public/js/widgets/flits.js @@ -3,9 +3,9 @@ Flash words widget ==========================================================*/ function mountFlits(root, initState){ - const S = { card:0, dur:2, idx:0, order:[] }; - if(initState){ S.card = initState.card ?? 0; S.dur = initState.dur || 2; } - let flashTimer = null; + const S = { card:0, dur:2, idx:0, order:[], score:0, tries:0 }; + if(initState){ S.card = initState.card ?? 0; S.dur = initState.dur || 2; S.score=+initState.score||0; S.tries=+initState.tries||0; } + let flashTimer = null, assessed=false; root.innerHTML = `
@@ -14,10 +14,15 @@ function mountFlits(root, initState){
+

• • •
+
@@ -26,7 +31,7 @@ function mountFlits(root, initState){
`; const el = s => root.querySelector(s); const srcSel = el(".fl-src"), durSel = el(".fl-dur"), - wordEl = el(".fl-word"), picEl = el(".fl-pic"), cntEl = el(".fl-cnt"); + wordEl = el(".fl-word"), picEl = el(".fl-pic"), cntEl = el(".fl-cnt"), reflect=el(".fl-reflect"); function list(){ S.card = normWordSource(S.card); return wordSourceList(S.card); } function buildSrc(){ @@ -44,27 +49,38 @@ function mountFlits(root, initState){ S.idx = 0; } function cur(){ return list()[S.order[S.idx]]; } - function hide(){ + function score(){ cntEl.textContent = `${T("word")} ${S.idx+1}/${S.order.length} · ${T("flScore").replace("{score}",S.score)}`; } + function hide(ask=false){ wordEl.textContent = "• • •"; wordEl.classList.add("dim"); picEl.textContent = ""; + reflect.hidden=!ask||assessed; } function flash(){ clearTimeout(flashTimer); + assessed=false; wordEl.textContent = cur()[0]; wordEl.classList.remove("dim"); picEl.textContent = ""; - cntEl.textContent = `${T("word")} ${S.idx+1}/${S.order.length}`; - flashTimer = setTimeout(hide, S.dur*1000); + reflect.hidden=true; score(); + flashTimer = setTimeout(()=>hide(true), S.dur*1000); } function show(){ clearTimeout(flashTimer); wordEl.textContent = cur()[0]; wordEl.classList.remove("dim"); setPic(picEl, cur()[1]); + reflect.hidden=assessed; + } + function assess(ok){ + if(assessed)return;assessed=true; + S.tries++; if(ok){S.score++;sndGood();confetti(el('.mt-card'));}else sndBad(); + score(); reflect.hidden=true; } el(".fl-flash").addEventListener("click", flash); el(".fl-show").addEventListener("click", show); + el(".fl-got").addEventListener("click",()=>assess(true)); + el(".fl-again").addEventListener("click",()=>assess(false)); el(".fl-next").addEventListener("click", ()=>{ S.idx++; if(S.idx >= S.order.length) newRound(); @@ -80,11 +96,15 @@ function mountFlits(root, initState){ el(".fl-flash").textContent = T("flFlash"); el(".fl-show").textContent = T("flShow"); el(".fl-next").textContent = T("flNext"); + el(".fl-mission").textContent=T("flMission"); + el(".fl-question").textContent=T("flQuestion"); + el(".fl-got").textContent=T("flGotIt"); + el(".fl-again").textContent=T("flAgain"); buildSrc(); + if(S.order.length)score(); } document.addEventListener("langchange", ()=>{ S.card=0; labels(); newRound(); hide(); }); document.addEventListener("wordschange", ()=>{ buildSrc(); newRound(); hide(); }); labels(); newRound(); - return { getState: ()=>({card:S.card, dur:S.dur}) }; + return { getState: ()=>({card:S.card, dur:S.dur, score:S.score, tries:S.tries}) }; } - diff --git a/public/js/widgets/hak.js b/public/js/widgets/hak.js index c211f1f..f9ae24c 100644 --- a/public/js/widgets/hak.js +++ b/public/js/widgets/hak.js @@ -1,4 +1,20 @@ /* widget: Hakken & plakken */ +/* Veelvoorkomende spellingpatronen blijven als herkenbare klankgroep bij elkaar. + De langste groep wint, zodat bijvoorbeeld 'sch' niet als 's' + 'ch' eindigt. */ +function soundChunks(word, lang=LANG){ + const patterns=(lang==='nl' + ? ['sch','aai','ooi','oei','eeuw','ieuw','ng','nk','ch','aa','ee','oo','uu','oe','ie','ei','ij','ou','au','ui','eu'] + : ['tch','igh','sh','ch','th','ph','ng','ee','oo','ea','ou','ow','ai','ay','oi']).sort((a,b)=>b.length-a.length); + const chunks=[]; + for(let i=0;irest.startsWith(pattern)); + const length=match?match.length:1; + chunks.push(word.slice(i,i+length)); i+=length; + } + return chunks; +} + /* ========================================================= Chop & blend widget — words in sounds, with read-aloud ==========================================================*/ @@ -13,6 +29,7 @@ function mountHak(root, initState){
+

@@ -36,14 +53,14 @@ function mountHak(root, initState){ const [w, pic] = cur(); wordEl.innerHTML = ""; if(S.chopped){ - w.split("").forEach((l,i)=>{ + soundChunks(w).forEach((l,i)=>{ if(i>0){ const sep = document.createElement("span"); sep.className = "hk-sep"; sep.textContent = "–"; wordEl.appendChild(sep); } const s = document.createElement("span"); - s.className = "hk-l " + (VOWELS.includes(l) ? "v" : "c"); + s.className = "hk-l " + (/[aeiouyáéíóúäëïöü]/i.test(l) ? "v" : "c"); s.textContent = l; s.style.animationDelay = (i*0.08)+"s"; wordEl.appendChild(s); @@ -66,7 +83,7 @@ function mountHak(root, initState){ speechSynthesis.speak(u); } el(".hk-say").addEventListener("click", ()=>{ - if(S.chopped) speak(cur()[0].split("").join(", "), 0.7); + if(S.chopped) speak(soundChunks(cur()[0]).join(", "), 0.7); else speak(cur()[0], 0.85); }); el(".hk-chop").addEventListener("click", ()=>{ S.chopped = true; display(); play(sndGood); }); @@ -84,6 +101,7 @@ function mountHak(root, initState){ el(".hk-say").textContent = T("hakSay"); el(".hk-chop").textContent = T("hakChop"); el(".hk-glue").textContent = T("hakGlue"); + el(".hk-hint").textContent = T("hakHint"); buildSrc(); } document.addEventListener("langchange", ()=>{ S.card=0; labels(); newRound(); S.chopped=false; display(); }); @@ -91,4 +109,3 @@ function mountHak(root, initState){ labels(); newRound(); display(); return { getState: ()=>({card:S.card}) }; } - diff --git a/public/js/widgets/learning.js b/public/js/widgets/learning.js index c87d319..bb53192 100644 --- a/public/js/widgets/learning.js +++ b/public/js/widgets/learning.js @@ -649,8 +649,9 @@ function btn(text,fn,kind='ghost'){const b=document.createElement('button');b.ty function num(value,min,max){const i=document.createElement('input');i.type='number';i.value=value;i.min=min;i.max=max;return i} function mountFractions(root,init){ - const S=Object.assign({den:4,show:true},init||{}), den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show; - const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;stage.className='fraction-wall';stage.innerHTML='';for(let d=1;d<=S.den;d++){const row=document.createElement('div');for(let n=0;nc.classList.toggle('selected');row.append(c)}stage.append(row)}}; + const S=Object.assign({den:4,show:true,activeD:0,activeN:0},init||{}), den=num(S.den,2,12), show=document.createElement('input'), stage=document.createElement('div');show.type='checkbox';show.checked=S.show; + const gcd=(a,b)=>b?gcd(b,a%b):a,summary=()=>{if(!S.activeD)return'';const divisor=gcd(S.activeN,S.activeD),fraction=`${S.activeN}/${S.activeD}`,simple=`${S.activeN/divisor}/${S.activeD/divisor}`;return T('fractionSelected').replace('{fraction}',divisor>1?`${fraction} = ${simple}`:fraction)}; + const draw=()=>{S.den=Math.max(2,Math.min(12,+den.value||4));S.show=show.checked;if(S.activeD>S.den){S.activeD=0;S.activeN=0}stage.className='fraction-game';stage.innerHTML='';const hint=document.createElement('p');hint.className='play-mission';hint.textContent=T('fractionHint');const result=document.createElement('output');result.className='fraction-summary';result.textContent=summary();const wall=document.createElement('div');wall.className='fraction-wall';for(let d=1;d<=S.den;d++){const row=document.createElement('div');for(let n=0;n{S.activeD=d;S.activeN=n+1;draw()};row.append(c)}wall.append(row)}const clear=btn(T('fractionClear'),()=>{S.activeD=0;S.activeN=0;draw()});clear.classList.add('fraction-clear');stage.append(hint,result,wall,clear)}; den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})}; } const clockTurnDelta=(next,previous)=>((next-previous+540)%360)-180; @@ -755,19 +756,20 @@ function mountMoney(root,init){ } function mountWordtypes(root,init){ - const S=Object.assign({sentence:'De vrolijke hond rent snel',answers:{}},init||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence; + const S=Object.assign({sentence:LANG==='nl'?'De vrolijke hond rent snel':'The cheerful dog runs quickly',answers:{}},init||{}),input=document.createElement('input'),stage=document.createElement('div');input.value=S.sentence; const types=['noun','verb','adjective','other'],draw=()=>{S.sentence=input.value.slice(0,120);stage.className='wordtypes';stage.innerHTML='';S.sentence.split(/\s+/).filter(Boolean).forEach((word,i)=>{const w=document.createElement('button');w.type='button';w.textContent=word;const type=S.answers[i]||'other';w.dataset.type=type;w.onclick=()=>{S.answers[i]=types[(types.indexOf(w.dataset.type)+1)%types.length];draw()};stage.append(w)});const legend=document.createElement('p');legend.textContent=T('wordtypeHint');stage.append(legend)};input.oninput=draw;learningShell(root,[field(T('wordtypeSentence'),input)],stage);draw();return{getState:()=>({...S})}; } function mountSchedule(root,init){ - const S=Object.assign({items:[{time:'08:30',text:'Start'},{time:'10:15',text:'Pauze'}]},init||{}),time=document.createElement('input'),text=document.createElement('input'),stage=document.createElement('div');time.type='time';time.value='09:00';text.placeholder=T('scheduleActivity'); - const draw=()=>{stage.className='schedule';stage.innerHTML='';S.items.sort((a,b)=>a.time.localeCompare(b.time)).forEach((it,i)=>{const row=document.createElement('div');const t=document.createElement('time');t.textContent=it.time;const s=document.createElement('span');s.textContent=it.text;row.append(t,s,btn('×',()=>{S.items.splice(i,1);draw()}));stage.append(row)});if(!S.items.length){const e=document.createElement('p');e.className='edu-empty';e.textContent=T('scheduleEmpty');stage.append(e)}}; + const defaults=LANG==='nl'?[{time:'08:30',text:'Start'},{time:'10:15',text:'Pauze'}]:[{time:'08:30',text:'Start'},{time:'10:15',text:'Break'}],S=Object.assign({items:defaults},init||{}),time=document.createElement('input'),text=document.createElement('input'),stage=document.createElement('div');time.type='time';time.value='09:00';text.placeholder=T('scheduleActivity'); + const activityIcon=value=>{const text=value.toLocaleLowerCase();if(/pauze|break|buiten|outside/.test(text))return'🛝';if(/eten|lunch|fruit|eat/.test(text))return'🍎';if(/lees|taal|read|language/.test(text))return'📚';if(/reken|math/.test(text))return'🔢';if(/gym|sport|move/.test(text))return'⚽';if(/crea|teken|muziek|art|music/.test(text))return'🎨';return'⭐'}; + const draw=()=>{stage.className='schedule';stage.innerHTML='';const status=document.createElement('p');status.className='class-total';status.textContent=T('scheduleCount').replace('{count}',S.items.length);stage.append(status);S.items.sort((a,b)=>a.time.localeCompare(b.time)).forEach((it,i)=>{const row=document.createElement('div');const icon=document.createElement('i');icon.className='schedule-icon';icon.textContent=activityIcon(it.text);const t=document.createElement('time');t.textContent=it.time;const s=document.createElement('span');s.textContent=it.text;row.append(icon,t,s,btn('×',()=>{S.items.splice(i,1);draw()}));stage.append(row)});if(!S.items.length){const e=document.createElement('p');e.className='edu-empty';e.textContent=T('scheduleEmpty');stage.append(e)}}; learningShell(root,[time,text,btn(T('addWord'),()=>{if(text.value.trim()){S.items.push({time:time.value,text:text.value.trim().slice(0,50)});text.value='';draw()}})],stage);draw();return{getState:()=>({...S,items:S.items.map(x=>({...x}))})}; } function mountMood(root,init){ const S=Object.assign({votes:[0,0,0,0,0]},init||{}),stage=document.createElement('div'),faces=['😢','😕','😐','🙂','🤩']; - const draw=()=>{stage.className='mood';stage.innerHTML='';faces.forEach((f,i)=>{const b=btn(f,()=>{S.votes[i]++;draw()});const n=document.createElement('span');n.textContent=S.votes[i];b.append(n);stage.append(b)})};learningShell(root,[btn(T('pollReset'),()=>{S.votes=[0,0,0,0,0];draw()})],stage);draw();return{getState:()=>({...S,votes:[...S.votes]})}; + const draw=()=>{stage.className='mood';stage.innerHTML='';const total=document.createElement('p');total.className='class-total';total.textContent=T('moodTotal').replace('{count}',S.votes.reduce((sum,n)=>sum+n,0));stage.append(total);faces.forEach((f,i)=>{const b=btn(f,()=>{S.votes[i]++;draw()});b.setAttribute('aria-label',T('moodVote').replace('{face}',f));const n=document.createElement('span');n.textContent=S.votes[i];b.append(n);stage.append(b)})};learningShell(root,[btn(T('pollReset'),()=>{S.votes=[0,0,0,0,0];draw()})],stage);draw();return{getState:()=>({...S,votes:[...S.votes]})}; } function mountPoll(root,init){ - const S=Object.assign({question:'Wat kies jij?',options:[{text:'A',votes:0},{text:'B',votes:0}]},init||{}),question=document.createElement('input'),options=document.createElement('textarea'),stage=document.createElement('div');question.value=S.question;options.value=S.options.map(x=>x.text).join('\n'); - const sync=()=>{S.question=question.value.slice(0,100);const old=new Map(S.options.map(x=>[x.text,x.votes]));S.options=options.value.split('\n').map(x=>x.trim()).filter(Boolean).slice(0,6).map(text=>({text,votes:old.get(text)||0}));draw()},draw=()=>{stage.className='poll';stage.innerHTML='';const h=document.createElement('h3');h.textContent=S.question;stage.append(h);const max=Math.max(1,...S.options.map(x=>x.votes));S.options.forEach(o=>{const b=btn(o.text,()=>{o.votes++;draw()});const bar=document.createElement('i');bar.style.width=(o.votes/max*100)+'%';const n=document.createElement('span');n.textContent=o.votes;b.append(bar,n);stage.append(b)})};question.oninput=options.oninput=sync;learningShell(root,[field(T('pollQuestion'),question),field(T('pollOptions'),options),btn(T('pollReset'),()=>{S.options.forEach(x=>x.votes=0);draw()})],stage);draw();return{getState:()=>({...S,options:S.options.map(x=>({...x}))})}; + const S=Object.assign({question:LANG==='nl'?'Wat kies jij?':'What do you choose?',options:[{text:'A',votes:0},{text:'B',votes:0}]},init||{}),question=document.createElement('input'),options=document.createElement('textarea'),stage=document.createElement('div');question.value=S.question;options.value=S.options.map(x=>x.text).join('\n'); + const sync=()=>{S.question=question.value.slice(0,100);const old=new Map(S.options.map(x=>[x.text,x.votes]));S.options=options.value.split('\n').map(x=>x.trim()).filter(Boolean).slice(0,6).map(text=>({text,votes:old.get(text)||0}));draw()},draw=()=>{stage.className='poll';stage.innerHTML='';const h=document.createElement('h3');h.textContent=S.question;const total=S.options.reduce((sum,item)=>sum+item.votes,0),totalEl=document.createElement('p');totalEl.className='class-total';totalEl.textContent=T('pollTotal').replace('{count}',total);stage.append(h,totalEl);const max=Math.max(1,...S.options.map(x=>x.votes));S.options.forEach(o=>{const b=btn(o.text,()=>{o.votes++;draw()});b.setAttribute('aria-label',`${o.text}: ${o.votes}`);const bar=document.createElement('i');bar.style.width=(o.votes/max*100)+'%';const n=document.createElement('span');n.textContent=o.votes;b.append(bar,n);stage.append(b)})};question.oninput=options.oninput=sync;learningShell(root,[field(T('pollQuestion'),question),field(T('pollOptions'),options),btn(T('pollReset'),()=>{S.options.forEach(x=>x.votes=0);draw()})],stage);draw();return{getState:()=>({...S,options:S.options.map(x=>({...x}))})}; } diff --git a/public/js/widgets/light.js b/public/js/widgets/light.js index e54310a..bd7e623 100644 --- a/public/js/widgets/light.js +++ b/public/js/widgets/light.js @@ -6,19 +6,30 @@ function mountLight(root, initState){ let on = (initState && initState.on) || "green"; root.innerHTML = `
+

- - - +
+
+
+
`; + const keys={red:['lightRed','lightRedHint'],orange:['lightOrange','lightOrangeHint'],green:['lightGreen','lightGreenHint']}; function apply(){ - root.querySelectorAll(".tl-lamp").forEach(l=>l.classList.toggle("on", l.dataset.c===on)); + root.querySelectorAll(".tl-lamp").forEach(l=>{ + const active=l.dataset.c===on; l.classList.toggle("on",active);l.setAttribute('aria-pressed',String(active)); + }); + const [color,hint]=keys[on]; + root.querySelector('.tl-status').textContent=T('lightStatus').replace('{color}',T(color)).replace('{hint}',T(hint)); } root.querySelectorAll(".tl-lamp").forEach(l=>{ - l.addEventListener("click", ()=>{ on = l.dataset.c; apply(); }); + l.addEventListener("click", ()=>{ on = l.dataset.c; apply(); tone(on==='red'?260:on==='orange'?390:520,.12,0,'sine',.06); }); }); - apply(); + function labels(){ + root.querySelector('.tl-mission').textContent=T('wg_light_d'); + root.querySelectorAll('.tl-option').forEach(option=>{const lamp=option.querySelector('.tl-lamp'),[color,hint]=keys[lamp.dataset.c];lamp.setAttribute('aria-label',`${T(color)}: ${T(hint)}`);option.querySelector('strong').textContent=T(color);option.querySelector('small').textContent=T(hint)}); + apply(); + } + document.addEventListener('langchange',labels);labels(); return { getState: ()=>({on}) }; } - diff --git a/public/js/widgets/names.js b/public/js/widgets/names.js index 5652263..88ee88d 100644 --- a/public/js/widgets/names.js +++ b/public/js/widgets/names.js @@ -3,13 +3,15 @@ Name picker widget ==========================================================*/ function mountNames(root, initState){ - let used = new Set(), spinning = false; + let used = new Set(initState?.used||[]), spinning = false; root.innerHTML = `
🎯
+
+
`; @@ -21,9 +23,10 @@ function mountNames(root, initState){ ta.value = initState.names || ""; nr.checked = !!initState.nr; } - ta.addEventListener("input", ()=>used.clear()); + ta.addEventListener("input", ()=>{used.clear();updateProgress();}); ta.addEventListener("keydown", e=>e.stopPropagation()); function names(){ return ta.value.split("\n").map(s=>s.trim()).filter(Boolean); } + function updateProgress(){const all=names();used=new Set([...used].filter(name=>all.includes(name)));root.querySelector('.np-progress').textContent=T('namesProgress').replace('{used}',used.size).replace('{total}',all.length)} pickBtn.addEventListener("click", ()=>{ if(spinning) return; const all = names(); @@ -31,7 +34,7 @@ function mountNames(root, initState){ let pool = nr.checked ? all.filter(n=>!used.has(n)) : all; if(!pool.length){ result.textContent = T("namesDone"); - used.clear(); + updateProgress(); return; } spinning = true; @@ -51,18 +54,21 @@ function mountNames(root, initState){ result.classList.add("winner"); confetti(result.parentElement); if(nr.checked) used.add(winner); + updateProgress(); sndWin(); spinning = false; } }, 80); }); + root.querySelector('.np-reset').addEventListener('click',()=>{used.clear();result.textContent='🎯';result.classList.remove('winner','spin');updateProgress()}); function labels(){ ta.placeholder = T("namesPh"); pickBtn.textContent = T("namesPick"); + root.querySelector('.np-reset').textContent=T('namesReset'); root.querySelector(".np-nrl").textContent = T("namesNoRep"); + updateProgress(); } document.addEventListener("langchange", labels); labels(); - return { getState: ()=>({names:ta.value, nr:nr.checked}) }; + return { getState: ()=>({names:ta.value, nr:nr.checked, used:[...used]}) }; } - diff --git a/public/js/widgets/notes.js b/public/js/widgets/notes.js index 4f1db0a..043a9b0 100644 --- a/public/js/widgets/notes.js +++ b/public/js/widgets/notes.js @@ -3,12 +3,17 @@ Notes widget ==========================================================*/ function mountNotes(root, initState){ + const S={theme:initState?.theme||'sun',large:!!initState?.large}; + root.innerHTML='
'; const ta = document.createElement("textarea"); ta.className = "notes"; ta.placeholder = T("notesPh"); if(initState && typeof initState.text==="string") ta.value = initState.text; - document.addEventListener("langchange", ()=>{ ta.placeholder = T("notesPh"); }); - root.appendChild(ta); - return { getState: ()=>({text: ta.value}) }; + const app=root.querySelector('.notes-app'),swatches=root.querySelector('.notes-swatches'); + ['sun','mint','sky','lilac'].forEach(theme=>{const b=document.createElement('button');b.type='button';b.dataset.theme=theme;b.setAttribute('aria-label',theme);b.onclick=()=>{S.theme=theme;apply()};swatches.append(b)}); + app.appendChild(ta); + function apply(){app.dataset.theme=S.theme;ta.classList.toggle('large',S.large);root.querySelectorAll('.notes-swatches button').forEach(b=>{const on=b.dataset.theme===S.theme;b.classList.toggle('on',on);b.setAttribute('aria-pressed',String(on))});root.querySelector('.notes-size').classList.toggle('on',S.large);root.querySelector('.notes-size').setAttribute('aria-pressed',String(S.large));root.querySelector('.notes-count').textContent=T('notesCount').replace('{count}',ta.value.length)} + function labels(){ta.placeholder=T('notesPh');root.querySelector('.notes-color-label').textContent=T('notesColor');root.querySelector('.notes-size').title=T('notesLarge');apply()} + ta.addEventListener('input',apply);root.querySelector('.notes-size').onclick=()=>{S.large=!S.large;apply()};document.addEventListener("langchange",labels);labels(); + return { getState: ()=>({text:ta.value,theme:S.theme,large:S.large}) }; } - diff --git a/public/js/widgets/numberline.js b/public/js/widgets/numberline.js index f00ab9f..ee8fdc9 100644 --- a/public/js/widgets/numberline.js +++ b/public/js/widgets/numberline.js @@ -1,6 +1,6 @@ /* widget: interactieve getallenlijn */ function mountNumberline(root, initState){ - const S=Object.assign({min:0,max:20,step:1,value:0,target:10},initState||{}); + const S=Object.assign({min:0,max:20,step:1,value:0,target:10,score:0,streak:0,solved:false},initState||{}); root.innerHTML=`
@@ -9,7 +9,7 @@ function mountNumberline(root, initState){
-
+
@@ -20,7 +20,7 @@ function mountNumberline(root, initState){
`; const q=s=>root.querySelector(s), minEl=q('.nlw-min'), maxEl=q('.nlw-max'), stepEl=q('.nlw-step'), - line=q('.nlw-line'), valueEl=q('.nlw-value'), task=q('.nlw-task'), feedback=q('.nlw-feedback'); + line=q('.nlw-line'), valueEl=q('.nlw-value'), task=q('.nlw-task'), feedback=q('.nlw-feedback'),score=q('.nlw-score'); minEl.value=S.min; maxEl.value=S.max; stepEl.value=S.step; function normalize(){ let min=Math.max(-100,Math.min(99,+minEl.value||0)); let max=Math.max(-99,Math.min(200,+maxEl.value||20)); @@ -37,13 +37,16 @@ function mountNumberline(root, initState){ const b=document.createElement('button'); b.type='button'; b.className='nlw-tick'; b.dataset.v=v; const mark=document.createElement('i'), label=document.createElement('span'); label.textContent=v; b.append(mark,label); b.classList.toggle('selected',v===S.value); - b.addEventListener('click',()=>{S.value=v; feedback.textContent=""; render();}); line.appendChild(b); + b.addEventListener('click',()=>{S.value=v;if(!S.solved)feedback.textContent="";render();}); line.appendChild(b); } valueEl.textContent=S.value; task.textContent=T('numberlineTask').replace('{number}',S.target); + score.textContent=T('numberlineScore').replace('{score}',S.score).replace('{streak}',S.streak); } function newTarget(){ - const count=Math.floor((S.max-S.min)/S.step); S.target=S.min+Math.floor(Math.random()*(count+1))*S.step; - feedback.textContent=""; render(); + const count=Math.floor((S.max-S.min)/S.step);let target=S.target; + for(let tries=0;tries<8&&target===S.target;tries++)target=S.min+Math.floor(Math.random()*(count+1))*S.step; + S.target=target;S.solved=false; + feedback.textContent="";feedback.className='nlw-feedback';render(); } [minEl,maxEl,stepEl].forEach(el=>el.addEventListener('change',normalize)); q('.nlw-minus').addEventListener('click',()=>{S.value=Math.max(S.min,S.value-S.step);render();}); @@ -51,7 +54,8 @@ function mountNumberline(root, initState){ q('.nlw-new').addEventListener('click',newTarget); q('.nlw-check').addEventListener('click',()=>{ const ok=S.value===S.target; feedback.textContent=T(ok?'numberlineGood':'numberlineTry'); - feedback.className='nlw-feedback '+(ok?'ok':'bad'); if(ok){sndGood();confetti(root);}else sndBad(); + feedback.className='nlw-feedback '+(ok?'ok':'bad'); + if(ok){if(!S.solved){S.score++;S.streak++;S.solved=true;}sndGood();confetti(root);}else{S.streak=0;S.solved=false;sndBad();}render(); }); function labels(){ q('.nlw-from-label').textContent=T('numberlineFrom'); q('.nlw-to-label').textContent=T('numberlineTo'); diff --git a/public/js/widgets/sentence.js b/public/js/widgets/sentence.js index af163ca..79107f1 100644 --- a/public/js/widgets/sentence.js +++ b/public/js/widgets/sentence.js @@ -4,7 +4,7 @@ function mountSentence(root, initState){ who:['De hond','Mijn vriend','De juf','Sara'], does:['lees','speelt','maakt','ziet'], what:['een boek','op het plein','een mooie tekening','de rode bal'], } : { - who:['The dog','My friend','The teacher','Sarah'], does:['read','plays','makes','sees'], + who:['The dog','My friend','The teacher','Sarah'], does:['reads','plays','makes','sees'], what:['a book','in the playground','a beautiful drawing','the red ball'], }; const S={banks:JSON.parse(JSON.stringify(initState?.banks||defaults)),sentence:[...(initState?.sentence||[])]}; @@ -17,10 +17,10 @@ function mountSentence(root, initState){
-
+
`; const q=s=>root.querySelector(s), sentenceEl=q('.sb-sentence'), banksEl=q('.sb-banks'), kind=q('.sb-kind'), word=q('.sb-word'); - function sentenceText(){ return S.sentence.map(x=>x.word).join(' ').replace(/s+([,.!?])/g,'$1'); } + function sentenceText(){ return S.sentence.map(x=>x.word).join(' ').replace(/\s+([,.!?])/g,'$1'); } function renderSentence(){ sentenceEl.innerHTML=""; if(!S.sentence.length){ sentenceEl.textContent=T('sentenceHint'); sentenceEl.classList.add('empty'); return; } @@ -47,10 +47,11 @@ function mountSentence(root, initState){ q('.sb-add-btn').addEventListener('click',addWord); word.addEventListener('keydown',e=>{e.stopPropagation();if(e.key==='Enter')addWord();}); q('.sb-back').addEventListener('click',()=>{S.sentence.pop();renderSentence();}); q('.sb-clear').addEventListener('click',()=>{S.sentence=[];renderSentence();}); + q('.sb-surprise').addEventListener('click',()=>{S.sentence=['who','does','what'].filter(k=>S.banks[k].length).map(kind=>({kind,word:S.banks[kind][Math.floor(Math.random()*S.banks[kind].length)]}));renderSentence();sndGood();confetti(sentenceEl)}); q('.sb-speak').addEventListener('click',()=>{const text=sentenceText();if(text&&'speechSynthesis'in window){speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.lang=LANG==='nl'?'nl-NL':'en-GB';speechSynthesis.speak(u);}}); function labels(){ [...kind.options].forEach(o=>o.textContent=T('sentence_'+o.value)); word.placeholder=T('sentenceAdd'); - q('.sb-clear').textContent=T('sentenceClear'); q('.sb-speak').textContent=T('sentenceSpeak'); renderSentence();renderBanks(); + q('.sb-surprise').textContent=T('sentenceSurprise');q('.sb-clear').textContent=T('sentenceClear'); q('.sb-speak').textContent=T('sentenceSpeak'); renderSentence();renderBanks(); } document.addEventListener('langchange',labels);labels(); return {getState:()=>({banks:S.banks,sentence:S.sentence})}; diff --git a/test/widgets.test.js b/test/widgets.test.js index 822ec71..d9878c3 100644 --- a/test/widgets.test.js +++ b/test/widgets.test.js @@ -341,6 +341,7 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px- names:'.np-result', birthday:'.bd{', schedule:'.schedule>div', + light:'.tl-house', mood:'.mood', poll:'.poll', media:'.media-bar', @@ -388,3 +389,42 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px- assert.match(pupil,/card\.style\.width = .min\(100%, \$\{def\.w\}px\)./); assert.ok(pupil.includes('card.style.minWidth = "0"')); }); + +test('alle widgetgroepen delen een kindvriendelijke spelstijl met toegankelijke bediening',async()=>{ + const [css,board]=await Promise.all([readFile('public/css/teach.css','utf8'),readFile('public/js/board.js','utf8')]); + assert.ok(css.includes('.widget:not(.bare){border-top:4px solid var(--widget-accent)}')); + assert.ok(css.includes('.widget-body .tbtn:active{transform:translateY(2px) scale(.985)')); + assert.ok(css.includes('@media(prefers-reduced-motion:reduce)')); + for(const id of ['letters','clock','trees','topography','birthday','light','media']){ + assert.ok(css.includes(`[data-widget="${id}"]`),id+' mist een herkenbaar categorie-accent'); + } + assert.match(board,/id:"light"[^\n]*cat:"class"[^\n]*mount:mountLight/); +}); + +test('compacte widgets hebben nu speldoel, voortgang en directe feedback',async()=>{ + const [flits,hak,dice,names,notes,numberline,learning,sentence,light]=await Promise.all([ + 'flits','hak','dice','names','notes','numberline','learning','sentence','light' + ].map(name=>readFile(`public/js/widgets/${name}.js`,'utf8'))); + assert.ok(flits.includes('class="fl-reflect"')&&flits.includes('S.score++')&&flits.includes('if(assessed)return')); + assert.ok(dice.includes('dc-history')&&dice.includes('history.push(total)')); + assert.ok(names.includes('np-progress')&&names.includes('np-reset')); + assert.ok(notes.includes('notes-swatches')&¬es.includes('notes-count')); + assert.ok(numberline.includes('nlw-score')&&numberline.includes('S.streak++')); + assert.ok(learning.includes("stage.className='fraction-game'")&&learning.includes("className='class-total'")); + assert.ok(sentence.includes('sb-surprise')&&sentence.includes("replace(/\\s+([,.!?])/g")); + assert.ok(light.includes('aria-pressed')&&light.includes('lightStatus')); + + const chunks=hak.slice(hak.indexOf('function soundChunks'),hak.indexOf('/* =========================================================',hak.indexOf('function soundChunks'))); + const context={LANG:'nl'}; + runInNewContext(chunks+';globalThis.examples=[soundChunks("boom"),soundChunks("schaap"),soundChunks("koning")]',context); + assert.deepEqual(JSON.parse(JSON.stringify(context.examples)),[['b','oo','m'],['sch','aa','p'],['k','o','n','i','ng']]); +}); + +test('topografie heeft een inhoudelijk brede naam en de beta-versie is gelijkgetrokken',async()=>{ + const [core,version]=await Promise.all([readFile('public/js/core.js','utf8'),readFile('VERSION','utf8')]); + assert.ok(core.includes('wg_topography:"Topografie"')); + assert.ok(core.includes('wg_topography:"Topography"')); + assert.doesNotMatch(core,/wg_topography:"(?:Topografie Nederland|Map of the Netherlands)"/); + assert.match(core,new RegExp(`const VERSION = "${version.trim().replaceAll('.','\\.')}"`)); + assert.match(version,/^0\.3\.77-beta\s*$/); +});