Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-65 into main AGit
5 changed files with 17 additions and 6 deletions
Showing only changes of commit d02f7de72d - Show all commits

View file

@ -1 +1 @@
0.3.39-beta
0.3.40-beta

View file

@ -1459,3 +1459,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
@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}}
.clock-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}.clock-prompt{margin:0;text-align:center}.clock-lesson{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:14px}.clock{overflow:hidden}.clock-tick{position:absolute!important;left:calc(50% - 1px)!important;bottom:50%!important;width:2px!important;height:8%!important;transform-origin:50% 625%!important;background:var(--ink)!important}.clock-tick.minor{height:4%!important;opacity:.35}.clock-hand{z-index:2}.clock-pin{position:absolute;width:15px;height:15px;border-radius:50%;background:var(--pink);left:calc(50% - 7px);top:calc(50% - 7px);z-index:3}.clock b{left:calc(50% - 11px)!important;top:calc(50% - 11px)!important;width:22px;height:22px;transform:rotate(var(--clock-angle)) translateY(-105px) rotate(calc(-1 * var(--clock-angle)));z-index:1}.clock-digital{font-size:clamp(34px,8vw,58px);font-weight:900;color:var(--blue)}.clock-words{flex-basis:100%;margin:0;font-size:20px;font-weight:900}.clock-choices{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}.clock-feedback{min-height:24px;margin:0;font-weight:900}.clock-feedback.ok{color:var(--green)}.clock-feedback.bad{color:var(--red)}.clock-score{color:var(--muted);font-weight:800}
.clock.interactive{cursor:grab;touch-action:none;user-select:none}.clock.interactive:active{cursor:grabbing}.clock.interactive .clock-hand{pointer-events:none}.clock.interactive:hover{box-shadow:0 0 0 5px var(--accent-soft)}
.clock-drag-hint{color:var(--muted);font-weight:800;text-align:center}

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.3.39-beta";
const VERSION = "0.3.40-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -123,7 +123,7 @@ const I18N = {
wg_schedule:"Dagplanning", wg_schedule_d:"Maak een duidelijke visuele planning voor de klas.",
wg_mood:"Gevoelsmeter", wg_mood_d:"Doe een snelle, anonieme check-in met de klas.",
wg_poll:"Klaspoll", wg_poll_d:"Stem samen op maximaal zes antwoorden.",
widgetSettings:"Instellingen", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Leerwijze", clockStyleAnalogue:"Analoog", clockStyleDigital:"Digitaal", clockStyleWords:"In woorden", clockStyleBoth:"Alles samen", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies Nieuwe opdracht om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", fractionRows:"Rijen", fractionLabels:"Breuken tonen", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw",
widgetSettings:"Instellingen", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Leerwijze", clockStyleAnalogue:"Analoog", clockStyleDigital:"Digitaal", clockStyleWords:"In woorden", clockStyleBoth:"Alles samen", clockDragHint:"Sleep op de klok om de wijzers te verzetten.", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies Nieuwe opdracht om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", fractionRows:"Rijen", fractionLabels:"Breuken tonen", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw",
wg_sentence:"Zinsbouwer", wg_sentence_d:"Bouw zinnen met kleurgecodeerde zinsdelen en voeg eigen woorden toe.",
sentence_who:"Wie?", sentence_does:"Doet wat?", sentence_what:"Wat of waar?", sentenceHint:"Kies woorden om een zin te maken…",
sentenceAdd:"eigen woord of zinsdeel…", sentenceClear:"Wissen", sentenceSpeak:"🔊 Lees voor", sentenceRemove:"Verwijder woord",
@ -321,7 +321,7 @@ const I18N = {
wg_schedule:"Day schedule", wg_schedule_d:"Create a clear visual class schedule.",
wg_mood:"Mood check-in", wg_mood_d:"Run a quick anonymous class check-in.",
wg_poll:"Class poll", wg_poll_d:"Vote together on up to six answers.",
widgetSettings:"Settings", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Learning style", clockStyleAnalogue:"Analogue", clockStyleDigital:"Digital", clockStyleWords:"In words", clockStyleBoth:"All together", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose New task to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", fractionRows:"Rows", fractionLabels:"Show fractions", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo",
widgetSettings:"Settings", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Learning style", clockStyleAnalogue:"Analogue", clockStyleDigital:"Digital", clockStyleWords:"In words", clockStyleBoth:"All together", clockDragHint:"Drag on the clock to move the hands.", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose New task to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", fractionRows:"Rows", fractionLabels:"Show fractions", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo",
wg_sentence:"Sentence builder", wg_sentence_d:"Build sentences with colour-coded parts and add your own words.",
sentence_who:"Who?", sentence_does:"Does what?", sentence_what:"What or where?", sentenceHint:"Choose words to build a sentence…",
sentenceAdd:"your own word or phrase…", sentenceClear:"Clear", sentenceSpeak:"🔊 Read aloud", sentenceRemove:"Remove word",

View file

@ -21,10 +21,10 @@ function mountClock(root,init){
const fill=()=>{const old=+m.value;m.innerHTML='';steps().forEach(v=>m.add(new Option(String(v).padStart(2,'0'),v)));S.m=steps().includes(old)?old:steps().includes(S.m)?S.m:0;m.value=S.m};
const words=(a,b)=>{const n=a===12?1:a+1;if(!b)return T('clockWordsHour').replace('{hour}',a);if(b===15)return T('clockWordsQuarterPast').replace('{hour}',a);if(b===30)return T('clockWordsHalf').replace('{hour}',n);if(b===45)return T('clockWordsQuarterTo').replace('{hour}',n);return T(b<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',b<30?b:60-b).replace('{hour}',b<30?a:n)};
const random=()=>{const v=steps();return{h:1+Math.floor(Math.random()*12),m:v[Math.floor(Math.random()*v.length)]}};
const face=(a,b)=>{const f=document.createElement('div');f.className='clock';f.setAttribute('role','img');f.setAttribute('aria-label',time(a,b));for(let i=0;i<60;i++){const x=document.createElement('i');x.className=i%5?'clock-tick minor':'clock-tick';x.style.transform=`rotate(${i*6}deg)`;f.append(x)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const x=document.createElement('b');x.textContent=n;x.style.setProperty('--clock-angle',`${i*30}deg`);f.append(x)});const ah=document.createElement('i'),am=document.createElement('i'),pin=document.createElement('span');ah.className='clock-hand clock-hour';am.className='clock-hand clock-min';pin.className='clock-pin';ah.style.transform=`rotate(${a*30+b/2}deg)`;am.style.transform=`rotate(${b*6}deg)`;f.append(ah,am,pin);return f};
const face=(a,b)=>{const f=document.createElement('div');f.className='clock';f.setAttribute('role','img');f.setAttribute('aria-label',time(a,b));for(let i=0;i<60;i++){const x=document.createElement('i');x.className=i%5?'clock-tick minor':'clock-tick';x.style.transform=`rotate(${i*6}deg)`;f.append(x)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const x=document.createElement('b');x.textContent=n;x.style.setProperty('--clock-angle',`${i*30}deg`);f.append(x)});const ah=document.createElement('i'),am=document.createElement('i'),pin=document.createElement('span');ah.className='clock-hand clock-hour';am.className='clock-hand clock-min';pin.className='clock-pin';ah.style.transform=`rotate(${a*30+b/2}deg)`;am.style.transform=`rotate(${b*6}deg)`;f.append(ah,am,pin);if(S.game==='set'){f.classList.add('interactive');const move=e=>{const r=f.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2,deg=(Math.atan2(dx,-dy)*180/Math.PI+360)%360;if(f.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=steps();S.m=allowed.reduce((best,v)=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted))<Math.min(Math.abs(best-wanted),60-Math.abs(best-wanted))?v:best);m.value=S.m}else{S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;h.value=S.h}ah.style.transform=`rotate(${S.h*30+S.m/2}deg)`;am.style.transform=`rotate(${S.m*6}deg)`;const digital=stage.querySelector('.clock-digital'),written=stage.querySelector('.clock-words');if(digital)digital.textContent=time(S.h,S.m);if(written)written.textContent=words(S.h,S.m)};f.onpointerdown=e=>{const r=f.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;f.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';f.setPointerCapture(e.pointerId);move(e)};f.onpointermove=e=>{if(f.hasPointerCapture(e.pointerId))move(e)};f.onpointerup=e=>{move(e);f.releasePointerCapture(e.pointerId);delete f.dataset.hand;draw()};}return f};
const feedback=ok=>{S.streak=ok?S.streak+1:0;if(ok)S.score++;ok?sndGood():sndBad();const p=stage.querySelector('.clock-feedback');p.textContent=T(ok?'numberlineGood':'clockTry').replace('{time}',time(S.targetH,S.targetM));p.className='clock-feedback '+(ok?'ok':'bad');stage.querySelector('.clock-score').textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak)};
const check=()=>{if(S.task)feedback(+h.value===S.targetH&&+m.value===S.targetM)},newTask=()=>{const x=random();S.targetH=x.h;S.targetM=x.m;S.task=true;S.h=12;S.m=0;h.value=S.h;fill();m.value=S.m;draw()};
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;S.h=Math.max(1,Math.min(12,+h.value||12));S.m=+m.value||0;stage.className='clock-wrap';stage.innerHTML='';const x=S.task&&S.game!=='set'?{h:S.targetH,m:S.targetM}:{h:S.h,m:S.m},title=document.createElement('h3');title.className='clock-prompt';title.textContent=!S.task?T('clockStart'):S.game==='set'?T('clockSet').replace('{time}',time(S.targetH,S.targetM)):S.game==='read'?T('clockRead'):T('clockChoose');stage.append(title);const lesson=document.createElement('div');lesson.className='clock-lesson';if(['analogue','both'].includes(S.style))lesson.append(face(x.h,x.m));if(['digital','both'].includes(S.style)){const o=document.createElement('output');o.className='clock-digital';o.textContent=time(x.h,x.m);lesson.append(o)}if(['words','both'].includes(S.style)){const p=document.createElement('p');p.className='clock-words';p.textContent=words(x.h,x.m);lesson.append(p)}stage.append(lesson);if(S.task&&S.game==='choose'){const a=[{h:S.targetH,m:S.targetM}];while(a.length<3){const c=random();if(!a.some(x=>x.h===c.h&&x.m===c.m))a.push(c)}a.sort(()=>Math.random()-.5);const choices=document.createElement('div');choices.className='clock-choices';a.forEach(c=>choices.append(btn(time(c.h,c.m),()=>feedback(c.h===S.targetH&&c.m===S.targetM))));stage.append(choices)}const status=document.createElement('p'),score=document.createElement('small');status.className='clock-feedback';score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);stage.append(status,score)};
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;S.h=Math.max(1,Math.min(12,+h.value||12));S.m=+m.value||0;stage.className='clock-wrap';stage.innerHTML='';const x=S.task&&S.game!=='set'?{h:S.targetH,m:S.targetM}:{h:S.h,m:S.m},title=document.createElement('h3');title.className='clock-prompt';title.textContent=!S.task?T('clockStart'):S.game==='set'?T('clockSet').replace('{time}',time(S.targetH,S.targetM)):S.game==='read'?T('clockRead'):T('clockChoose');stage.append(title);const lesson=document.createElement('div');lesson.className='clock-lesson';if(['analogue','both'].includes(S.style))lesson.append(face(x.h,x.m));if(['digital','both'].includes(S.style)){const o=document.createElement('output');o.className='clock-digital';o.textContent=time(x.h,x.m);lesson.append(o)}if(['words','both'].includes(S.style)){const p=document.createElement('p');p.className='clock-words';p.textContent=words(x.h,x.m);lesson.append(p)}stage.append(lesson);if(S.game==='set'&&['analogue','both'].includes(S.style)){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');stage.append(hint)}if(S.task&&S.game==='choose'){const a=[{h:S.targetH,m:S.targetM}];while(a.length<3){const c=random();if(!a.some(x=>x.h===c.h&&x.m===c.m))a.push(c)}a.sort(()=>Math.random()-.5);const choices=document.createElement('div');choices.className='clock-choices';a.forEach(c=>choices.append(btn(time(c.h,c.m),()=>feedback(c.h===S.targetH&&c.m===S.targetM))));stage.append(choices)}const status=document.createElement('p'),score=document.createElement('small');status.className='clock-feedback';score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);stage.append(status,score)};
level.onchange=()=>{S.level=level.value;fill();S.task=false;draw()};style.onchange=draw;game.onchange=()=>{S.task=false;draw()};h.oninput=m.onchange=draw;fill();learningShell(root,[field(T('clockLevel'),level),field(T('clockLearningStyle'),style),field(T('clockGame'),game),field(T('clockHour'),h),field(T('clockMinute'),m),btn(T('numberlineNew'),newTask),btn(T('numberlineCheck'),check,'')],stage);draw();return{getState:()=>({...S})};
}
function mountPlacevalue(root,init){

View file

@ -65,3 +65,10 @@ test('oefenklok biedt niveaus leerwijzen en meerdere spellen', async()=>{
for(const value of ["'hour'","'quarter'","'five'","'minute'","'analogue'","'digital'","'words'","'both'","'set'","'read'","'choose'"])assert.ok(source.includes(value),value);
for(const key of ['clockLevel','clockLearningStyle','clockGame','clockScore'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
});
test('analoge oefenklok is met aanwijzer en aanraking bedienbaar', async()=>{
const [source,css]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8')]);
for(const feature of ['onpointerdown','onpointermove','setPointerCapture','releasePointerCapture','getBoundingClientRect'])assert.ok(source.includes(feature),feature);
assert.match(css,/clock\.interactive[^}]*touch-action:none/);
});