Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-66 into main AGit
4 changed files with 20 additions and 8 deletions
Showing only changes of commit ecf60304a9 - Show all commits

View file

@ -1 +1 @@
0.3.65-beta 0.3.66-beta

View file

@ -1596,6 +1596,10 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.clock-task-card h3{margin:0;text-align:center;font-size:clamp(18px,3vw,24px)}.clock-intro{max-width:440px;margin:0;text-align:center;color:var(--muted);font-weight:700;line-height:1.5} .clock-task-card h3{margin:0;text-align:center;font-size:clamp(18px,3vw,24px)}.clock-intro{max-width:440px;margin:0;text-align:center;color:var(--muted);font-weight:700;line-height:1.5}
.clock-target{width:100%;min-height:88px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;padding:10px;border-radius:15px;background:var(--surface-2)}.clock-target>span:first-child{flex-basis:100%;color:var(--muted);font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.5px}.clock-target-words{font-size:20px;color:var(--purple)}.clock-assignment{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;text-align:center}.clock-assignment.compact{width:100%}.clock-assignment.compact .clock-task-digital{font-size:44px}.clock-assignment.compact .clock-task-words{font-size:24px}.clock-task-digital{font-size:clamp(48px,10vw,78px);line-height:1;font-weight:950;letter-spacing:2px;color:var(--blue)}.clock-task-words{flex-basis:100%;margin:0;font-size:clamp(24px,5vw,36px);line-height:1.15;font-weight:950;color:var(--purple)}.clock-daypart{padding:6px 11px;border-radius:999px;background:var(--purple);color:#fff;font-size:14px;font-weight:900}.clock-target .clock-reference{--clock-size:112px;border-width:5px;box-shadow:inset 0 0 0 2px var(--line),0 4px 10px rgba(20,35,60,.12)}.clock-reference b{font-size:9px}.clock-reference .clock-pin{left:calc(50% - 6px);top:calc(50% - 6px);width:12px;height:12px;border-width:2px} .clock-target{width:100%;min-height:88px;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;padding:10px;border-radius:15px;background:var(--surface-2)}.clock-target>span:first-child{flex-basis:100%;color:var(--muted);font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.5px}.clock-target-words{font-size:20px;color:var(--purple)}.clock-assignment{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;text-align:center}.clock-assignment.compact{width:100%}.clock-assignment.compact .clock-task-digital{font-size:44px}.clock-assignment.compact .clock-task-words{font-size:24px}.clock-task-digital{font-size:clamp(48px,10vw,78px);line-height:1;font-weight:950;letter-spacing:2px;color:var(--blue)}.clock-task-words{flex-basis:100%;margin:0;font-size:clamp(24px,5vw,36px);line-height:1.15;font-weight:950;color:var(--purple)}.clock-daypart{padding:6px 11px;border-radius:999px;background:var(--purple);color:#fff;font-size:14px;font-weight:900}.clock-target .clock-reference{--clock-size:112px;border-width:5px;box-shadow:inset 0 0 0 2px var(--line),0 4px 10px rgba(20,35,60,.12)}.clock-reference b{font-size:9px}.clock-reference .clock-pin{left:calc(50% - 6px);top:calc(50% - 6px);width:12px;height:12px;border-width:2px}
.clock-work{display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}.clock{--clock-size:min(230px,62vw);width:var(--clock-size);height:var(--clock-size);flex:none;position:relative;overflow:hidden;border:9px solid var(--ink);border-radius:50%;background:radial-gradient(circle at 50% 46%,var(--surface) 0 62%,var(--surface-2) 100%);box-shadow:inset 0 0 0 3px var(--line),0 8px 20px rgba(20,35,60,.14)} .clock-work{display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap}.clock{--clock-size:min(230px,62vw);width:var(--clock-size);height:var(--clock-size);flex:none;position:relative;overflow:hidden;border:9px solid var(--ink);border-radius:50%;background:radial-gradient(circle at 50% 46%,var(--surface) 0 62%,var(--surface-2) 100%);box-shadow:inset 0 0 0 3px var(--line),0 8px 20px rgba(20,35,60,.14)}
.clock-set-answer{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px}
.clock-period{display:flex;align-items:center;justify-content:center;gap:3px;padding:3px;border-radius:999px;background:var(--surface-2)}
.clock-period .tbtn{min-height:36px;padding:0 12px;border-radius:999px}
.clock-period .tbtn.active{background:var(--purple);color:#fff;border-color:transparent}
.clock-tick{position:absolute;z-index:1;border-radius:4px}.clock-tick.hour{left:calc(50% - 2px);top:3%;width:4px;height:5%;background:var(--blue);transform-origin:50% 940%}.clock-tick.minor{left:calc(50% - 1px);top:4%;width:2px;height:4%;background:var(--ink);opacity:.28;transform-origin:50% 1150%} .clock-tick{position:absolute;z-index:1;border-radius:4px}.clock-tick.hour{left:calc(50% - 2px);top:3%;width:4px;height:5%;background:var(--blue);transform-origin:50% 940%}.clock-tick.minor{left:calc(50% - 1px);top:4%;width:2px;height:4%;background:var(--ink);opacity:.28;transform-origin:50% 1150%}
.clock b{position:absolute;z-index:2;left:calc(50% - 12px);top:calc(50% - 12px);width:24px;height:24px;display:grid;place-items:center;font-size:14px;transform:rotate(var(--clock-angle)) translateY(calc(var(--clock-size)*-.34)) rotate(calc(-1 * var(--clock-angle)))} .clock b{position:absolute;z-index:2;left:calc(50% - 12px);top:calc(50% - 12px);width:24px;height:24px;display:grid;place-items:center;font-size:14px;transform:rotate(var(--clock-angle)) translateY(calc(var(--clock-size)*-.34)) rotate(calc(-1 * var(--clock-angle)))}
.clock-hand{position:absolute;z-index:4;left:calc(50% - 5px);bottom:50%;width:10px;border-radius:9px;transform-origin:50% 100%;background:var(--ink);cursor:grab;touch-action:none}.clock-hand:active{cursor:grabbing}.clock-hour{height:28%}.clock-min{left:calc(50% - 4px);width:8px;height:39%;background:var(--pink)}.clock-pin{position:absolute;z-index:5;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;border:3px solid var(--surface);border-radius:50%;background:var(--pink);box-shadow:0 1px 4px rgba(0,0,0,.28)} .clock-hand{position:absolute;z-index:4;left:calc(50% - 5px);bottom:50%;width:10px;border-radius:9px;transform-origin:50% 100%;background:var(--ink);cursor:grab;touch-action:none}.clock-hand:active{cursor:grabbing}.clock-hour{height:28%}.clock-min{left:calc(50% - 4px);width:8px;height:39%;background:var(--pink)}.clock-pin{position:absolute;z-index:5;left:calc(50% - 8px);top:calc(50% - 8px);width:16px;height:16px;border:3px solid var(--surface);border-radius:50%;background:var(--pink);box-shadow:0 1px 4px rgba(0,0,0,.28)}
@ -1714,7 +1718,7 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.sb-actions{gap:5px} .sb-actions{gap:5px}
/* educatieve widgets: dezelfde compacte opbouw als de oefenklok */ /* educatieve widgets: dezelfde compacte opbouw als de oefenklok */
.edu-stage{align-items:flex-start;justify-content:flex-start;padding:10px} .edu-stage{align-items:safe center;justify-content:safe center;padding:10px}
.edu-controls{gap:6px;padding:0 8px 8px} .edu-controls{gap:6px;padding:0 8px 8px}
.fraction-wall,.place-grid,.wordtypes,.schedule,.mood,.poll{align-self:center} .fraction-wall,.place-grid,.wordtypes,.schedule,.mood,.poll{align-self:center}
.place-grid b{padding:6px 2px;font-size:9px;overflow-wrap:anywhere} .place-grid b{padding:6px 2px;font-size:9px;overflow-wrap:anywhere}

View file

@ -659,22 +659,24 @@ function mountClock(root,init){
const style=document.createElement('select'),game=document.createElement('select'),linkHands=document.createElement('input'),use24=document.createElement('input'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value))); const style=document.createElement('select'),game=document.createElement('select'),linkHands=document.createElement('input'),use24=document.createElement('input'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);style.value=S.style;game.value=S.game;linkHands.type='checkbox';linkHands.checked=S.linkHands;linkHands.onchange=()=>{S.linkHands=linkHands.checked};use24.type='checkbox';use24.checked=S.use24;use24.onchange=()=>{S.use24=use24.checked;newTask()};const linkSetting=field(T('clockLinkHands'),linkHands),hour24Setting=field(T('clock24Hour'),use24),syncSettings=()=>{linkSetting.hidden=game.value!=='set';hour24Setting.hidden=S.level!==3}; add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);style.value=S.style;game.value=S.game;linkHands.type='checkbox';linkHands.checked=S.linkHands;linkHands.onchange=()=>{S.linkHands=linkHands.checked};use24.type='checkbox';use24.checked=S.use24;use24.onchange=()=>{S.use24=use24.checked;newTask()};const linkSetting=field(T('clockLinkHands'),linkHands),hour24Setting=field(T('clock24Hour'),use24),syncSettings=()=>{linkSetting.hidden=game.value!=='set';hour24Setting.hidden=S.level!==3};
const minuteValues=()=>S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`; const minuteValues=()=>S.level===1?[0,30]:S.level===2?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
const daypartKey=hour=>hour<6?'clockNight':hour<12?'clockMorning':hour<18?'clockAfternoon':'clockEvening',clockHour=hour=>hour%12||12;
const uses24=()=>S.level===3&&S.use24; const uses24=()=>S.level===3&&S.use24;
const daypartKey=hour=>hour<6?'clockNight':hour<12?'clockMorning':hour<18?'clockAfternoon':'clockEvening',clockHour=hour=>hour%12||12;
const inputHour=dialHour=>uses24()?(dialHour%12)+(S.h>=12?12:0):dialHour;
const words=(hour,minute)=>{const shown=clockHour(hour),next=shown===12?1:shown+1;let result;if(minute===0)result=T('clockWordsHour').replace('{hour}',shown);else if(minute===15)result=T('clockWordsQuarterPast').replace('{hour}',shown);else if(minute===30)result=T('clockWordsHalf').replace('{hour}',next);else if(minute===45)result=T('clockWordsQuarterTo').replace('{hour}',next);else result=T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?shown:next);return uses24()?result+' '+T(daypartKey(hour)):result}; const words=(hour,minute)=>{const shown=clockHour(hour),next=shown===12?1:shown+1;let result;if(minute===0)result=T('clockWordsHour').replace('{hour}',shown);else if(minute===15)result=T('clockWordsQuarterPast').replace('{hour}',shown);else if(minute===30)result=T('clockWordsHalf').replace('{hour}',next);else if(minute===45)result=T('clockWordsQuarterTo').replace('{hour}',next);else result=T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?shown:next);return uses24()?result+' '+T(daypartKey(hour)):result};
const randomTime=()=>{const values=minuteValues();return{h:uses24()?Math.floor(Math.random()*24):1+Math.floor(Math.random()*12),m:values[Math.floor(Math.random()*values.length)]}}; const randomTime=()=>{const values=minuteValues();return{h:uses24()?Math.floor(Math.random()*24):1+Math.floor(Math.random()*12),m:values[Math.floor(Math.random()*values.length)]}};
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm clock-digit-block';const label=document.createElement('span');label.className='bl';label.textContent=String(value);const dots=dotGrid(Number(value));dots.classList.add('blok-dots');block.append(label,dots);return block}; const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm clock-digit-block';const label=document.createElement('span');label.className='bl';label.textContent=String(value);const dots=dotGrid(Number(value));dots.classList.add('blok-dots');block.append(label,dots);return block};
const timeBlocks=(hour,minute,click)=>{const el=document.createElement(click?'button':'div');if(click)el.type='button';el.className='clock-time-blocks';el.setAttribute('aria-label',time(hour,minute));const digits=String(hour).padStart(2,'0')+String(minute).padStart(2,'0');el.append(woodDigit(digits[0]),woodDigit(digits[1]));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(digits[2]),woodDigit(digits[3]));if(click)el.onclick=click;return el}; const timeBlocks=(hour,minute,click)=>{const el=document.createElement(click?'button':'div');if(click)el.type='button';el.className='clock-time-blocks';el.setAttribute('aria-label',time(hour,minute));const digits=String(hour).padStart(2,'0')+String(minute).padStart(2,'0');el.append(woodDigit(digits[0]),woodDigit(digits[1]));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(digits[2]),woodDigit(digits[3]));if(click)el.onclick=click;return el};
const makeClock=(hour,minute,interactive)=>{const face=document.createElement('div');face.className='clock'+(interactive?' interactive':'');face.setAttribute('role','img');face.setAttribute('aria-label',time(hour,minute));for(let i=0;i<60;i++){const tick=document.createElement('i');tick.className=i%5===0?'clock-tick hour':'clock-tick minor';tick.style.transform=`rotate(${i*6}deg)`;face.append(tick)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const mark=document.createElement('b');mark.textContent=n;mark.style.setProperty('--clock-angle',`${i*30}deg`);face.append(mark)});const hourHand=document.createElement('i'),minuteHand=document.createElement('i'),pin=document.createElement('span');hourHand.className='clock-hand clock-hour';minuteHand.className='clock-hand clock-min';pin.className='clock-pin';const paint=()=>{hourHand.style.transform=`rotate(${S.h*30+S.m/2}deg)`;minuteHand.style.transform=`rotate(${S.m*6}deg)`};if(interactive)paint();else{hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}face.append(hourHand,minuteHand,pin);if(interactive){const move=e=>{const r=face.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(face.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=minuteValues(),distance=v=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted));S.m=allowed.reduce((best,v)=>distance(v)<distance(best)?v:best)}else if(S.linkHands){let bestH=12,bestM=0,bestDistance=360;for(let hour=1;hour<=12;hour++)minuteValues().forEach(minute=>{const angle=(hour%12)*30+minute/2,difference=Math.min(Math.abs(angle-deg),360-Math.abs(angle-deg));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}else S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;paint();const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}};const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;face.setPointerCapture(e.pointerId);move(e)};hourHand.onpointerdown=e=>start(e,'hour');minuteHand.onpointerdown=e=>start(e,'minute');face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);delete face.dataset.hand};face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);delete face.dataset.hand}}return face}; const makeClock=(hour,minute,interactive)=>{const face=document.createElement('div');face.className='clock'+(interactive?' interactive':'');face.setAttribute('role','img');face.setAttribute('aria-label',time(hour,minute));for(let i=0;i<60;i++){const tick=document.createElement('i');tick.className=i%5===0?'clock-tick hour':'clock-tick minor';tick.style.transform=`rotate(${i*6}deg)`;face.append(tick)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const mark=document.createElement('b');mark.textContent=n;mark.style.setProperty('--clock-angle',`${i*30}deg`);face.append(mark)});const hourHand=document.createElement('i'),minuteHand=document.createElement('i'),pin=document.createElement('span');hourHand.className='clock-hand clock-hour';minuteHand.className='clock-hand clock-min';pin.className='clock-pin';const paint=()=>{hourHand.style.transform=`rotate(${S.h*30+S.m/2}deg)`;minuteHand.style.transform=`rotate(${S.m*6}deg)`};if(interactive)paint();else{hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}face.append(hourHand,minuteHand,pin);if(interactive){const move=e=>{const r=face.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(face.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=minuteValues(),distance=v=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted));S.m=allowed.reduce((best,v)=>distance(v)<distance(best)?v:best)}else if(S.linkHands){let bestH=12,bestM=0,bestDistance=360;for(let hour=1;hour<=12;hour++)minuteValues().forEach(minute=>{const angle=(hour%12)*30+minute/2,difference=Math.min(Math.abs(angle-deg),360-Math.abs(angle-deg));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=inputHour(bestH);S.m=bestM}else S.h=inputHour((Math.round((deg-S.m/2)/30)+11)%12+1);paint();const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}};const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;face.setPointerCapture(e.pointerId);move(e)};hourHand.onpointerdown=e=>start(e,'hour');minuteHand.onpointerdown=e=>start(e,'minute');face.onpointermove=e=>{if(face.hasPointerCapture(e.pointerId))move(e)};face.onpointerup=e=>{move(e);face.releasePointerCapture(e.pointerId);delete face.dataset.hand};face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);delete face.dataset.hand}}return face};
const assignment=(hour,minute,compact=false)=>{const wrap=document.createElement('div');wrap.className='clock-assignment'+(compact?' compact':'');if(['analogue','both'].includes(S.style)){const clock=makeClock(hour,minute,false);if(compact)clock.classList.add('clock-reference');wrap.append(clock)}if(['digital','both'].includes(S.style)){const digital=document.createElement('output');digital.className='clock-task-digital';digital.textContent=time(hour,minute);wrap.append(digital)}if(['words','both'].includes(S.style)){const written=document.createElement('p');written.className='clock-task-words';written.textContent=words(hour,minute);wrap.append(written)}if(uses24()&&S.style==='analogue'){const period=document.createElement('span');period.className='clock-daypart';period.textContent=T(daypartKey(hour));wrap.append(period)}return wrap}; const assignment=(hour,minute,compact=false)=>{const wrap=document.createElement('div');wrap.className='clock-assignment'+(compact?' compact':'');if(['analogue','both'].includes(S.style)){const clock=makeClock(hour,minute,false);if(compact)clock.classList.add('clock-reference');wrap.append(clock)}if(['digital','both'].includes(S.style)){const digital=document.createElement('output');digital.className='clock-task-digital';digital.textContent=time(hour,minute);wrap.append(digital)}if(['words','both'].includes(S.style)){const written=document.createElement('p');written.className='clock-task-words';written.textContent=words(hour,minute);wrap.append(written)}if(uses24()&&S.style==='analogue'){const period=document.createElement('span');period.className='clock-daypart';period.textContent=T(daypartKey(hour));wrap.append(period)}return wrap};
const periodControl=()=>{const control=document.createElement('div');control.className='clock-period';control.setAttribute('aria-label',T('clock24Hour'));[0,12].forEach(period=>{const button=btn(period?'1223':'0011',()=>{if(S.solved)return;const dial=clockHour(S.h);S.h=(dial%12)+period;S.feedback='';draw()},'ghost');button.title=period?'12:0023:59':'00:0011:59';const active=(S.h>=12?12:0)===period;button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active));control.append(button)});return control};
const blockAnswer=()=>{const answer=document.createElement('div');answer.className='clock-block-answer';const instruction=document.createElement('p');instruction.textContent=T('clockBuildHint');const slots=document.createElement('div');slots.className='clock-answer-slots';const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm clock-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('clockDigitGhost');const dots=dotGrid(S.answerDigits[index]===null?0:S.answerDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','clock-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};slots.append(makeSlot(0),makeSlot(1));const colon=document.createElement('strong');colon.className='clock-slot-colon';colon.textContent=':';slots.append(colon,makeSlot(2),makeSlot(3));const tray=document.createElement('div');tray.className='clock-block-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();try{block.releasePointerCapture(e.pointerId)}catch(_){}const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.clock-digit-slot').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';draw()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};'0123456789'.split('').forEach(d=>{const block=woodDigit(+d);block.classList.add('clock-drag-block');block.dataset.digit=d;block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',d));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.indexOf(null);if(index>=0){S.answerDigits[index]=+d;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer}; const blockAnswer=()=>{const answer=document.createElement('div');answer.className='clock-block-answer';const instruction=document.createElement('p');instruction.textContent=T('clockBuildHint');const slots=document.createElement('div');slots.className='clock-answer-slots';const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm clock-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('clockDigitGhost');const dots=dotGrid(S.answerDigits[index]===null?0:S.answerDigits[index]);dots.classList.add('slot-dots');slot.append(ghost,dots);if(S.answerDigits[index]!==null){const placed=woodDigit(S.answerDigits[index]);placed.classList.add('in-slot','clock-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};slots.append(makeSlot(0),makeSlot(1));const colon=document.createElement('strong');colon.className='clock-slot-colon';colon.textContent=':';slots.append(colon,makeSlot(2),makeSlot(3));const tray=document.createElement('div');tray.className='clock-block-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();try{block.releasePointerCapture(e.pointerId)}catch(_){}const clone=block.cloneNode(true);clone.id='dragClone';document.body.append(clone);block.classList.add('dragging');const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},hit=ev=>{let found=null;slots.querySelectorAll('.clock-digit-slot').forEach(slot=>{const r=slot.getBoundingClientRect(),pad=8;if(ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad)found=slot});return found};moveTo(e);let hovered=null;const move=ev=>{moveTo(ev);const target=hit(ev);if(hovered&&hovered!==target)hovered.classList.remove('droptarget');hovered=target;if(hovered)hovered.classList.add('droptarget')},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);clone.remove();block.classList.remove('dragging');if(hovered)hovered.classList.remove('droptarget')},up=ev=>{const target=hit(ev);clean();if(target){S.answerDigits[+target.dataset.index]=+block.dataset.digit;S.feedback='';draw()}},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)}};'0123456789'.split('').forEach(d=>{const block=woodDigit(+d);block.classList.add('clock-drag-block');block.dataset.digit=d;block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',d));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.indexOf(null);if(index>=0){S.answerDigits[index]=+d;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
const setFeedback=ok=>{if(S.solved)return;if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()}; const setFeedback=ok=>{if(S.solved)return;if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
const check=()=>{if(!S.task)return;if(S.game==='read'){if(S.amode==='blocks'){if(S.answerDigits.some(x=>x===null)){S.feedback='incomplete';sndBad();draw();return}S.h=Number(''+S.answerDigits[0]+S.answerDigits[1]);S.m=Number(''+S.answerDigits[2]+S.answerDigits[3])}else{if(S.input.length<4){S.feedback='incomplete';sndBad();draw();return}S.h=+S.input.slice(0,2);S.m=+S.input.slice(2,4)}}const expectedHour=S.game==='set'?clockHour(S.targetH):S.targetH;setFeedback(S.h===expectedHour&&S.m===S.targetM)}; const check=()=>{if(!S.task)return;if(S.game==='read'){if(S.amode==='blocks'){if(S.answerDigits.some(x=>x===null)){S.feedback='incomplete';sndBad();draw();return}S.h=Number(''+S.answerDigits[0]+S.answerDigits[1]);S.m=Number(''+S.answerDigits[2]+S.answerDigits[3])}else{if(S.input.length<4){S.feedback='incomplete';sndBad();draw();return}S.h=+S.input.slice(0,2);S.m=+S.input.slice(2,4)}}const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH;setFeedback(S.h===expectedHour&&S.m===S.targetM)};
const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='clock-type-answer';const hint=document.createElement('p');hint.textContent=T('clockTypeHint');const display=document.createElement('output');display.className='clock-type-display';const shown=(S.input+'____').slice(0,4);display.textContent=shown.slice(0,2)+':'+shown.slice(2);const pad=document.createElement('div');pad.className='clock-keypad';['7','8','9','⌫','4','5','6','0','1','2','3','✓'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key==='✓'){check();return}else if(S.input.length<4)S.input+=key;S.feedback='';draw()});if(key==='✓')button.classList.add('ok');pad.append(button)});wrap.append(hint,display,pad);return wrap}; const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='clock-type-answer';const hint=document.createElement('p');hint.textContent=T('clockTypeHint');const display=document.createElement('output');display.className='clock-type-display';const shown=(S.input+'____').slice(0,4);display.textContent=shown.slice(0,2)+':'+shown.slice(2);const pad=document.createElement('div');pad.className='clock-keypad';['7','8','9','⌫','4','5','6','0','1','2','3','✓'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key==='✓'){check();return}else if(S.input.length<4)S.input+=key;S.feedback='';draw()});if(key==='✓')button.classList.add('ok');pad.append(button)});wrap.append(hint,display,pad);return wrap};
const newTask=()=>{const target=randomTime();S.targetH=target.h;S.targetM=target.m;S.h=12;S.m=0;S.task=true;S.solved=false;S.feedback='';S.input='';S.choices=[];S.answerDigits=[null,null,null,null];if(S.game==='choose'){S.choices=[target];while(S.choices.length<3){const option=randomTime();if(!S.choices.some(x=>x.h===option.h&&x.m===option.m))S.choices.push(option)}S.choices.sort(()=>Math.random()-.5)}draw()}; const newTask=()=>{const target=randomTime();S.targetH=target.h;S.targetM=target.m;S.h=uses24()?0:12;S.m=0;S.task=true;S.solved=false;S.feedback='';S.input='';S.choices=[];S.answerDigits=[null,null,null,null];if(S.game==='choose'){S.choices=[target];while(S.choices.length<3){const option=randomTime();if(!S.choices.some(x=>x.h===option.h&&x.m===option.m))S.choices.push(option)}S.choices.sort(()=>Math.random()-.5)}draw()};
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;syncSettings();newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.feedback='';S.input='';S.answerDigits=[null,null,null,null];draw()}; const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;syncSettings();newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.feedback='';S.input='';S.answerDigits=[null,null,null,null];draw()};
const draw=()=>{S.style=style.value;S.game=game.value;stage.className='clock-app';stage.innerHTML='';const top=document.createElement('div');top.className='clock-top';const levelControl=document.createElement('div');levelControl.className='clock-level-control';const label=document.createElement('span');label.textContent=T('clockLevel');const levels=document.createElement('div');levels.className='lg-levels clock-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('clockLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(label,levels);top.append(levelControl);if(S.game!=='set'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('clock-mode');mode.title=T('amode');top.append(mode)}const score=document.createElement('span');score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);const card=document.createElement('section');card.className='clock-task-card';const title=document.createElement('h3');title.textContent=S.game==='set'?T('clockSetTitle'):S.game==='read'?T('clockReadTitle'):T('clockChooseTitle');card.append(title); const draw=()=>{S.style=style.value;S.game=game.value;stage.className='clock-app';stage.innerHTML='';const top=document.createElement('div');top.className='clock-top';const levelControl=document.createElement('div');levelControl.className='clock-level-control';const label=document.createElement('span');label.textContent=T('clockLevel');const levels=document.createElement('div');levels.className='lg-levels clock-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('clockLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(label,levels);top.append(levelControl);if(S.game!=='set'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('clock-mode');mode.title=T('amode');top.append(mode)}const score=document.createElement('span');score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);const card=document.createElement('section');card.className='clock-task-card';const title=document.createElement('h3');title.textContent=S.game==='set'?T('clockSetTitle'):S.game==='read'?T('clockReadTitle'):T('clockChooseTitle');card.append(title);
if(S.task&&S.game==='set'){const prompt=document.createElement('div');prompt.className='clock-target';const label=document.createElement('span');label.textContent=T('clockYourTask');prompt.append(label,assignment(S.targetH,S.targetM,true));card.append(prompt);const work=document.createElement('div');work.className='clock-work';work.append(makeClock(S.h,S.m,true));const live=document.createElement('div');live.className='clock-live-answer';live.append(timeBlocks(S.h,S.m));work.append(live);card.append(work);} if(S.task&&S.game==='set'){const prompt=document.createElement('div');prompt.className='clock-target';const label=document.createElement('span');label.textContent=T('clockYourTask');prompt.append(label,assignment(S.targetH,S.targetM,true));card.append(prompt);const work=document.createElement('div');work.className='clock-work';const answer=document.createElement('div');answer.className='clock-set-answer';if(uses24())answer.append(periodControl());const live=document.createElement('div');live.className='clock-live-answer';live.append(timeBlocks(S.h,S.m));answer.append(live);work.append(makeClock(S.h,S.m,true),answer);card.append(work);}
if(S.task&&S.game==='read'){const layout=document.createElement('div');layout.className='clock-read-layout';layout.append(assignment(S.targetH,S.targetM),S.amode==='blocks'?blockAnswer():typeAnswer());card.append(layout)} if(S.task&&S.game==='read'){const layout=document.createElement('div');layout.className='clock-read-layout';layout.append(assignment(S.targetH,S.targetM),S.amode==='blocks'?blockAnswer():typeAnswer());card.append(layout)}
if(S.task&&S.game==='choose'){card.append(assignment(S.targetH,S.targetM));if(!S.solved){const choices=document.createElement('div');choices.className='clock-choices '+(S.amode==='blocks'?'wood':'plain');const asWords=['digital','both'].includes(S.style);S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(asWords){const button=btn(words(option.h,option.m),choose);button.classList.add('clock-word-choice');choices.append(button)}else if(S.amode==='blocks')choices.append(timeBlocks(option.h,option.m,choose));else{const button=btn(time(option.h,option.m),choose);button.classList.add('clock-digital-choice');choices.append(button)}});card.append(choices)}} if(S.task&&S.game==='choose'){card.append(assignment(S.targetH,S.targetM));if(!S.solved){const choices=document.createElement('div');choices.className='clock-choices '+(S.amode==='blocks'?'wood':'plain');const asWords=['digital','both'].includes(S.style);S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(asWords){const button=btn(words(option.h,option.m),choose);button.classList.add('clock-word-choice');choices.append(button)}else if(S.amode==='blocks')choices.append(timeBlocks(option.h,option.m,choose));else{const button=btn(time(option.h,option.m),choose);button.classList.add('clock-digital-choice');choices.append(button)}});card.append(choices)}}
const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'ghost'));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)}; const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':S.feedback==='incomplete'?'clockIncomplete':'clockTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved&&!(S.game==='read'&&S.amode==='type'))actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('clockNextTask'),newTask,'ghost'));card.append(actions);if(S.task&&S.game==='set'){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');card.append(hint)}stage.append(card)};

View file

@ -94,6 +94,10 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
assert.ok(clockSource.includes('S.level===3&&S.use24'), '24-uursklok is een niveau-3-optie'); assert.ok(clockSource.includes('S.level===3&&S.use24'), '24-uursklok is een niveau-3-optie');
assert.ok(clockSource.includes("hour24Setting.hidden=S.level!==3")); assert.ok(clockSource.includes("hour24Setting.hidden=S.level!==3"));
assert.ok(clockSource.includes("use24.onchange=()=>{S.use24=use24.checked;newTask()}")); assert.ok(clockSource.includes("use24.onchange=()=>{S.use24=use24.checked;newTask()}"));
assert.ok(clockSource.includes("period?'1223':'0011'"), '24-uursmodus laat de juiste daghelft kiezen');
assert.ok(clockSource.includes('S.h=inputHour(bestH)'), 'de urenwijzer bewaart de gekozen daghelft');
assert.ok(clockSource.includes("const expectedHour=S.game==='set'?(uses24()?S.targetH:clockHour(S.targetH)):S.targetH"), '24-uursantwoorden worden exact gecontroleerd');
assert.ok(clockSource.includes('S.h=uses24()?0:12'), 'een 24-uursopdracht begint met een geldige invoerwaarde');
assert.ok(clockSource.includes("hourHand.onpointerdown=e=>start(e,'hour')")); assert.ok(clockSource.includes("hourHand.onpointerdown=e=>start(e,'hour')"));
assert.ok(clockSource.includes("minuteHand.onpointerdown=e=>start(e,'minute')")); assert.ok(clockSource.includes("minuteHand.onpointerdown=e=>start(e,'minute')"));
assert.ok(!clockSource.includes('face.onpointerdown='), 'de hele klok is geen sleepzone'); assert.ok(!clockSource.includes('face.onpointerdown='), 'de hele klok is geen sleepzone');
@ -101,6 +105,8 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge
for(const key of ['clockMorning','clockAfternoon','clockEvening','clockNight'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key); for(const key of ['clockMorning','clockAfternoon','clockEvening','clockNight'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
assert.match(css,/clock-top\{[^}]*position:sticky[^}]*flex-wrap:wrap/); assert.match(css,/clock-top\{[^}]*position:sticky[^}]*flex-wrap:wrap/);
assert.match(css,/clock-app\{[^}]*justify-content:flex-start/); assert.match(css,/clock-app\{[^}]*justify-content:flex-start/);
assert.match(css,/clock-set-answer\{[^}]*align-items:center[^}]*justify-content:center/);
assert.match(css,/clock-period \.tbtn\.active\{[^}]*background:var\(--purple\)/);
}); });
test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', async()=>{ test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', async()=>{
@ -242,7 +248,7 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
'.an-hint', '.an-hint',
'.bd{grid-template-columns:1fr}', '.bd{grid-template-columns:1fr}',
'.sb-banks{grid-template-columns:1fr}', '.sb-banks{grid-template-columns:1fr}',
'.edu-stage{align-items:flex-start', '.edu-stage{align-items:safe center;justify-content:safe center',
'.money-task-card,.clock-task-card', '.money-task-card,.clock-task-card',
]){ ]){
assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels'); assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels');