Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5
13 changed files with 200 additions and 62 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.76-beta
|
||||
0.3.77-beta
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="dc">
|
||||
|
|
@ -13,8 +13,10 @@ function mountDice(root, initState){
|
|||
</div>
|
||||
<div class="mt-score dc-sumlbl"></div>
|
||||
</div>
|
||||
<p class="play-mission dc-mission"></p>
|
||||
<div class="dc-row"></div>
|
||||
<div class="dc-sum"></div>
|
||||
<div class="dc-sum" aria-live="polite"></div>
|
||||
<div class="dc-history"></div>
|
||||
<button class="tbtn dc-roll"></button>
|
||||
</div>`;
|
||||
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]}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="mt">
|
||||
|
|
@ -14,10 +14,15 @@ function mountFlits(root, initState){
|
|||
<select class="fl-sel fl-dur"></select>
|
||||
<div class="mt-score fl-cnt"></div>
|
||||
</div>
|
||||
<p class="play-mission fl-mission"></p>
|
||||
<div class="mt-card">
|
||||
<div class="fl-word dim">• • •</div>
|
||||
<div class="fl-pic"></div>
|
||||
</div>
|
||||
<div class="fl-reflect" hidden>
|
||||
<p class="fl-question"></p>
|
||||
<div><button class="tbtn fl-got"></button><button class="tbtn ghost fl-again"></button></div>
|
||||
</div>
|
||||
<div class="trow" style="display:flex;gap:10px;justify-content:center;flex:none;">
|
||||
<button class="tbtn fl-flash"></button>
|
||||
<button class="tbtn ghost fl-show"></button>
|
||||
|
|
@ -26,7 +31,7 @@ function mountFlits(root, initState){
|
|||
</div>`;
|
||||
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}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;i<word.length;){
|
||||
const rest=word.slice(i).toLocaleLowerCase(lang==='nl'?'nl':'en');
|
||||
const match=patterns.find(pattern=>rest.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){
|
|||
<select class="fl-sel hk-src"></select>
|
||||
<div class="mt-score hk-cnt"></div>
|
||||
</div>
|
||||
<p class="play-mission hk-hint"></p>
|
||||
<div class="mt-card">
|
||||
<div class="fl-pic hk-pic"></div>
|
||||
<div class="hk-word"></div>
|
||||
|
|
@ -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}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;n<d;n++){const c=document.createElement('button');c.type='button';c.style.setProperty('--fraction-hue',d*31);c.textContent=S.show?`1/${d}`:'';c.className='fraction-piece';c.onclick=()=>c.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<d;n++){const c=document.createElement('button');c.type='button';c.style.setProperty('--fraction-hue',d*31);c.textContent=S.show?`1/${d}`:'';c.className='fraction-piece';c.classList.toggle('selected',S.activeD===d&&n<S.activeN);c.setAttribute('aria-label',`${n+1}/${d}`);c.onclick=()=>{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}))})};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,19 +6,30 @@ function mountLight(root, initState){
|
|||
let on = (initState && initState.on) || "green";
|
||||
root.innerHTML = `
|
||||
<div class="tl">
|
||||
<p class="play-mission tl-mission"></p>
|
||||
<div class="tl-house">
|
||||
<button class="tl-lamp red" data-c="red"></button>
|
||||
<button class="tl-lamp orange" data-c="orange"></button>
|
||||
<button class="tl-lamp green" data-c="green"></button>
|
||||
<div class="tl-option"><button class="tl-lamp red" data-c="red"></button><span><strong></strong><small></small></span></div>
|
||||
<div class="tl-option"><button class="tl-lamp orange" data-c="orange"></button><span><strong></strong><small></small></span></div>
|
||||
<div class="tl-option"><button class="tl-lamp green" data-c="green"></button><span><strong></strong><small></small></span></div>
|
||||
</div>
|
||||
<output class="tl-status" aria-live="polite"></output>
|
||||
</div>`;
|
||||
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); });
|
||||
});
|
||||
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}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="np">
|
||||
<div class="np-result">🎯</div>
|
||||
<div class="np-progress" aria-live="polite"></div>
|
||||
<textarea class="np-list" spellcheck="false"></textarea>
|
||||
<div class="np-row">
|
||||
<button class="tbtn np-pick"></button>
|
||||
<button class="tbtn ghost np-reset"></button>
|
||||
<label><input type="checkbox" class="np-nr"><span class="np-nrl"></span></label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -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]}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,17 @@
|
|||
Notes widget
|
||||
==========================================================*/
|
||||
function mountNotes(root, initState){
|
||||
const S={theme:initState?.theme||'sun',large:!!initState?.large};
|
||||
root.innerHTML='<div class="notes-app"><div class="notes-tools"><span class="notes-color-label"></span><div class="notes-swatches"></div><button class="notes-size" type="button">Aa</button><output class="notes-count"></output></div></div>';
|
||||
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}) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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=`
|
||||
<div class="nlw">
|
||||
<div class="nlw-settings">
|
||||
|
|
@ -9,7 +9,7 @@ function mountNumberline(root, initState){
|
|||
<label><span class="nlw-step-label"></span><select class="nlw-step"><option>1</option><option>2</option><option>5</option><option>10</option></select></label>
|
||||
<button class="tbtn ghost nlw-new" type="button"></button>
|
||||
</div>
|
||||
<div class="nlw-task"></div>
|
||||
<div class="nlw-playtop"><div class="nlw-task"></div><output class="nlw-score"></output></div>
|
||||
<div class="nlw-scroll"><div class="nlw-line"></div></div>
|
||||
<div class="nlw-actions">
|
||||
<button class="tbtn ghost nlw-minus" type="button">−</button>
|
||||
|
|
@ -20,7 +20,7 @@ function mountNumberline(root, initState){
|
|||
<div class="nlw-feedback" aria-live="polite"></div>
|
||||
</div>`;
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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){
|
|||
<input class="sb-word" type="text" maxlength="40">
|
||||
<button class="tbtn ghost sb-add-btn" type="button">+</button>
|
||||
</div>
|
||||
<div class="sb-actions"><button class="tbtn ghost sb-back" type="button">↶</button><button class="tbtn ghost sb-clear" type="button"></button><button class="tbtn sb-speak" type="button"></button></div>
|
||||
<div class="sb-actions"><button class="tbtn ghost sb-surprise" type="button"></button><button class="tbtn ghost sb-back" type="button">↶</button><button class="tbtn ghost sb-clear" type="button"></button><button class="tbtn sb-speak" type="button"></button></div>
|
||||
</div>`;
|
||||
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})};
|
||||
|
|
|
|||
|
|
@ -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*$/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue