teach/public/js/widgets/learning.js
Ramon daad79c8b9
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 56s
fix: vakken tonen nu het puntenpatroon van het júíste cijfer, zoals bij letterblokken (v0.4.17-beta)
- De vorige puntenpatroon-fix (v0.4.16) loste alleen op dat de tien
  cijferblokken onderling verschillen. De écht ontbrekende stap: bij de
  letterblokken en de rekenwidgets (madd/msub/mmul/mdiv/mpct) toont een
  leeg vakje altijd het puntenpatroon van het cijfer/de letter die er
  hoort te staan - een tastbare hint, ongeacht wat er al ligt. Bij de
  oefenklok en het geldwidget toonde het vakje in plaats daarvan niets
  (leeg) of het patroon van wat er toevallig al in lag - geen hint.
- Oefenklok (blockAnswer), geldwidget (digitAnswer) en de nieuwe
  plaatswaarde-oefening (buildAnswer) tonen nu overal het patroon van het
  juiste doelcijfer per vak, consistent met letterblokken/rekenwidgets.
- Geverifieerd door drie oefeningen (geldrekenen niveau 2/3, oefenklok
  niveau 3, plaatswaarde niveau 3) volledig op te lossen met uitsluitend
  de puntenpatronen als leidraad (geen tekst gelezen) - in alle gevallen
  "Goed gedaan!".
2026-07-18 16:33:07 +02:00

833 lines
60 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const MONEY_LEVELS=Object.freeze({
"1": {
"min": 100,
"max": 900,
"step": 100,
"denominations": [
200,
100
],
"payments": [
500,
1000
],
"fixedIndices": [
0,
2,
3
],
"emptyDigits": [
0,
null,
0,
0
]
},
"2": {
"min": 50,
"max": 995,
"step": 5,
"denominations": [
500,
200,
100,
50,
20,
10,
5
],
"payments": [
500,
1000,
2000
],
"fixedIndices": [
0
],
"emptyDigits": [
0,
null,
null,
null
]
},
"3": {
"min": 1,
"max": 4999,
"step": 1,
"denominations": [
2000,
1000,
500,
200,
100,
50,
20,
10,
5,
2,
1
],
"payments": [
100,
200,
500,
1000,
2000,
5000
],
"fixedIndices": [],
"emptyDigits": [
null,
null,
null,
null
]
}
});
const MONEY_PRODUCTS=Object.freeze([
{
"id": "apple",
"icon": "🍎",
"category": "food",
"nl": "Appel",
"en": "Apple",
"prices": {
"1": [
100
],
"2": [
75,
100
],
"3": [
69,
79,
99
]
}
},
{
"id": "bananas",
"icon": "🍌",
"category": "food",
"nl": "Tros bananen",
"en": "Bunch of bananas",
"prices": {
"1": [
100,
200
],
"2": [
100,
150
],
"3": [
99,
145,
149
]
}
},
{
"id": "juice",
"icon": "🧃",
"category": "food",
"nl": "Pakje sap",
"en": "Juice box",
"prices": {
"1": [
200,
300
],
"2": [
200,
250
],
"3": [
199,
229,
259
]
}
},
{
"id": "sandwich",
"icon": "🥪",
"category": "food",
"nl": "Broodje",
"en": "Sandwich",
"prices": {
"1": [
400,
500
],
"2": [
350,
450,
550
],
"3": [
349,
449,
549
]
}
},
{
"id": "yogurt",
"icon": "🥣",
"category": "food",
"nl": "Yoghurt",
"en": "Yogurt",
"prices": {
"1": [
100,
200
],
"2": [
100,
150,
200
],
"3": [
99,
159,
189
]
}
},
{
"id": "cookie",
"icon": "🍪",
"category": "food",
"nl": "Koek",
"en": "Cookie",
"prices": {
"1": [
100,
200
],
"2": [
100,
150,
250
],
"3": [
99,
149,
249
]
}
},
{
"id": "water",
"icon": "💧",
"category": "food",
"nl": "Flesje water",
"en": "Bottle of water",
"prices": {
"1": [
100,
200
],
"2": [
100,
150,
200
],
"3": [
99,
149,
199
]
}
},
{
"id": "icecream",
"icon": "🍦",
"category": "food",
"nl": "IJsje",
"en": "Ice cream",
"prices": {
"1": [
200,
300
],
"2": [
200,
250,
350
],
"3": [
199,
249,
349
]
}
},
{
"id": "pencil",
"icon": "✏️",
"category": "school",
"nl": "Potlood",
"en": "Pencil",
"prices": {
"1": [
100
],
"2": [
50,
100
],
"3": [
49,
79,
99
]
}
},
{
"id": "eraser",
"icon": "🧽",
"category": "school",
"nl": "Gum",
"en": "Eraser",
"prices": {
"1": [
100
],
"2": [
50,
100
],
"3": [
49,
69,
99
]
}
},
{
"id": "ruler",
"icon": "📏",
"category": "school",
"nl": "Liniaal",
"en": "Ruler",
"prices": {
"1": [
100,
200
],
"2": [
100,
150,
200
],
"3": [
99,
149,
199
]
}
},
{
"id": "notebook",
"icon": "📓",
"category": "school",
"nl": "Schrift",
"en": "Notebook",
"prices": {
"1": [
300,
400
],
"2": [
250,
350,
450
],
"3": [
249,
349,
449
]
}
},
{
"id": "book",
"icon": "📘",
"category": "school",
"nl": "Boek",
"en": "Book",
"prices": {
"1": [
700,
900
],
"2": [
695,
895,
995
],
"3": [
699,
899,
1299
]
}
},
{
"id": "markers",
"icon": "🖍️",
"category": "school",
"nl": "Stiften",
"en": "Markers",
"prices": {
"1": [
500,
700
],
"2": [
495,
695,
895
],
"3": [
499,
699,
899
]
}
},
{
"id": "scissors",
"icon": "✂️",
"category": "school",
"nl": "Schaar",
"en": "Scissors",
"prices": {
"1": [
300,
400
],
"2": [
250,
350,
450
],
"3": [
249,
349,
449
]
}
},
{
"id": "backpack",
"icon": "🎒",
"category": "school",
"nl": "Rugzak",
"en": "Backpack",
"prices": {
"3": [
2499,
2999,
3499
]
}
},
{
"id": "calculator",
"icon": "🧮",
"category": "school",
"nl": "Rekenmachine",
"en": "Calculator",
"prices": {
"1": [
900
],
"2": [
895,
995
],
"3": [
899,
1299,
1799
]
}
},
{
"id": "ball",
"icon": "⚽",
"category": "play",
"nl": "Bal",
"en": "Ball",
"prices": {
"1": [
600,
900
],
"2": [
600,
900
],
"3": [
599,
899,
999
]
}
},
{
"id": "teddy",
"icon": "🧸",
"category": "play",
"nl": "Knuffel",
"en": "Teddy bear",
"prices": {
"1": [
900
],
"2": [
995
],
"3": [
999,
1499,
1899
]
}
},
{
"id": "puzzle",
"icon": "🧩",
"category": "play",
"nl": "Puzzel",
"en": "Puzzle",
"prices": {
"1": [
800,
900
],
"2": [
795,
995
],
"3": [
799,
999,
1499
]
}
},
{
"id": "rope",
"icon": "➰",
"category": "play",
"nl": "Springtouw",
"en": "Skipping rope",
"prices": {
"1": [
300,
600
],
"2": [
300,
600
],
"3": [
299,
599,
699
]
}
},
{
"id": "kite",
"icon": "🪁",
"category": "play",
"nl": "Vlieger",
"en": "Kite",
"prices": {
"1": [
700,
900
],
"2": [
695,
895
],
"3": [
699,
999,
1299
]
}
},
{
"id": "blocks",
"icon": "🧱",
"category": "play",
"nl": "Bouwblokken",
"en": "Building blocks",
"prices": {
"1": [
900
],
"2": [
895,
995
],
"3": [
999,
1499,
1999
]
}
},
{
"id": "boardgame",
"icon": "🎲",
"category": "play",
"nl": "Spel",
"en": "Game",
"prices": {
"1": [
900
],
"2": [
995
],
"3": [
999,
1499,
2499
]
}
},
{
"id": "skateboard",
"icon": "🛹",
"category": "play",
"nl": "Skateboard",
"en": "Skateboard",
"prices": {
"3": [
2999,
3999,
4999
]
}
}
]);
const moneyPriceFitsLevel=(price,level)=>{const rule=MONEY_LEVELS[level];return Number.isInteger(price)&&price>=rule.min&&price<=rule.max&&(price-rule.min)%rule.step===0};
function createMoneyTask(level,game,category='all',previousCents=null,previousProductId=null,random=Math.random){
const rule=MONEY_LEVELS[level],rnd=max=>Math.floor(random()*max),randomAmount=()=>rule.min+rnd(Math.floor((rule.max-rule.min)/rule.step)+1)*rule.step;
let targetCents,productId=previousProductId;
if(game==='change'){
const pool=MONEY_PRODUCTS.filter(item=>(category==='all'||item.category===category)&&(item.prices[level]||[]).some(price=>moneyPriceFitsLevel(price,level)));
const pick=()=>{const product=pool[rnd(pool.length)],prices=product.prices[level].filter(price=>moneyPriceFitsLevel(price,level));return{product,price:prices[rnd(prices.length)]}};
let offer=pick();for(let attempt=0;attempt<8&&offer.product.id===previousProductId&&offer.price===previousCents;attempt++)offer=pick();
targetCents=offer.price;productId=offer.product.id;
}else{targetCents=randomAmount();if(targetCents===previousCents)targetCents=randomAmount()}
const paidCents=rule.payments.find(value=>value>targetCents)||rule.payments[rule.payments.length-1];
return{targetCents,paidCents,productId};
}
/* Acht compacte educatieve en klaswidgets. Alle invoer blijft tekstContent-safe. */
function learningShell(root, settings, stage){
root.innerHTML='<div class="edu"><details class="edu-settings"><summary>⚙ <span></span></summary><div class="edu-controls"></div></details><div class="edu-stage"></div></div>';
root.querySelector('summary span').textContent=T('widgetSettings');
root.querySelector('.edu-controls').append(...settings);
root.querySelector('.edu-stage').append(stage);
}
function field(label, el){const w=document.createElement('label');const s=document.createElement('span');s.textContent=label;w.append(s,el);return w}
function btn(text,fn,kind='ghost'){const b=document.createElement('button');b.type='button';b.className='tbtn '+kind;b.textContent=text;b.onclick=fn;return b}
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,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;
const clockHourCrossing=(next,previous)=>{const delta=clockTurnDelta(next,previous);return delta>0&&next<previous?1:delta<0&&next>previous?-1:0};
const clockStepHour=(hour,step,use24)=>use24?(hour+step+24)%24:((hour-1+step+12)%12)+1;
const clock24Angle=(angle,delta)=>(angle+delta+720)%720;
const clock24Hour=(angle,minute)=>((Math.round((angle-minute/2)/30)%24)+24)%24;
function mountClock(root,init){
const S=Object.assign({h:12,m:0,level:1,style:'both',game:'set',amode:'blocks',input:'',linkHands:true,use24:true,task:true,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[],answerDigits:[null,null,null,null]},init||{});
if(typeof S.level==='string')S.level={hour:1,quarter:2,five:2,minute:3}[S.level]||1;S.level=Math.max(1,Math.min(3,+S.level||1));S.amode=S.amode==='type'?'type':'blocks';S.input=String(S.input||'').replace(/\D/g,'').slice(0,4);
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};
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 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 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 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 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 updateAnswer=()=>{face.setAttribute('aria-label',time(S.h,S.m));const answer=stage.querySelector('.clock-live-answer');if(answer){answer.innerHTML='';answer.append(timeBlocks(S.h,S.m))}stage.querySelectorAll('.clock-period .tbtn').forEach(button=>{const active=+button.dataset.period===(S.h>=12?12:0);button.classList.toggle('active',active);button.setAttribute('aria-pressed',String(active))})};
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 previous=face._lastPointerAngle;if(Number.isFinite(previous)){const step=clockHourCrossing(deg,previous);if(step)S.h=clockStepHour(S.h,step,uses24())}face._lastPointerAngle=deg;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(uses24()){const previous=face._lastPointerAngle;if(Number.isFinite(previous)){face._dragAngle=clock24Angle(face._dragAngle,clockTurnDelta(deg,previous));if(S.linkHands){let bestH=0,bestM=0,bestDistance=720;for(let hour=0;hour<24;hour++)minuteValues().forEach(minute=>{const angle=hour*30+minute/2,difference=Math.min(Math.abs(angle-face._dragAngle),720-Math.abs(angle-face._dragAngle));if(difference<bestDistance){bestDistance=difference;bestH=hour;bestM=minute}});S.h=bestH;S.m=bestM}else S.h=clock24Hour(face._dragAngle,S.m)}face._lastPointerAngle=deg}
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();updateAnswer()};
const clearDrag=()=>{delete face.dataset.hand;delete face._dragAngle;delete face._lastPointerAngle};
const start=(e,hand)=>{if(S.solved)return;e.preventDefault();e.stopPropagation();face.dataset.hand=hand;if(hand==='minute')face._lastPointerAngle=null;else if(uses24()){face._dragAngle=S.h*30+S.m/2;face._lastPointerAngle=null}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);clearDrag()};
face.onpointercancel=e=>{if(face.hasPointerCapture(e.pointerId))face.releasePointerCapture(e.pointerId);clearDrag()}
}
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 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.dataset.period=period;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';
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
puntenpatroon van het júíste cijfer op die plek (niet van wat er nu in
staat) - dat is de tastbare hint, ongeacht of er al iets ligt. */
const targetDigits=String(S.targetH).padStart(2,'0')+String(S.targetM).padStart(2,'0');
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(targetDigits[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 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 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 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';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==='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)};
style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLearningStyle'),style),field(T('clockGame'),game),linkSetting,hour24Setting],stage);syncSettings();newTask();return{getState:()=>({...S,choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})};
}
const PLACE_COLUMNS=['HD','TD','D','H','T','E'];
const placeLevelDigits=level=>level===1?2:level===2?3:6;
function mountPlacevalue(root,init){
const S=Object.assign({number:1342,game:'explore',level:1,target:null,answerDigits:null,score:0,streak:0,solved:false,feedback:''},init||{});
S.level=Math.max(1,Math.min(3,+S.level||1));S.game=S.game==='build'?'build':'explore';if(!Array.isArray(S.answerDigits))S.answerDigits=null;
const input=num(S.number,0,999999),game=document.createElement('select'),stage=document.createElement('div');
game.add(new Option(T('placeGameExplore'),'explore'));game.add(new Option(T('placeGameBuild'),'build'));game.value=S.game;
const gameSetting=field(T('placeGame'),game),numberSetting=field(T('placeNumber'),input),diceBtn=btn('🎲',()=>{input.value=Math.floor(Math.random()*999999);drawExplore()});
const syncSettings=()=>{numberSetting.hidden=S.game!=='explore';diceBtn.hidden=S.game!=='explore'};
const activeCols=()=>PLACE_COLUMNS.slice(6-placeLevelDigits(S.level));
const maxForLevel=()=>Math.pow(10,placeLevelDigits(S.level))-1;
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm place-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 drawExplore=()=>{S.number=Math.max(0,Math.min(999999,+input.value||0));const digits=String(S.number).padStart(6,'0').split('');stage.className='place-grid';stage.innerHTML='';PLACE_COLUMNS.forEach((x,i)=>{const d=document.createElement('div');const b=document.createElement('b');b.textContent=x;const o=document.createElement('output');o.textContent=digits[i];d.append(b,o);stage.append(d)})};
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()}drawBuild()};
const check=()=>{if(S.answerDigits.some(d=>d===null)){S.feedback='incomplete';sndBad();drawBuild();return}setFeedback(Number(S.answerDigits.join(''))===S.target)};
const needsNewTask=()=>S.target===null||!Array.isArray(S.answerDigits)||S.answerDigits.length!==activeCols().length;
const newTask=()=>{S.target=Math.floor(Math.random()*(maxForLevel()+1));S.answerDigits=activeCols().map(()=>null);S.solved=false;S.feedback='';drawBuild()};
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;newTask()};
const buildAnswer=()=>{
const answer=document.createElement('div');answer.className='place-block-answer';const instruction=document.createElement('p');instruction.textContent=T('placeBuildHint');
const slots=document.createElement('div');slots.className='place-answer-slots';const cols=activeCols();
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
puntenpatroon van het júíste cijfer op die plek. */
const targetDigits=String(S.target).padStart(cols.length,'0');
const makeSlot=index=>{const wrap=document.createElement('div');wrap.className='place-slot-col';const colLabel=document.createElement('b');colLabel.textContent=cols[index];
const slot=document.createElement('div');slot.className='slot sm place-digit-slot'+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;
const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent=T('placeDigitGhost');const dots=dotGrid(targetDigits[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','place-drag-block');slot.append(placed);slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';drawBuild()}}}
wrap.append(colLabel,slot);return wrap};
cols.forEach((_,i)=>slots.append(makeSlot(i)));
const tray=document.createElement('div');tray.className='place-digit-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('.place-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='';drawBuild()}},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('place-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;drawBuild()}}};attachDrag(block);tray.append(block)});
answer.append(instruction,slots,tray);return answer;
};
const drawBuild=()=>{
stage.className='place-app';stage.innerHTML='';const top=document.createElement('div');top.className='place-top';
const levelControl=document.createElement('div');levelControl.className='place-level-control';const label=document.createElement('span');label.textContent=T('placeLevel');
const levels=document.createElement('div');levels.className='lg-levels place-levels';
[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('placeLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});
levelControl.append(label,levels);top.append(levelControl);
const score=document.createElement('span');score.className='place-score';score.textContent=T('placeScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);
const card=document.createElement('section');card.className='place-task-card';const title=document.createElement('h3');title.textContent=T('placeBuildTitle');card.append(title);
const target=document.createElement('div');target.className='place-target';const targetLabel=document.createElement('span');targetLabel.textContent=T('placeYourTask');
const targetValue=document.createElement('output');targetValue.className='place-task-digital';targetValue.textContent=String(S.target);target.append(targetLabel,targetValue);card.append(target,buildAnswer());
const feedback=document.createElement('p');feedback.className='place-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');
if(S.feedback)feedback.textContent=T(S.feedback==='good'?'placeCorrect':S.feedback==='incomplete'?'placeIncomplete':'placeTryAgain');card.append(feedback);
const actions=document.createElement('div');actions.className='place-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T('placeNextTask'),newTask,'ghost'));card.append(actions);stage.append(card);
};
input.oninput=drawExplore;game.onchange=()=>{S.game=game.value;syncSettings();if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild()};
learningShell(root,[gameSetting,numberSetting,diceBtn],stage);syncSettings();
if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild();
return{getState:()=>({...S,answerDigits:S.answerDigits?[...S.answerDigits]:null})};
}
function mountMoney(root,init){
const legacy=init||{},S=Object.assign({level:1,style:'both',game:'make',category:'all',productId:'juice',amode:'blocks',targetCents:375,paidCents:500,selected:[],given:[],input:'',answerDigits:[0,null,0,0],score:0,streak:0,solved:false,feedback:''},legacy);
if(legacy.targetCents==null&&Number.isFinite(legacy.target))S.targetCents=Math.round(legacy.target*100);
S.level=Math.max(1,Math.min(3,+S.level||1));S.style=['amount','words','both'].includes(S.style)?S.style:'both';S.game=['make','count','change'].includes(S.game)?S.game:'make';S.category=['all','food','school','play'].includes(S.category)?S.category:'all';S.amode=S.amode==='type'?'type':'blocks';S.selected=Array.isArray(S.selected)?S.selected.map(Number).filter(Number.isFinite):[];S.given=Array.isArray(S.given)?S.given.map(Number).filter(Number.isFinite):[];
const style=document.createElement('select'),game=document.createElement('select'),category=document.createElement('select'),stage=document.createElement('div'),add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
add(style,[['amount','moneyStyleAmount'],['words','moneyStyleWords'],['both','moneyStyleBoth']]);add(game,[['make','moneyGameMake'],['count','moneyGameCount'],['change','moneyGameChange']]);add(category,[['all','moneyCategoryAll'],['food','moneyCategoryFood'],['school','moneyCategorySchool'],['play','moneyCategoryPlay']]);style.value=S.style;game.value=S.game;category.value=S.category;
const styleSetting=field(T('moneyLearningStyle'),style),gameSetting=field(T('moneyGame'),game),categorySetting=field(T('moneyCategory'),category),syncSettings=()=>{styleSetting.hidden=S.game==='count';categorySetting.hidden=S.game!=='change'};
const format=cents=>{const value=(Math.max(0,cents)/100).toFixed(2);return '€ '+(LANG==='nl'?value.replace('.',','):value)};
const nlSmall=['nul','een','twee','drie','vier','vijf','zes','zeven','acht','negen','tien','elf','twaalf','dertien','veertien','vijftien','zestien','zeventien','achttien','negentien'],nlTens=['','','twintig','dertig','veertig','vijftig','zestig','zeventig','tachtig','negentig'];
const enSmall=['zero','one','two','three','four','five','six','seven','eight','nine','ten','eleven','twelve','thirteen','fourteen','fifteen','sixteen','seventeen','eighteen','nineteen'],enTens=['','','twenty','thirty','forty','fifty','sixty','seventy','eighty','ninety'];
const numberWords=value=>{const n=Math.max(0,Math.min(99,value));if(LANG==='nl'){if(n<20)return nlSmall[n];const ones=n%10;return ones?nlSmall[ones]+nlTens[Math.floor(n/10)]:nlTens[Math.floor(n/10)]}if(n<20)return enSmall[n];const ones=n%10;return enTens[Math.floor(n/10)]+(ones?'-'+enSmall[ones]:'')};
const amountWords=cents=>{const euros=Math.floor(cents/100),rest=cents%100;if(LANG==='nl')return numberWords(euros)+' euro'+(rest?' en '+numberWords(rest)+' cent':'');return numberWords(euros)+' euro'+(euros===1?'':'s')+(rest?' and '+numberWords(rest)+' cent'+(rest===1?'':'s'):'')};
const levelRule=()=>MONEY_LEVELS[S.level],denominations=()=>levelRule().denominations;
const breakdown=amount=>{const result=[];let rest=amount;denominations().forEach(value=>{while(rest>=value&&result.length<24){result.push(value);rest-=value}});return result.sort(()=>Math.random()-.5)};
const fixedIndices=()=>levelRule().fixedIndices,emptyDigits=()=>[...levelRule().emptyDigits],currentProduct=()=>MONEY_PRODUCTS.find(item=>item.id===S.productId)||MONEY_PRODUCTS[0];
const legacyProduct=MONEY_PRODUCTS.find(item=>item.icon===legacy.product);if(!MONEY_PRODUCTS.some(item=>item.id===S.productId))S.productId=legacyProduct?.id||'juice';delete S.product;
const piece=(value,interactive=false)=>{const el=document.createElement(interactive?'button':'span');if(interactive)el.type='button';const type=value>=500?'note':'coin',extension=value>=500?'jpg':'png';el.className='money-piece '+type;el.dataset.value=value;const image=document.createElement('img'),fallback=document.createElement('span');image.className='money-piece-image';image.src=`/img/money/${type}-${value}.${extension}`;image.alt='';image.draggable=false;image.decoding='async';image.setAttribute('aria-hidden','true');image.onerror=()=>el.classList.add('image-missing');fallback.className='money-piece-fallback';fallback.textContent=value>=100?'€'+value/100:value+'c';el.append(image,fallback);el.setAttribute('aria-label',format(value));return el};
const pile=(values,interactive=false,wallet=null)=>{const wrap=document.createElement('div');wrap.className='money-pile'+(interactive?' interactive':' readonly');values.forEach((value,index)=>{const el=piece(value,interactive);if(interactive){el.title=T('moneyRemove');el.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=el.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone','from-wallet');document.body.append(clone);const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},over=ev=>{const r=wallet.getBoundingClientRect();return ev.clientX>=r.left&&ev.clientX<=r.right&&ev.clientY>=r.top&&ev.clientY<=r.bottom},hasMoved=ev=>Math.hypot(ev.clientX-startX,ev.clientY-startY)>=6;moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('money-remove-target',hasMoved(ev)&&!over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('money-remove-target');clone.remove()},remove=()=>{S.selected.splice(index,1);S.feedback='';draw()},up=ev=>{const moved=hasMoved(ev),outside=!over(ev);clean();if(!moved||(moved&&outside))remove()},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};el.onkeydown=e=>{if((e.key==='Enter'||e.key===' '||e.key==='Delete'||e.key==='Backspace')&&!S.solved){e.preventDefault();S.selected.splice(index,1);S.feedback='';draw()}}}wrap.append(el)});return wrap};
const amountDisplay=(cents,compact=false)=>{const wrap=document.createElement('div');wrap.className='money-assignment'+(compact?' compact':'');if(['amount','both'].includes(S.style)){const output=document.createElement('output');output.className='money-task-amount';output.textContent=format(cents);wrap.append(output)}if(['words','both'].includes(S.style)){const words=document.createElement('p');words.className='money-task-words';words.textContent=amountWords(cents);wrap.append(words)}return wrap};
const woodDigit=value=>{const block=document.createElement('span');block.className='blok sm money-digit-block';block.dataset.digit=value;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 answerValue=()=>{if(S.amode==='blocks'){if(S.answerDigits.some(value=>value===null))return null;return Number(S.answerDigits.join(''))}if(!S.input)return null;const parts=S.input.replace('.',',').split(',');if(parts.length>2||parts[1]?.length>2)return null;return (+parts[0]||0)*100+Number((parts[1]||'').padEnd(2,'0')||0)};
const digitAnswer=()=>{const answer=document.createElement('div');answer.className='money-digit-answer';const instruction=document.createElement('p');instruction.textContent=T('moneyBuildHint');const slots=document.createElement('div');slots.className='money-answer-slots';const fixed=new Set(fixedIndices());
/* zelfde patroon als letterblokken/rekenblokken: het vak toont altijd het
puntenpatroon van het júíste cijfer op die plek. */
const targetDigits=String(expected()).padStart(4,'0');
const makeSlot=index=>{const slot=document.createElement('div');slot.className='slot sm money-digit-slot'+(fixed.has(index)?' fixed':'')+(S.answerDigits[index]!==null?' filled':'');slot.dataset.index=index;const ghost=document.createElement('div');ghost.className='ghost';ghost.textContent='?';const dots=dotGrid(targetDigits[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','money-drag-digit');slot.append(placed);if(!fixed.has(index))slot.onclick=()=>{if(!S.solved){S.answerDigits[index]=null;S.feedback='';draw()}}}return slot};
const euro=document.createElement('strong');euro.textContent='€';slots.append(euro,makeSlot(0),makeSlot(1));const comma=document.createElement('strong');comma.textContent=LANG==='nl'?',':'.';slots.append(comma,makeSlot(2),makeSlot(3));
const tray=document.createElement('div');tray.className='money-digit-tray lg-tray';const attachDrag=block=>{block.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();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('.money-digit-slot:not(.fixed)').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(digit=>{const block=woodDigit(+digit);block.classList.add('money-drag-digit');block.setAttribute('role','button');block.setAttribute('aria-label',T('clockDigit').replace('{digit}',digit));block.tabIndex=0;block.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();const index=S.answerDigits.findIndex((value,i)=>value===null&&!fixed.has(i));if(index>=0){S.answerDigits[index]=+digit;draw()}}};attachDrag(block);tray.append(block)});answer.append(instruction,slots,tray);return answer};
const typeAnswer=()=>{const wrap=document.createElement('div');wrap.className='money-type-answer';const hint=document.createElement('p');hint.textContent=T('moneyTypeHint');const display=document.createElement('output');display.className='money-type-display';display.textContent='€ '+(S.input||'…');const pad=document.createElement('div');pad.className='money-keypad';['7','8','9','⌫','4','5','6',LANG==='nl'?',':'.','1','2','3','0'].forEach(key=>{const button=btn(key,()=>{if(S.solved)return;if(key==='⌫')S.input=S.input.slice(0,-1);else if(key===','||key==='.'){if(!/[,.]/.test(S.input))S.input=(S.input||'0')+key}else{const parts=S.input.split(/[,.]/);if((parts.length===1&&parts[0].length<2)||(parts.length===2&&parts[1].length<2))S.input+=key}S.feedback='';draw()});pad.append(button)});wrap.append(hint,display,pad);return wrap};
const addMoney=value=>{if(!S.solved){S.selected.push(value);S.feedback='';draw()}};
const moneyTray=wallet=>{const section=document.createElement('section');section.className='money-tray-wrap';const label=document.createElement('b');label.textContent=T('moneyTray');const tray=document.createElement('div');tray.className='money-cash-tray';denominations().forEach(value=>{const item=piece(value,true);item.onpointerdown=e=>{if(S.solved||(e.pointerType==='mouse'&&e.button!==0))return;e.preventDefault();const startX=e.clientX,startY=e.clientY,clone=item.cloneNode(true);clone.id='dragClone';clone.classList.add('money-drag-clone');document.body.append(clone);const moveTo=ev=>{const zoom=VZ();clone.style.left=ev.clientX/zoom+'px';clone.style.top=ev.clientY/zoom+'px'},over=ev=>{const r=wallet.getBoundingClientRect(),pad=12;return ev.clientX>=r.left-pad&&ev.clientX<=r.right+pad&&ev.clientY>=r.top-pad&&ev.clientY<=r.bottom+pad};moveTo(e);const move=ev=>{moveTo(ev);wallet.classList.toggle('droptarget',over(ev))},clean=()=>{document.removeEventListener('pointermove',move);document.removeEventListener('pointerup',up);document.removeEventListener('pointercancel',cancel);wallet.classList.remove('droptarget');clone.remove()},up=ev=>{const tapped=Math.hypot(ev.clientX-startX,ev.clientY-startY)<6,target=over(ev);clean();if(tapped||target)addMoney(value)},cancel=()=>clean();document.addEventListener('pointermove',move);document.addEventListener('pointerup',up);document.addEventListener('pointercancel',cancel)};item.onkeydown=e=>{if((e.key==='Enter'||e.key===' ')&&!S.solved){e.preventDefault();addMoney(value)}};tray.append(item)});section.append(label,tray);return section};
const expected=()=>S.game==='change'?S.paidCents-S.targetCents:S.targetCents,setFeedback=ok=>{if(ok){S.solved=true;S.score++;S.streak++;S.feedback='good';sndGood()}else{S.streak=0;S.feedback='bad';sndBad()}draw()};
const check=()=>{const value=S.game==='make'?S.selected.reduce((sum,item)=>sum+item,0):answerValue();if(value===null){S.feedback='incomplete';sndBad();draw();return}setFeedback(value===expected())};
const feedbackText=()=>{if(S.feedback==='good')return T('moneyCorrect');if(S.feedback==='incomplete')return T('moneyIncomplete');if(S.feedback==='bad'&&S.game==='make'){const total=S.selected.reduce((sum,item)=>sum+item,0),difference=S.targetCents-total;return T(difference>0?'moneyNeed':'moneyTooMuch').replace('{amount}',format(Math.abs(difference)))}return T('moneyTryAgain')};
const resetAnswer=()=>{S.selected=[];S.input='';S.answerDigits=emptyDigits();S.feedback='';S.solved=false;draw()};
const newTask=()=>{const task=createMoneyTask(S.level,S.game,S.category,S.targetCents,S.productId);S.targetCents=task.targetCents;S.paidCents=task.paidCents;S.productId=task.productId;S.selected=[];S.given=breakdown(task.targetCents);S.input='';S.answerDigits=emptyDigits();S.solved=false;S.feedback='';draw()};
const chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;newTask()},toggleMode=()=>{S.amode=S.amode==='blocks'?'type':'blocks';S.input='';S.answerDigits=emptyDigits();S.feedback='';draw()};
const draw=()=>{S.style=style.value;S.game=game.value;stage.className='money-app';stage.innerHTML='';const top=document.createElement('div');top.className='money-top';const levelControl=document.createElement('div');levelControl.className='money-level-control';const levelLabel=document.createElement('span');levelLabel.textContent=T('moneyLevel');const levels=document.createElement('div');levels.className='lg-levels money-levels';[1,2,3].forEach(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('moneyLevel'+value+'Hint');button.classList.toggle('active',S.level===value);button.onclick=()=>chooseLevel(value);levels.append(button)});levelControl.append(levelLabel,levels);top.append(levelControl);if(S.game!=='make'){const mode=btn(S.amode==='blocks'?'🧱':'⌨️',toggleMode);mode.classList.add('money-mode');mode.title=T('amode');top.append(mode)}const score=document.createElement('span');score.className='money-score';score.textContent=T('moneyScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(score);stage.append(top);
const card=document.createElement('section');card.className='money-task-card';const title=document.createElement('h3'),levelHint=document.createElement('p');title.textContent=T(S.game==='make'?'moneyMakeTitle':S.game==='count'?'moneyCountTitle':'moneyChangeTitle');levelHint.className='money-level-hint';levelHint.textContent=T('moneyLevel'+S.level+'Hint');card.append(title,levelHint);
if(S.game==='make'){const target=document.createElement('div');target.className='money-target';const label=document.createElement('span');label.textContent=T('moneyYourTask');target.append(label,amountDisplay(S.targetCents,true));card.append(target);const wallet=document.createElement('div');wallet.className='money-wallet';const walletHead=document.createElement('div');const walletLabel=document.createElement('b');walletLabel.textContent=T('moneyWallet');const total=document.createElement('output');total.textContent=format(S.selected.reduce((sum,item)=>sum+item,0));walletHead.append(walletLabel,total);wallet.append(walletHead);if(S.selected.length)wallet.append(pile(S.selected,true,wallet));else{const hint=document.createElement('p');hint.textContent=T('moneyWalletHint');wallet.append(hint)}card.append(wallet,moneyTray(wallet))}
if(S.game==='count'){const target=document.createElement('div');target.className='money-count-task';target.append(pile(S.given));card.append(target,S.amode==='blocks'?digitAnswer():typeAnswer())}
if(S.game==='change'){const item=currentProduct(),shop=document.createElement('div');shop.className='money-shop-task';const product=document.createElement('div');product.className='money-product';const emoji=document.createElement('span'),productName=document.createElement('strong');emoji.className='money-product-icon';emoji.textContent=item.icon;productName.className='money-product-name';productName.textContent=LANG==='nl'?item.nl:item.en;const priceLabel=document.createElement('b');priceLabel.textContent=T('moneyPrice');product.append(emoji,productName,priceLabel,amountDisplay(S.targetCents,true));const paid=document.createElement('div');paid.className='money-paid';const paidLabel=document.createElement('b');paidLabel.textContent=T('moneyPaid');const paidAmount=document.createElement('output');paidAmount.textContent=format(S.paidCents);paid.append(paidLabel,paidAmount,pile([S.paidCents]));shop.append(product,paid);card.append(shop,S.amode==='blocks'?digitAnswer():typeAnswer())}
const feedback=document.createElement('p');feedback.className='money-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');feedback.textContent=S.feedback?feedbackText():'';card.append(feedback);const actions=document.createElement('div');actions.className='money-actions';if(!S.solved)actions.append(btn(T('numberlineCheck'),check,''));if(!S.solved)actions.append(btn(T('moneyClear'),resetAnswer,'ghost'));actions.append(btn(T('moneyNextTask'),newTask,'ghost'));card.append(actions);stage.append(card)};
style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[styleSetting,gameSetting,categorySetting],stage);syncSettings();newTask();return{getState:()=>({...S,selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})};
}
function mountWordtypes(root,init){
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 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='';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: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}))})};
}