feat: compacter instellingenmenu voor acht widgets (v0.4.80-beta)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 36s

Bij veel widgets nam de instellingenstrook te veel hoogte in, vooral bij een
smal widgetvenster waar de rij naar meerdere regels uitwaaierde. Nieuw
gedeeld patroon: besturing die voortdurend nodig is (kleur, niveau,
woordbron) blijft in één compacte rij zichtbaar; besturing die maar zelden
wordt aangepast (thema, geluid aan/uit, invoerwijze, bereik) gaat achter een
inklapbaar ⚙-paneel. Dit patroon bestond al voor klok/geld/rekengetallen/
wereld (learningShell() in learning.js) - nu als widgetGear()/gearField() in
core.js beschikbaar voor de rest van de app, met dezelfde .edu-settings-
opmaak zodat het overal hetzelfde oogt en werkt.

- kleurplaten: twee volle rijen (thema/plaat + kwast/emmer/gum/kleur/dikte/
  hulp/reset/klaar) teruggebracht naar één vaste rij (gereedschap, kleur,
  dikte, klaar) plus het ingeklapte paneel (thema, plaat, kleurhulp, reset).
  Losstaand: coloring.css gebruikte @media(max-width:620px) in plaats van de
  @container widget-conventie die de rest van de app gebruikt, waardoor die
  regels nooit afgingen bij het verkleinen van het widgetvenster zelf - nu
  gecorrigeerd.
- letterblokken: geluid, invoerwijze en aantal kaarten in het paneel; niveau
  en woordbron blijven vast.
- rekenwidgets (optellen/aftrekken/keer/delen/procenten): geluid en
  invoerwijze in het paneel; niveau blijft vast.
- ballonnenwoord: geluid in het paneel.
- flitswoorden: flitstijd in het paneel.
- getallenlijn: het hele bereik-instelblok (van/tot/stap/nieuw) in het
  paneel - dat stond toch al uit voor leerlingen met een beheerd niveau.
- zinsbouwer: het rijtje voor eigen woorden toevoegen in het paneel; de
  speelknoppen (verrassing/ongedaan/wissen/voorlezen) blijven vast.
- timer: geluid en weergavevorm in het paneel; starttijden en start/reset
  blijven vast.
- geluidsmeter: de twee drempelschuifjes plus de privacynotitie in het
  paneel.

Widgets die dit patroon al hadden (klok, geld, breuken, plaatswaarde,
woordsoorten, bomen/vogels/planten, topografie, stemming, stemming-poll,
dagplanning) of waar de rij al compact genoeg was (schrijfblad, hakken en
plakken, dobbelstenen, notities, naamtrekker) zijn ongewijzigd gelaten.

Geverifieerd door elke aangepaste widget live via de echte makeWidget()/
REGISTRY in een headless browser te openen, het paneel open en dicht te
klappen en te controleren op consolefouten - ook op de minimale
widgetbreedte (coloring, minW 390). 202 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS
This commit is contained in:
Ramon 2026-07-30 16:46:37 +02:00
parent f4a4cbd28c
commit d8610b010d
12 changed files with 93 additions and 47 deletions

View file

@ -1 +1 @@
0.4.79-beta
0.4.80-beta

View file

@ -6,11 +6,15 @@
bestand zet nooit een `fill` op een kleurvak - dat zou van de vulling van de
verfemmer winnen en de verf onzichtbaar maken. */
.coloring-app{--coloring-ink:#1c293d;--coloring-detail:#2d3e56;height:100%;min-height:0;display:flex;flex-direction:column;background:linear-gradient(180deg,color-mix(in srgb,var(--orange) 4%,var(--surface)),var(--surface));color:var(--ink)}
.coloring-choices,.coloring-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));background:linear-gradient(100deg,color-mix(in srgb,var(--orange) 10%,var(--surface)),color-mix(in srgb,var(--purple) 5%,var(--surface)))}
/* de veelgebruikte rij (kwast/emmer/gum, kleur, dikte) blijft altijd
zichtbaar; thema, plaat, kleurhulp en reset - die je zelden aanpast -
zitten achter het -tandwiel (.edu-settings, zie teach.css) zodat ze niet
permanent ruimte innemen naast wat je continu gebruikt */
.coloring-tools{display:flex;align-items:center;gap:7px;flex-wrap:wrap;padding:9px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));background:color-mix(in srgb,var(--surface) 96%,var(--orange))}
.coloring-choices{gap:8px}
.coloring-choices label{display:grid;grid-template-columns:auto minmax(120px,1fr);align-items:center;gap:6px;color:var(--muted);font-size:11px;font-weight:850}
.coloring-choices select{height:36px;max-width:230px;border:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));border-radius:11px;padding:0 30px 0 10px;background:var(--surface);color:var(--ink);box-shadow:0 2px 8px rgba(30,50,90,.07);font:inherit;font-weight:750}
.coloring-fit{margin-left:auto;padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 75%,var(--orange));border-radius:999px;background:color-mix(in srgb,var(--surface) 92%,var(--orange));color:var(--muted);box-shadow:0 2px 8px rgba(30,50,90,.06);font-size:11px;font-weight:800;white-space:nowrap}
.coloring-tools{gap:7px;background:color-mix(in srgb,var(--surface) 96%,var(--orange))}
.coloring-fit{padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 75%,var(--orange));border-radius:999px;background:color-mix(in srgb,var(--surface) 92%,var(--orange));color:var(--muted);box-shadow:0 2px 8px rgba(30,50,90,.06);font-size:11px;font-weight:800;white-space:nowrap}
.coloring-tool-buttons,.coloring-palette,.coloring-sizes{display:flex;align-items:center;gap:4px;padding:4px;border:1px solid color-mix(in srgb,var(--line) 78%,transparent);border-radius:13px;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 82%,#fff),var(--surface-2));box-shadow:inset 0 1px 0 rgba(255,255,255,.72),0 2px 7px rgba(30,50,90,.06)}
.coloring-tools button{width:36px;height:36px;display:grid;place-items:center;border:0;border-radius:10px;background:transparent;color:var(--ink);font:inherit;font-size:18px;cursor:pointer;transition:transform .14s ease,background .14s ease,box-shadow .14s ease}
.coloring-tool-buttons button{width:auto;min-width:72px;grid-auto-flow:column;grid-template-columns:auto auto;gap:5px;padding:0 9px}
@ -24,10 +28,10 @@
.coloring-size.on{outline:2px solid var(--orange);outline-offset:-2px}
.coloring-custom{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:10px;font-weight:800}
.coloring-custom input{width:36px;height:36px;padding:3px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 2px 7px rgba(30,50,90,.08);cursor:pointer}
.coloring-tools .coloring-guide-btn{margin-left:auto;background:color-mix(in srgb,var(--teal) 14%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--teal) 20%,transparent)}
.coloring-tools .coloring-guide-btn.on{outline:2px solid var(--teal);outline-offset:-2px}
.coloring-tools .coloring-reset{background:var(--red-soft);color:var(--red);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--red) 18%,transparent)}
.coloring-tools .coloring-done{width:auto;min-width:112px;padding:0 14px;color:#fff;box-shadow:0 4px 10px color-mix(in srgb,var(--orange) 24%,transparent);font-size:13px}
.coloring-choices .coloring-guide-btn{background:color-mix(in srgb,var(--teal) 14%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--teal) 20%,transparent)}
.coloring-choices .coloring-guide-btn.on{outline:2px solid var(--teal);outline-offset:-2px}
.coloring-choices .coloring-reset{background:var(--red-soft);color:var(--red);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--red) 18%,transparent)}
.coloring-tools .coloring-done{width:auto;min-width:112px;margin-left:auto;padding:0 14px;color:#fff;box-shadow:0 4px 10px color-mix(in srgb,var(--orange) 24%,transparent);font-size:13px}
.coloring-stage{flex:1;min-height:0;overflow:auto;display:grid;place-items:center;padding:18px;background:radial-gradient(circle at 12% 18%,color-mix(in srgb,var(--orange) 13%,transparent),transparent 31%),radial-gradient(circle at 88% 82%,color-mix(in srgb,var(--purple) 10%,transparent),transparent 34%),linear-gradient(145deg,var(--surface-2),color-mix(in srgb,var(--purple) 5%,var(--surface)))}
/* het papier vangt alle aanwijzing op; binnen de SVG wordt meetkundig
aangewezen met isPointInFill/isPointInStroke */
@ -57,17 +61,19 @@
.coloring-app[data-tool="eraser"] .coloring-paper{cursor:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30'%3E%3Ctext y='24' font-size='23'%3E%F0%9F%A7%BD%3C/text%3E%3C/svg%3E") 6 24,pointer}
.widget[data-widget="coloring"]{--widget-accent:var(--orange)}
@media(max-width:620px){
.coloring-choices,.coloring-tools{padding:6px;gap:4px}
/* @container in plaats van @media: dit moet reageren op de breedte van het
widget-venster (dat kan smal staan op een brede monitor), niet op de
breedte van de hele browser - zie .widget-body{container-name:widget} */
@container widget (max-width:620px){
.coloring-tools{padding:6px;gap:4px}
.coloring-choices label{grid-template-columns:1fr;gap:2px;flex:1;min-width:125px}
.coloring-choices select{width:100%;max-width:none;height:32px}
.coloring-fit{width:100%;margin:0;text-align:center}
.coloring-fit{width:100%;text-align:center}
.coloring-tools button{width:32px;height:32px;font-size:16px}
.coloring-tool-buttons button{min-width:36px;width:36px;padding:0}.coloring-tool-label{display:none}
.coloring-color i{width:18px;height:18px}
.coloring-custom span{display:none}.coloring-custom input{width:32px;height:32px}
.coloring-tools .coloring-guide-btn{margin-left:0}
.coloring-tools .coloring-done{min-width:90px;font-size:11px}
.coloring-tools .coloring-done{min-width:90px;margin-left:0;font-size:11px}
.coloring-stage{place-items:start center;padding:8px}
.coloring-paper{min-width:360px;border-radius:10px}
.coloring-hint{font-size:10px}

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.4.79-beta";
const VERSION = "0.4.80-beta";
/* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte
achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk
zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en
@ -118,7 +118,7 @@ const I18N = {
wFull:"Maximaliseren", wUnfull:"Terugzetten",
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
wg_flits:"Flitswoorden", wg_flits_d:"Een woord flitst kort in beeld lezen voordat het verdwijnt!",
flFlash:"⚡ Flits", flShow:"👁 Toon", flNext:"Volgende ▸", flSec:"sec", flMission:"Lees het woord voordat het verdwijnt.", flQuestion:"Kon je het woord zelf lezen?", flGotIt:"🌟 Gelukt!", flAgain:"Nog oefenen", flScore:"{score} sterren",
flFlash:"⚡ Flits", flShow:"👁 Toon", flNext:"Volgende ▸", flSec:"sec", flDurLabel:"Flitstijd", flMission:"Lees het woord voordat het verdwijnt.", flQuestion:"Kon je het woord zelf lezen?", flGotIt:"🌟 Gelukt!", flAgain:"Nog oefenen", flScore:"{score} sterren",
wg_dice:"Dobbelstenen", wg_dice_d:"Gooi 1, 2 of 3 grote dobbelstenen.",
diceRoll:"🎲 Gooi!", diceSum:"Samen", diceMission:"Gooi, tel de ogen en controleer de uitkomst.", diceHistory:"Vorige worpen",
wg_light:"Klassenstoplicht", wg_light_d:"Maak in één oogopslag duidelijk of kinderen luisteren, fluisteren of rustig mogen werken.",
@ -551,7 +551,7 @@ const I18N = {
wFull:"Maximize", wUnfull:"Restore",
wZoom:"Content bigger or smaller (low vision)",
wg_flits:"Flash words", wg_flits_d:"A word flashes briefly read it before it disappears!",
flFlash:"⚡ Flash", flShow:"👁 Show", flNext:"Next ▸", flSec:"sec", flMission:"Read the word before it disappears.", flQuestion:"Could you read the word by yourself?", flGotIt:"🌟 Got it!", flAgain:"Practise again", flScore:"{score} stars",
flFlash:"⚡ Flash", flShow:"👁 Show", flNext:"Next ▸", flSec:"sec", flDurLabel:"Flash time", flMission:"Read the word before it disappears.", flQuestion:"Could you read the word by yourself?", flGotIt:"🌟 Got it!", flAgain:"Practise again", flScore:"{score} stars",
wg_dice:"Dice", wg_dice_d:"Roll 1, 2 or 3 big dice.",
diceRoll:"🎲 Roll!", diceSum:"Total", diceMission:"Roll, count the dots and check the total.", diceHistory:"Previous rolls",
wg_light:"Class traffic light", wg_light_d:"Show at a glance whether children listen, whisper or work quietly.",
@ -917,6 +917,29 @@ const T = k => I18N[LANG][k] ?? I18N.nl[k] ?? k;
are visual pixels, absolute positions are layout pixels divide by VZ() */
const VZ = ()=>{ const z = parseFloat(document.body.style.zoom); return z>0 ? z : 1; };
/* Herbruikbaar instellingen-tandwiel: zeldzaam gebruikte besturing (thema,
niveau-stijl, achtergrond...) gaat ingeklapt achter een , zodat die niet
in dezelfde rij hoeft te concurreren met knoppen die een leerling
voortdurend gebruikt (kwast, kleur, dikte). Zelfde <details>-opbouw en
CSS-klassen als learningShell() in widgets/learning.js, maar los
bruikbaar: de aanroeper bepaalt zelf waar het tandwiel in de eigen
layout wordt ingevoegd. */
function widgetGear(controls,label){
const details=document.createElement("details");details.className="edu-settings widget-gear";
const summary=document.createElement("summary");
summary.append(document.createTextNode("⚙ "));
const span=document.createElement("span");span.textContent=label||T("widgetSettings");
summary.appendChild(span);
const box=document.createElement("div");box.className="edu-controls";
box.append(...controls);
details.append(summary,box);
return details;
}
/* Kleine bouwstenen voor widgetGear()-inhoud, zelfde vorm als de
gelijknamige helpers in widgets/learning.js (bewust hier gedupliceerd in
plaats van gedeeld, want learning.js laadt pas de meeste widgets). */
function gearField(label,el){const w=document.createElement("label");const s=document.createElement("span");s.textContent=label;w.append(s,el);return w}
function applyI18n(){
document.querySelectorAll("[data-i18n]").forEach(el=>{ el.textContent = T(el.dataset.i18n); });
document.querySelectorAll("#langToggle button").forEach(b=>b.classList.toggle("active", b.dataset.lang===LANG));

View file

@ -12,7 +12,6 @@ function mountBall(root, initState, opts){
root.innerHTML = `
<div class="mt">
<div class="mt-top">
<button class="lg-snd"></button>
<select class="fl-sel bl-src"></select>
<div class="mt-score bl-score"></div>
</div>
@ -26,10 +25,12 @@ function mountBall(root, initState, opts){
<div class="lg-tray bl-kbd lg-kbd" style="display:flex;"></div>
</div>`;
const el = s => root.querySelector(s);
const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd";
el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([gearField(T("sndWidget"),sndBtn)]));
const srcSel = el(".bl-src"), balloonsEl = el(".bl-balloons"), picEl=el(".bl-pic"),
wordEl = el(".bl-word"), msgEl = el(".bl-msg"),
nextBtn = el(".bl-next"), kbdEl = el(".bl-kbd"),
scoreEl = el(".bl-score"), sndBtn = el(".lg-snd");
scoreEl = el(".bl-score");
function list(){
if(S.assignmentWords?.length)return languageWidgetWords(S.assignmentWords,S.yearGroup,S.level,"ball");

View file

@ -191,20 +191,21 @@ function mountColoring(root,initState,opts={}){
const clipStrokes=coloringClipsStrokes(yearGroup,level);
root.innerHTML=`<div class="coloring-app">
<div class="coloring-choices">
<label><span class="coloring-theme-label"></span><select class="coloring-theme"></select></label>
<label><span class="coloring-page-label"></span><select class="coloring-page"></select></label>
<span class="coloring-fit"></span>
</div>
<div class="coloring-tools">
<div class="coloring-tool-buttons"></div>
<div class="coloring-palette"></div>
<label class="coloring-custom"><span></span><input type="color"></label>
<div class="coloring-sizes"></div>
<button class="coloring-guide-btn" type="button">🎯</button>
<button class="coloring-reset" type="button"></button>
<button class="coloring-done tbtn" type="button"></button>
</div>
<details class="edu-settings widget-gear"><summary> <span class="coloring-settings-label"></span></summary>
<div class="coloring-choices edu-controls">
<label><span class="coloring-theme-label"></span><select class="coloring-theme"></select></label>
<label><span class="coloring-page-label"></span><select class="coloring-page"></select></label>
<span class="coloring-fit"></span>
<button class="coloring-guide-btn" type="button">🎯</button>
<button class="coloring-reset" type="button"></button>
</div></details>
<div class="coloring-stage"><div class="coloring-paper" role="img"></div></div>
<div class="coloring-hint" aria-live="polite"></div>
</div>`;
@ -337,6 +338,7 @@ function mountColoring(root,initState,opts={}){
return T(tool==="bucket"?"colorBucketHint":tool==="eraser"?"colorEraserHint":"colorBrushHint")+" "+levelHint();
}
function updateTexts(){
root.querySelector(".coloring-settings-label").textContent=T("widgetSettings");
root.querySelector(".coloring-theme-label").textContent=T("colorTheme");
root.querySelector(".coloring-page-label").textContent=T("colorPage");
root.querySelector(".coloring-fit").textContent=T("colorProfile").replace("{year}",yearGroup).replace("{level}",level);

View file

@ -13,7 +13,6 @@ function mountFlits(root, initState, opts){
<div class="mt">
<div class="mt-top">
<select class="fl-sel fl-src"></select>
<select class="fl-sel fl-dur"></select>
<div class="mt-score fl-cnt"></div>
</div>
<p class="play-mission fl-mission"></p>
@ -32,7 +31,9 @@ function mountFlits(root, initState, opts){
</div>
</div>`;
const el = s => root.querySelector(s);
const srcSel = el(".fl-src"), durSel = el(".fl-dur"),
const durSel=document.createElement("select");durSel.className="fl-sel fl-dur";
el(".mt").insertBefore(widgetGear([gearField(T("flDurLabel"),durSel)]),el(".mt-top").nextSibling);
const srcSel = el(".fl-src"),
wordEl = el(".fl-word"), picEl = el(".fl-pic"), cntEl = el(".fl-cnt"), reflect=el(".fl-reflect");
function list(){

View file

@ -21,13 +21,10 @@ function mountLetterGame(root, initState, opts){
root.innerHTML = `
<div class="lg">
<div class="lg-top">
<button class="lg-snd"></button>
<button class="lg-mode"></button>
<div class="lg-levels">
<button data-l="1"></button><button data-l="2"></button><button data-l="3"></button>
</div>
<div class="lg-cards"></div>
<select class="lg-num"></select>
<div class="lg-progress"><span class="lg-count"></span><span class="lg-stars"></span></div>
</div>
<div class="lg-instr"></div>
@ -89,8 +86,16 @@ function mountLetterGame(root, initState, opts){
instrEl = el(".lg-instr"), nextBtn = el(".lg-next"), doneEl = el(".lg-done"),
countEl = el(".lg-count"), starsEl = el(".lg-stars"), cardsEl = el(".lg-cards");
/* zeldzaam gebruikte besturing (geluid, invoerwijze, aantal kaarten) achter
een tandwiel: die concurreerden eerder met de niveauknoppen om ruimte in
één brede rij, terwijl ze maar zelden worden aangepast */
const numSel=document.createElement("select");numSel.className="lg-num";
const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd";
const modeBtn=document.createElement("button");modeBtn.type="button";modeBtn.className="lg-mode";
el(".lg-top").insertAdjacentElement("beforebegin",widgetGear([
gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn),gearField(T("numWords"),numSel)
]));
/* card buttons (built dynamically: N cards + star + edit) */
const numSel = root.querySelector(".lg-num");
function buildNumSel(){
numSel.innerHTML = "";
[4,6,8,10,"all"].forEach(v=>{
@ -106,7 +111,6 @@ function mountLetterGame(root, initState, opts){
S.count = numSel.value==="all" ? "all" : +numSel.value;
newRound(); render();
});
const sndBtn = root.querySelector(".lg-snd");
sndBtn.addEventListener("click", ()=>{
S.snd = !S.snd;
updateSndBtn();
@ -119,7 +123,6 @@ function mountLetterGame(root, initState, opts){
modeBtn.title = T("amode");
root.querySelector(".lg").classList.toggle("typemode", S.amode==="type");
}
const modeBtn = root.querySelector(".lg-mode");
modeBtn.addEventListener("click", ()=>{
S.amode = S.amode==="blocks" ? "type" : "blocks";
updateSndBtn();

View file

@ -21,8 +21,6 @@ function mountMath(root, initState, mode, opts){
root.innerHTML = `
<div class="mt blocks">
<div class="mt-top">
<button class="lg-snd"></button>
<button class="lg-mode"></button>
<div class="lg-levels">
<button data-l="1"></button><button data-l="2"></button><button data-l="3"></button>
</div>
@ -56,9 +54,11 @@ function mountMath(root, initState, mode, opts){
fbEl.appendChild(cheer);
}
}
const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd";
const modeBtn=document.createElement("button");modeBtn.type="button";modeBtn.className="lg-mode";
el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn)]));
const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"),
scoreEl = el(".mt-score"), doneEl = el(".mt-done"),
sndBtn = el(".lg-snd"), modeBtn = el(".lg-mode"),
padEl = el(".mt-pad"), slotsEl = el(".mt-slots"), trayEl = el(".mt-tray");
/* ---- digit blocks with dot patterns (like the letter blocks) ---- */

View file

@ -21,11 +21,14 @@ function mountNoise(root, initState){
</div>
<output class="nm-status" aria-live="polite"></output>
<button class="tbtn nm-toggle"></button>
<details class="edu-settings widget-gear"><summary> <span class="nm-settings-label"></span></summary>
<div class="edu-controls">
<div class="nm-zones">
<label class="nm-zone-row"><span class="nm-zone-lbl"></span><input type="range" class="nm-z1" min="10" max="90" step="5"></label>
<label class="nm-zone-row"><span class="nm-zone-lbl"></span><input type="range" class="nm-z2" min="10" max="95" step="5"></label>
</div>
<p class="nm-privacy"></p>
</div></details>
</div>`;
const gauge = root.querySelector(".nm-gauge"),
fill = root.querySelector(".nm-fill"),
@ -112,6 +115,7 @@ function mountNoise(root, initState){
watchWarn();
function labels(){
root.querySelector(".nm-mission").textContent = T("wg_noise_d");
root.querySelector(".nm-settings-label").textContent = T("widgetSettings");
toggle.textContent = running ? "⏸ " + T("noiseStop") : "🎙️ " + T("noiseStart");
root.querySelectorAll(".nm-zone-lbl")[0].textContent = T("noiseZoneGreen");
root.querySelectorAll(".nm-zone-lbl")[1].textContent = T("noiseZoneYellow");

View file

@ -5,12 +5,13 @@ function mountNumberline(root, initState){
if(S.managedLearningLevel&&rules){S.min=rules.numberlineMin;S.max=rules.numberlineMax;S.step=rules.numberlineStep;S.value=S.min;S.target=S.min+S.step;}
root.innerHTML=`
<div class="nlw">
<div class="nlw-settings">
<details class="edu-settings widget-gear"><summary> <span class="nlw-settings-label"></span></summary>
<div class="nlw-settings edu-controls">
<label><span class="nlw-from-label"></span><input class="nlw-min" type="number" min="-1000000" max="999999"></label>
<label><span class="nlw-to-label"></span><input class="nlw-max" type="number" min="-999999" max="1000000"></label>
<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></details>
<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">
@ -62,6 +63,7 @@ function mountNumberline(root, initState){
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-settings-label').textContent=T('widgetSettings');
q('.nlw-from-label').textContent=T('numberlineFrom'); q('.nlw-to-label').textContent=T('numberlineTo');
q('.nlw-step-label').textContent=T('numberlineStep'); q('.nlw-new').textContent=T('numberlineNew');
q('.nlw-check').textContent=T('numberlineCheck'); render();

View file

@ -26,11 +26,12 @@ function mountSentence(root, initState){
<div class="sb">
<div class="sb-sentence" aria-live="polite"></div>
<div class="sb-banks"></div>
<div class="sb-add">
<details class="edu-settings widget-gear"><summary> <span class="sb-add-label"></span></summary>
<div class="sb-add edu-controls">
<select class="sb-kind"><option value="who"></option><option value="does"></option><option value="what"></option></select>
<input class="sb-word" type="text" maxlength="40">
<button class="tbtn ghost sb-add-btn" type="button"></button>
</div>
</div></details>
<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');
@ -64,6 +65,7 @@ function mountSentence(root, initState){
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(){
q('.sb-add-label').textContent=T('widgetSettings');
[...kind.options].forEach(o=>o.textContent=T('sentence_'+o.value)); word.placeholder=T('sentenceAdd');
q('.sb-surprise').textContent=T('sentenceSurprise');q('.sb-clear').textContent=T('sentenceClear'); q('.sb-speak').textContent=T('sentenceSpeak'); renderSentence();renderBanks();
}

View file

@ -14,14 +14,15 @@ function mountTimer(root, initState){
}
root.innerHTML = `
<div class="timer m-both">
<div class="topts">
<details class="edu-settings widget-gear"><summary> <span class="topts-label"></span></summary>
<div class="topts edu-controls">
<button class="bsnd ticon"></button>
<div class="tseg">
<button data-m="dig">🔢</button>
<button data-m="pie">🔴</button>
<button data-m="both">🔢🔴</button>
</div>
</div>
</div></details>
<div class="tstage">
<svg class="tpie" viewBox="0 0 150 150">
<circle cx="75" cy="75" r="70" fill="#eef3f9" stroke="#d5dfeb" stroke-width="2"/>
@ -78,6 +79,7 @@ function mountTimer(root, initState){
drawPie();
}
function labels(){
root.querySelector(".topts-label").textContent = T("widgetSettings");
bstart.textContent = (iv ? "⏸ "+T("pause") : "▶ "+T("start"));
breset.textContent = "↺ "+T("reset");
presets.querySelectorAll("button").forEach(b=>b.textContent = b.dataset.m+" "+T("min"));