Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
5 changed files with 51 additions and 23 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.40-beta
|
||||
0.3.41-beta
|
||||
|
|
|
|||
|
|
@ -1451,15 +1451,25 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
|
|||
.gallery-head,.gallery-tools,.gallery-data{display:flex;align-items:center;gap:8px}.gallery-head h2{flex:1}.gallery-tools{position:sticky;top:-24px;z-index:2;background:var(--surface);padding:6px 0 10px}.gallery-tools input{flex:1;height:42px;border:1px solid var(--line);border-radius:12px;padding:0 13px;background:var(--surface);color:var(--ink);font:inherit}.gallery-data{margin:8px 0 14px;flex-wrap:wrap}#galleryTemplates{display:flex;gap:7px;overflow-x:auto;padding:2px 0 8px}.gcat-title{margin:18px 0 8px;font-size:13px;font-weight:900;color:var(--muted);text-transform:uppercase}.gcard{position:relative}.gstar{position:absolute;right:7px;top:7px;border:0;background:transparent;color:var(--orange);font-size:22px;cursor:pointer}.gallery-empty,.edu-empty{text-align:center;color:var(--muted);padding:28px}.widget.locked .widget-rs{display:none}.widget.locked{outline:2px solid var(--accent-soft)}.widget.preview .widget-head{position:absolute;z-index:4;width:100%;opacity:0}.widget.preview:hover .widget-head,.widget.preview:focus-within .widget-head{opacity:1}.widget.preview .widget-rs{display:none}.widget.preview{border-color:transparent;box-shadow:none}.wpreview.on{background:var(--accent-soft);color:var(--accent-ink)}
|
||||
.edu{height:100%;display:flex;flex-direction:column;background:var(--surface);color:var(--ink)}.edu-settings{flex:none;background:var(--surface-2);border-bottom:1px solid var(--line)}.edu-settings summary{cursor:pointer;padding:9px 12px;font-weight:800;font-size:13px}.edu-controls{display:flex;align-items:end;gap:8px;flex-wrap:wrap;padding:0 12px 10px}.edu-controls label{display:flex;flex-direction:column;gap:3px;font-size:11px;font-weight:800;color:var(--muted)}.edu-controls input,.edu-controls select,.edu-controls textarea{min-height:34px;border:1px solid var(--line);border-radius:9px;padding:5px 8px;background:var(--surface);color:var(--ink);font:inherit}.edu-controls textarea{height:65px;resize:vertical}.edu-stage{flex:1;min-height:0;overflow:auto;padding:16px;display:flex;align-items:center;justify-content:center}
|
||||
.fraction-wall{width:100%;display:flex;flex-direction:column;gap:5px}.fraction-wall>div{display:flex;gap:4px}.fraction-piece{flex:1;min-height:34px;border:2px solid hsl(var(--fraction-hue) 55% 43%);border-radius:5px;background:hsl(var(--fraction-hue) 75% 90%);font-weight:800;color:#20304a}.fraction-piece.selected{background:hsl(var(--fraction-hue) 70% 55%);color:#fff}
|
||||
.clock-wrap{text-align:center}.clock{width:min(270px,65vw);aspect-ratio:1;border:12px solid var(--ink);border-radius:50%;position:relative;background:var(--surface)}.clock i{position:absolute;width:7px;left:calc(50% - 3px);bottom:50%;background:var(--ink);border-radius:8px;transform-origin:50% 100%}.clock-hour{height:29%}.clock-min{height:40%;background:var(--pink)!important}.clock b{position:absolute}.clock b:nth-of-type(1){top:5px;left:47%}.clock b:nth-of-type(2){right:10px;top:47%}.clock b:nth-of-type(3){bottom:5px;left:49%}.clock b:nth-of-type(4){left:10px;top:47%}.clock-wrap output{display:block;font-size:28px;font-weight:900;margin-top:10px}
|
||||
.place-grid{display:grid;grid-template-columns:repeat(6,1fr);width:100%;border:1px solid var(--line);border-radius:14px;overflow:hidden}.place-grid div{text-align:center;border-right:1px solid var(--line)}.place-grid b{display:block;padding:8px;background:var(--surface-2);font-size:12px}.place-grid output{display:block;padding:18px 4px;font-size:clamp(25px,5vw,48px);font-weight:900;color:var(--blue)}
|
||||
.money{text-align:center;display:flex;align-items:center;justify-content:center;gap:9px;flex-wrap:wrap}.money h3{flex:1 0 100%;font-size:25px}.money .tbtn{border-radius:50%;min-width:62px}.money output{flex:1 0 100%;font-size:32px;font-weight:900;color:var(--green)}
|
||||
.wordtypes{display:flex;gap:9px;flex-wrap:wrap;justify-content:center}.wordtypes button{border:0;border-radius:10px;padding:10px 13px;font:inherit;font-weight:900;background:var(--surface-2)}.wordtypes button[data-type=noun]{background:#dce9ff;color:#174c9b}.wordtypes button[data-type=verb]{background:#dcf7e7;color:#176b3a}.wordtypes button[data-type=adjective]{background:#fff0cf;color:#855b00}.wordtypes p{flex-basis:100%;text-align:center;color:var(--muted);font-size:12px}
|
||||
.schedule{width:min(100%,450px);display:flex;flex-direction:column;gap:7px}.schedule>div{display:flex;align-items:center;gap:12px;padding:9px;background:var(--surface-2);border-radius:10px}.schedule time{font-weight:900;color:var(--blue)}.schedule span{flex:1;font-weight:800}.schedule .tbtn{height:30px;min-height:30px;padding:0 9px}.mood{display:flex;gap:8px;flex-wrap:wrap}.mood .tbtn{font-size:36px;height:auto;padding:12px}.mood span{display:block;font-size:14px}.poll{width:min(100%,440px);display:flex;flex-direction:column;gap:8px}.poll h3{text-align:center}.poll .tbtn{position:relative;overflow:hidden;text-align:left}.poll .tbtn i{position:absolute;inset:0 auto 0 0;background:var(--accent-soft);z-index:0}.poll .tbtn span{position:relative;float:right;font-weight:900}.poll .tbtn{isolation:isolate}
|
||||
@media(max-width:600px){.gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.gcard{min-height:120px;padding:13px 8px}.gcard .gd{display:none}.widget .wdup,.widget .wpreview{display:none}.edu-stage{padding:10px}.place-grid b{font-size:9px}.place-grid output{padding:12px 2px}}
|
||||
|
||||
.clock-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:8px}.clock-prompt{margin:0;text-align:center}.clock-lesson{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:14px}.clock{overflow:hidden}.clock-tick{position:absolute!important;left:calc(50% - 1px)!important;bottom:50%!important;width:2px!important;height:8%!important;transform-origin:50% 625%!important;background:var(--ink)!important}.clock-tick.minor{height:4%!important;opacity:.35}.clock-hand{z-index:2}.clock-pin{position:absolute;width:15px;height:15px;border-radius:50%;background:var(--pink);left:calc(50% - 7px);top:calc(50% - 7px);z-index:3}.clock b{left:calc(50% - 11px)!important;top:calc(50% - 11px)!important;width:22px;height:22px;transform:rotate(var(--clock-angle)) translateY(-105px) rotate(calc(-1 * var(--clock-angle)));z-index:1}.clock-digital{font-size:clamp(34px,8vw,58px);font-weight:900;color:var(--blue)}.clock-words{flex-basis:100%;margin:0;font-size:20px;font-weight:900}.clock-choices{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}.clock-feedback{min-height:24px;margin:0;font-weight:900}.clock-feedback.ok{color:var(--green)}.clock-feedback.bad{color:var(--red)}.clock-score{color:var(--muted);font-weight:800}
|
||||
|
||||
.clock.interactive{cursor:grab;touch-action:none;user-select:none}.clock.interactive:active{cursor:grabbing}.clock.interactive .clock-hand{pointer-events:none}.clock.interactive:hover{box-shadow:0 0 0 5px var(--accent-soft)}
|
||||
|
||||
.clock-drag-hint{color:var(--muted);font-weight:800;text-align:center}
|
||||
.clock-app{width:100%;min-height:100%;display:flex;flex-direction:column;justify-content:center;color:var(--ink)}
|
||||
.clock-top{display:flex;justify-content:flex-end;margin-bottom:8px}.clock-score{padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900}
|
||||
.clock-task-card{width:min(100%,620px);margin:auto;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px solid var(--line);border-radius:20px;background:var(--surface);box-shadow:var(--shadow-1)}
|
||||
.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-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-tick{position:absolute;z-index:1;left:calc(50% - 1px);top:3%;width:2px;height:8%;border-radius:2px;background:var(--ink);transform-origin:50% 587.5%}.clock-tick.minor{top:4%;height:4%;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)*-.40)) rotate(calc(-1 * var(--clock-angle)))}
|
||||
.clock-hand{position:absolute;z-index:4;left:calc(50% - 4px);bottom:50%;width:8px;border-radius:9px;transform-origin:50% 100%;background:var(--ink)}.clock-hour{height:28%}.clock-min{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.interactive{cursor:grab;touch-action:none;user-select:none;outline:4px solid transparent;transition:outline-color .15s,transform .15s}.clock.interactive:hover,.clock.interactive:focus-within{outline-color:var(--accent-soft)}.clock.interactive:active{cursor:grabbing;transform:scale(.99)}
|
||||
.clock-time-blocks{display:flex;align-items:center;justify-content:center;gap:5px;margin:0;padding:6px;border:0;border-radius:14px;background:transparent;color:var(--ink);font:inherit}.clock-time-blocks:is(button){cursor:pointer}.clock-time-blocks:is(button):hover,.clock-time-blocks:is(button):focus-visible{background:var(--accent-soft);outline:2px solid var(--accent)}.clock-time-blocks>strong{font-size:30px}
|
||||
.clock-digit-block.blok{width:50px;height:68px;min-height:68px;padding-top:3px;cursor:default}.clock-digit-block.blok .bl{height:31px;font-size:22px;color:var(--purple)}.clock-digit-block.blok .blok-dots{margin-bottom:5px}.clock-digit-block.blok .dots{grid-template-columns:repeat(2,6px);grid-template-rows:repeat(3,6px);gap:2px}.clock-digit-block.blok .dots i{width:6px;height:6px}
|
||||
.clock-live-answer{padding:6px 12px;border:2px dashed var(--line);border-radius:16px;background:var(--surface-2)}.clock-steppers{display:flex;justify-content:center;gap:18px;flex-wrap:wrap}.clock-stepper>span{display:block;margin-bottom:5px;color:var(--muted);font-size:12px;font-weight:900}.clock-stepper>div{display:flex;align-items:center;gap:6px}.clock-stepper .tbtn{width:38px;padding:0;font-size:22px}.clock-step-value{pointer-events:none}
|
||||
.clock-choices{display:flex;gap:10px;justify-content:center;flex-wrap:wrap}.clock-choices.wood .clock-time-blocks{border:1px solid var(--line);background:var(--surface-2)}.clock-word-choice{min-height:48px;border:2px solid var(--wood-d)!important;background:linear-gradient(#fdf3dd,var(--wood))!important;color:var(--ink)!important;box-shadow:0 3px 0 var(--wood-d)}
|
||||
.clock-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.clock-feedback.ok{color:var(--green)}.clock-feedback.bad{color:var(--red)}.clock-actions{display:flex;justify-content:center;gap:8px;flex-wrap:wrap}.clock-actions .tbtn:not(.ghost){background:var(--green);color:#fff}.clock-drag-hint{color:var(--muted);font-weight:800;text-align:center}
|
||||
@media(max-width:480px){.clock-task-card{padding:12px}.clock{--clock-size:min(210px,68vw)}.clock-digit-block.blok{width:44px;height:62px;min-height:62px}.clock-stepper .tbtn{width:34px}.clock-target{min-height:72px}}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"use strict";
|
||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
||||
Kept in sync by hand with the VERSION file at the repo root on every release. */
|
||||
const VERSION = "0.3.40-beta";
|
||||
const VERSION = "0.3.41-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -123,7 +123,7 @@ const I18N = {
|
|||
wg_schedule:"Dagplanning", wg_schedule_d:"Maak een duidelijke visuele planning voor de klas.",
|
||||
wg_mood:"Gevoelsmeter", wg_mood_d:"Doe een snelle, anonieme check-in met de klas.",
|
||||
wg_poll:"Klaspoll", wg_poll_d:"Stem samen op maximaal zes antwoorden.",
|
||||
widgetSettings:"Instellingen", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Leerwijze", clockStyleAnalogue:"Analoog", clockStyleDigital:"Digitaal", clockStyleWords:"In woorden", clockStyleBoth:"Alles samen", clockDragHint:"Sleep op de klok om de wijzers te verzetten.", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies ‘Nieuwe opdracht’ om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", fractionRows:"Rijen", fractionLabels:"Breuken tonen", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw",
|
||||
widgetSettings:"Instellingen", clockWelcome:"Oefenen met de klok", clockWelcomeHint:"Kies je niveau en spel bij Instellingen. Start daarna een opdracht.", clockSetTitle:"Zet de wijzers op de goede tijd", clockReadTitle:"Lees de klok en bouw het antwoord", clockChooseTitle:"Kies het juiste tijdblok", clockYourTask:"Jouw opdracht", clockCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", clockTryAgain:"Bijna! Probeer het nog een keer.", clockStartTask:"Start opdracht", clockNextTask:"Volgende opdracht", clockLevel:"Niveau", clockLevelHour:"Hele uren", clockLevelQuarter:"Kwartieren", clockLevelFive:"Vijf minuten", clockLevelMinute:"Iedere minuut", clockLearningStyle:"Leerwijze", clockStyleAnalogue:"Analoog", clockStyleDigital:"Digitaal", clockStyleWords:"In woorden", clockStyleBoth:"Alles samen", clockDragHint:"Sleep op de klok om de wijzers te verzetten.", clockGame:"Spel", clockGameSet:"Klok zetten", clockGameRead:"Tijd aflezen", clockGameChoose:"Kies de tijd", clockStart:"Kies ‘Nieuwe opdracht’ om te beginnen", clockRead:"Welke tijd zie je? Stel je antwoord hieronder in.", clockChoose:"Welke digitale tijd hoort bij deze klok?", clockTry:"Nog niet. Het goede antwoord is {time}.", clockScore:"Score: {score} · reeks: {streak}", clockWordsHour:"{hour} uur", clockWordsQuarterPast:"kwart over {hour}", clockWordsHalf:"half {hour}", clockWordsQuarterTo:"kwart voor {hour}", clockWordsPast:"{minutes} over {hour}", clockWordsTo:"{minutes} voor {hour}", fractionRows:"Rijen", fractionLabels:"Breuken tonen", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", galleryFavorites:"Favorieten", galleryFavorite:"Favoriet maken", galleryEmpty:"Geen widgets gevonden.", gallerySearch:"Zoek widgets…", templateMath:"Rekenles", templateLanguage:"Taalles", templateDay:"Dagstart", exportBoard:"Exporteren", importBoard:"Importeren", importError:"Dit is geen geldig Teach-bordbestand.", widgetDuplicate:"Dupliceren", widgetLock:"Vastzetten", widgetUnlock:"Losmaken", widgetPreview:"Voorbeeld zonder bediening", undo:"Ongedaan maken", redo:"Opnieuw",
|
||||
wg_sentence:"Zinsbouwer", wg_sentence_d:"Bouw zinnen met kleurgecodeerde zinsdelen en voeg eigen woorden toe.",
|
||||
sentence_who:"Wie?", sentence_does:"Doet wat?", sentence_what:"Wat of waar?", sentenceHint:"Kies woorden om een zin te maken…",
|
||||
sentenceAdd:"eigen woord of zinsdeel…", sentenceClear:"Wissen", sentenceSpeak:"🔊 Lees voor", sentenceRemove:"Verwijder woord",
|
||||
|
|
@ -321,7 +321,7 @@ const I18N = {
|
|||
wg_schedule:"Day schedule", wg_schedule_d:"Create a clear visual class schedule.",
|
||||
wg_mood:"Mood check-in", wg_mood_d:"Run a quick anonymous class check-in.",
|
||||
wg_poll:"Class poll", wg_poll_d:"Vote together on up to six answers.",
|
||||
widgetSettings:"Settings", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Learning style", clockStyleAnalogue:"Analogue", clockStyleDigital:"Digital", clockStyleWords:"In words", clockStyleBoth:"All together", clockDragHint:"Drag on the clock to move the hands.", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose ‘New task’ to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", fractionRows:"Rows", fractionLabels:"Show fractions", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo",
|
||||
widgetSettings:"Settings", clockWelcome:"Practise telling time", clockWelcomeHint:"Choose your level and game in Settings, then start a task.", clockSetTitle:"Move the hands to the correct time", clockReadTitle:"Read the clock and build the answer", clockChooseTitle:"Choose the correct time block", clockYourTask:"Your task", clockCorrect:"Well done! Choose the next task. 🌟", clockTryAgain:"Almost! Try again.", clockStartTask:"Start task", clockNextTask:"Next task", clockLevel:"Level", clockLevelHour:"Whole hours", clockLevelQuarter:"Quarter hours", clockLevelFive:"Five minutes", clockLevelMinute:"Every minute", clockLearningStyle:"Learning style", clockStyleAnalogue:"Analogue", clockStyleDigital:"Digital", clockStyleWords:"In words", clockStyleBoth:"All together", clockDragHint:"Drag on the clock to move the hands.", clockGame:"Game", clockGameSet:"Set the clock", clockGameRead:"Read the time", clockGameChoose:"Choose the time", clockStart:"Choose ‘New task’ to begin", clockRead:"What time is it? Set your answer below.", clockChoose:"Which digital time matches this clock?", clockTry:"Not yet. The correct answer is {time}.", clockScore:"Score: {score} · streak: {streak}", clockWordsHour:"{hour} o'clock", clockWordsQuarterPast:"quarter past {hour}", clockWordsHalf:"half past {hour}", clockWordsQuarterTo:"quarter to {hour}", clockWordsPast:"{minutes} past {hour}", clockWordsTo:"{minutes} to {hour}", fractionRows:"Rows", fractionLabels:"Show fractions", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", galleryFavorites:"Favourites", galleryFavorite:"Add favourite", galleryEmpty:"No widgets found.", gallerySearch:"Search widgets…", templateMath:"Maths lesson", templateLanguage:"Language lesson", templateDay:"Day start", exportBoard:"Export", importBoard:"Import", importError:"This is not a valid Teach board file.", widgetDuplicate:"Duplicate", widgetLock:"Lock", widgetUnlock:"Unlock", widgetPreview:"Preview without controls", undo:"Undo", redo:"Redo",
|
||||
wg_sentence:"Sentence builder", wg_sentence_d:"Build sentences with colour-coded parts and add your own words.",
|
||||
sentence_who:"Who?", sentence_does:"Does what?", sentence_what:"What or where?", sentenceHint:"Choose words to build a sentence…",
|
||||
sentenceAdd:"your own word or phrase…", sentenceClear:"Clear", sentenceSpeak:"🔊 Read aloud", sentenceRemove:"Remove word",
|
||||
|
|
|
|||
|
|
@ -15,17 +15,32 @@ function mountFractions(root,init){
|
|||
den.onchange=show.onchange=draw;learningShell(root,[field(T('fractionRows'),den),field(T('fractionLabels'),show)],stage);draw();return{getState:()=>({...S})};
|
||||
}
|
||||
function mountClock(root,init){
|
||||
const S=Object.assign({h:10,m:0,level:'quarter',style:'both',game:'set',task:false,targetH:3,targetM:30,score:0,streak:0},init||{}),level=document.createElement('select'),style=document.createElement('select'),game=document.createElement('select'),h=num(S.h,1,12),m=document.createElement('select'),stage=document.createElement('div');
|
||||
const add=(el,a)=>a.forEach(([v,k])=>el.add(new Option(T(k),v)));add(level,[['hour','clockLevelHour'],['quarter','clockLevelQuarter'],['five','clockLevelFive'],['minute','clockLevelMinute']]);add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);level.value=S.level;style.value=S.style;game.value=S.game;
|
||||
const steps=()=>S.level==='hour'?[0]:S.level==='quarter'?[0,15,30,45]:S.level==='five'?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i),time=(a,b)=>`${a}:${String(b).padStart(2,'0')}`;
|
||||
const fill=()=>{const old=+m.value;m.innerHTML='';steps().forEach(v=>m.add(new Option(String(v).padStart(2,'0'),v)));S.m=steps().includes(old)?old:steps().includes(S.m)?S.m:0;m.value=S.m};
|
||||
const words=(a,b)=>{const n=a===12?1:a+1;if(!b)return T('clockWordsHour').replace('{hour}',a);if(b===15)return T('clockWordsQuarterPast').replace('{hour}',a);if(b===30)return T('clockWordsHalf').replace('{hour}',n);if(b===45)return T('clockWordsQuarterTo').replace('{hour}',n);return T(b<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',b<30?b:60-b).replace('{hour}',b<30?a:n)};
|
||||
const random=()=>{const v=steps();return{h:1+Math.floor(Math.random()*12),m:v[Math.floor(Math.random()*v.length)]}};
|
||||
const face=(a,b)=>{const f=document.createElement('div');f.className='clock';f.setAttribute('role','img');f.setAttribute('aria-label',time(a,b));for(let i=0;i<60;i++){const x=document.createElement('i');x.className=i%5?'clock-tick minor':'clock-tick';x.style.transform=`rotate(${i*6}deg)`;f.append(x)}[12,1,2,3,4,5,6,7,8,9,10,11].forEach((n,i)=>{const x=document.createElement('b');x.textContent=n;x.style.setProperty('--clock-angle',`${i*30}deg`);f.append(x)});const ah=document.createElement('i'),am=document.createElement('i'),pin=document.createElement('span');ah.className='clock-hand clock-hour';am.className='clock-hand clock-min';pin.className='clock-pin';ah.style.transform=`rotate(${a*30+b/2}deg)`;am.style.transform=`rotate(${b*6}deg)`;f.append(ah,am,pin);if(S.game==='set'){f.classList.add('interactive');const move=e=>{const r=f.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2,deg=(Math.atan2(dx,-dy)*180/Math.PI+360)%360;if(f.dataset.hand==='minute'){const wanted=Math.round(deg/6)%60,allowed=steps();S.m=allowed.reduce((best,v)=>Math.min(Math.abs(v-wanted),60-Math.abs(v-wanted))<Math.min(Math.abs(best-wanted),60-Math.abs(best-wanted))?v:best);m.value=S.m}else{S.h=(Math.round((deg-S.m/2)/30)+11)%12+1;h.value=S.h}ah.style.transform=`rotate(${S.h*30+S.m/2}deg)`;am.style.transform=`rotate(${S.m*6}deg)`;const digital=stage.querySelector('.clock-digital'),written=stage.querySelector('.clock-words');if(digital)digital.textContent=time(S.h,S.m);if(written)written.textContent=words(S.h,S.m)};f.onpointerdown=e=>{const r=f.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;f.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';f.setPointerCapture(e.pointerId);move(e)};f.onpointermove=e=>{if(f.hasPointerCapture(e.pointerId))move(e)};f.onpointerup=e=>{move(e);f.releasePointerCapture(e.pointerId);delete f.dataset.hand;draw()};}return f};
|
||||
const feedback=ok=>{S.streak=ok?S.streak+1:0;if(ok)S.score++;ok?sndGood():sndBad();const p=stage.querySelector('.clock-feedback');p.textContent=T(ok?'numberlineGood':'clockTry').replace('{time}',time(S.targetH,S.targetM));p.className='clock-feedback '+(ok?'ok':'bad');stage.querySelector('.clock-score').textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak)};
|
||||
const check=()=>{if(S.task)feedback(+h.value===S.targetH&&+m.value===S.targetM)},newTask=()=>{const x=random();S.targetH=x.h;S.targetM=x.m;S.task=true;S.h=12;S.m=0;h.value=S.h;fill();m.value=S.m;draw()};
|
||||
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;S.h=Math.max(1,Math.min(12,+h.value||12));S.m=+m.value||0;stage.className='clock-wrap';stage.innerHTML='';const x=S.task&&S.game!=='set'?{h:S.targetH,m:S.targetM}:{h:S.h,m:S.m},title=document.createElement('h3');title.className='clock-prompt';title.textContent=!S.task?T('clockStart'):S.game==='set'?T('clockSet').replace('{time}',time(S.targetH,S.targetM)):S.game==='read'?T('clockRead'):T('clockChoose');stage.append(title);const lesson=document.createElement('div');lesson.className='clock-lesson';if(['analogue','both'].includes(S.style))lesson.append(face(x.h,x.m));if(['digital','both'].includes(S.style)){const o=document.createElement('output');o.className='clock-digital';o.textContent=time(x.h,x.m);lesson.append(o)}if(['words','both'].includes(S.style)){const p=document.createElement('p');p.className='clock-words';p.textContent=words(x.h,x.m);lesson.append(p)}stage.append(lesson);if(S.game==='set'&&['analogue','both'].includes(S.style)){const hint=document.createElement('small');hint.className='clock-drag-hint';hint.textContent=T('clockDragHint');stage.append(hint)}if(S.task&&S.game==='choose'){const a=[{h:S.targetH,m:S.targetM}];while(a.length<3){const c=random();if(!a.some(x=>x.h===c.h&&x.m===c.m))a.push(c)}a.sort(()=>Math.random()-.5);const choices=document.createElement('div');choices.className='clock-choices';a.forEach(c=>choices.append(btn(time(c.h,c.m),()=>feedback(c.h===S.targetH&&c.m===S.targetM))));stage.append(choices)}const status=document.createElement('p'),score=document.createElement('small');status.className='clock-feedback';score.className='clock-score';score.textContent=T('clockScore').replace('{score}',S.score).replace('{streak}',S.streak);stage.append(status,score)};
|
||||
level.onchange=()=>{S.level=level.value;fill();S.task=false;draw()};style.onchange=draw;game.onchange=()=>{S.task=false;draw()};h.oninput=m.onchange=draw;fill();learningShell(root,[field(T('clockLevel'),level),field(T('clockLearningStyle'),style),field(T('clockGame'),game),field(T('clockHour'),h),field(T('clockMinute'),m),btn(T('numberlineNew'),newTask),btn(T('numberlineCheck'),check,'')],stage);draw();return{getState:()=>({...S})};
|
||||
const S=Object.assign({h:12,m:0,level:'quarter',style:'both',game:'set',task:false,targetH:3,targetM:30,score:0,streak:0,solved:false,feedback:'',choices:[]},init||{});
|
||||
const level=document.createElement('select'),style=document.createElement('select'),game=document.createElement('select'),stage=document.createElement('div');
|
||||
const add=(el,items)=>items.forEach(([value,key])=>el.add(new Option(T(key),value)));
|
||||
add(level,[['hour','clockLevelHour'],['quarter','clockLevelQuarter'],['five','clockLevelFive'],['minute','clockLevelMinute']]);
|
||||
add(style,[['analogue','clockStyleAnalogue'],['digital','clockStyleDigital'],['words','clockStyleWords'],['both','clockStyleBoth']]);
|
||||
add(game,[['set','clockGameSet'],['read','clockGameRead'],['choose','clockGameChoose']]);
|
||||
level.value=S.level;style.value=S.style;game.value=S.game;
|
||||
const minuteValues=()=>S.level==='hour'?[0]:S.level==='quarter'?[0,15,30,45]:S.level==='five'?Array.from({length:12},(_,i)=>i*5):Array.from({length:60},(_,i)=>i);
|
||||
const time=(hour,minute)=>`${hour}:${String(minute).padStart(2,'0')}`;
|
||||
const words=(hour,minute)=>{const next=hour===12?1:hour+1;if(minute===0)return T('clockWordsHour').replace('{hour}',hour);if(minute===15)return T('clockWordsQuarterPast').replace('{hour}',hour);if(minute===30)return T('clockWordsHalf').replace('{hour}',next);if(minute===45)return T('clockWordsQuarterTo').replace('{hour}',next);return T(minute<30?'clockWordsPast':'clockWordsTo').replace('{minutes}',minute<30?minute:60-minute).replace('{hour}',minute<30?hour:next)};
|
||||
const randomTime=()=>{const values=minuteValues();return{h: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).padStart(2,'0');const dots=dotGrid(Number(value)%10);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));el.append(woodDigit(hour));const colon=document.createElement('strong');colon.textContent=':';el.append(colon,woodDigit(minute));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?'clock-tick minor':'clock-tick';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){hourHand.style.transform=`rotate(${hour*30+minute/2}deg)`;minuteHand.style.transform=`rotate(${minute*6}deg)`}else paint();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 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))}};face.onpointerdown=e=>{if(S.solved)return;e.preventDefault();const r=face.getBoundingClientRect(),dx=e.clientX-r.left-r.width/2,dy=e.clientY-r.top-r.height/2;face.dataset.hand=Math.hypot(dx,dy)>r.width*.22?'minute':'hour';face.setPointerCapture(e.pointerId);move(e)};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 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)setFeedback(S.h===S.targetH&&S.m===S.targetM)};
|
||||
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.choices=[];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 stepper=(label,values,key)=>{const wrap=document.createElement('div');wrap.className='clock-stepper';const name=document.createElement('span');name.textContent=label;const controls=document.createElement('div'),back=btn('−',()=>step(-1)),next=btn('+',()=>step(1));const value=woodDigit(S[key]);value.classList.add('clock-step-value');const step=delta=>{const index=values.indexOf(S[key]);S[key]=values[(index+delta+values.length)%values.length];S.feedback='';draw()};controls.append(back,value,next);wrap.append(name,controls);return wrap};
|
||||
const draw=()=>{S.level=level.value;S.style=style.value;S.game=game.value;stage.className='clock-app';stage.innerHTML='';const top=document.createElement('div');top.className='clock-top';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.task?T('clockWelcome'):S.game==='set'?T('clockSetTitle'):S.game==='read'?T('clockReadTitle'):T('clockChooseTitle');card.append(title);
|
||||
if(!S.task){const intro=document.createElement('p');intro.className='clock-intro';intro.textContent=T('clockWelcomeHint');card.append(intro)}
|
||||
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);if(S.style==='analogue'){const example=makeClock(S.targetH,S.targetM,false);example.classList.add('clock-reference');prompt.append(example)}if(['digital','both'].includes(S.style))prompt.append(timeBlocks(S.targetH,S.targetM));if(['words','both'].includes(S.style)){const written=document.createElement('strong');written.className='clock-target-words';written.textContent=words(S.targetH,S.targetM);prompt.append(written)}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 work=document.createElement('div');work.className='clock-work';work.append(makeClock(S.targetH,S.targetM,false));card.append(work)}
|
||||
if(S.task&&S.game==='read'){const answer=document.createElement('div');answer.className='clock-steppers';answer.append(stepper(T('clockHour'),Array.from({length:12},(_,i)=>i+1),'h'),stepper(T('clockMinute'),minuteValues(),'m'));card.append(answer)}
|
||||
if(S.task&&S.game==='choose'&&!S.solved){const choices=document.createElement('div');choices.className='clock-choices wood';S.choices.forEach(option=>{const choose=()=>setFeedback(option.h===S.targetH&&option.m===S.targetM);if(S.style==='words'){const b=btn(words(option.h,option.m),choose);b.classList.add('clock-word-choice');choices.append(b)}else choices.append(timeBlocks(option.h,option.m,choose))});card.append(choices)}
|
||||
const feedback=document.createElement('p');feedback.className='clock-feedback '+(S.feedback==='good'?'ok':S.feedback==='bad'?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'clockCorrect':'clockTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='clock-actions';if(S.task&&S.game!=='choose'&&!S.solved)actions.append(btn(T('numberlineCheck'),check,''));actions.append(btn(T(S.task?'clockNextTask':'clockStartTask'),newTask,S.task?'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)};
|
||||
level.onchange=()=>{S.level=level.value;if(!minuteValues().includes(S.m))S.m=0;S.task=false;S.solved=false;S.feedback='';draw()};style.onchange=draw;game.onchange=()=>{S.task=false;S.feedback='';draw()};learningShell(root,[field(T('clockLevel'),level),field(T('clockLearningStyle'),style),field(T('clockGame'),game)],stage);draw();return{getState:()=>({...S,choices:S.choices.map(x=>({...x}))})};
|
||||
}
|
||||
function mountPlacevalue(root,init){
|
||||
const S=Object.assign({number:1342},init||{}),input=num(S.number,0,999999),stage=document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -67,8 +67,11 @@ test('oefenklok biedt niveaus leerwijzen en meerdere spellen', async()=>{
|
|||
});
|
||||
|
||||
|
||||
test('analoge oefenklok is met aanwijzer en aanraking bedienbaar', async()=>{
|
||||
test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellingen', async()=>{
|
||||
const [source,css]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8')]);
|
||||
for(const feature of ['onpointerdown','onpointermove','setPointerCapture','releasePointerCapture','getBoundingClientRect'])assert.ok(source.includes(feature),feature);
|
||||
for(const feature of ['onpointerdown','onpointermove','setPointerCapture','releasePointerCapture','getBoundingClientRect','clock-time-blocks','dotGrid'])assert.ok(source.includes(feature),feature);
|
||||
assert.match(css,/clock\.interactive[^}]*touch-action:none/);
|
||||
assert.match(source,/learningShell\(root,\[field\(T\('clockLevel'\),level\),field\(T\('clockLearningStyle'\),style\),field\(T\('clockGame'\),game\)\],stage\)/);
|
||||
const clockSource=source.slice(source.indexOf('function mountClock'),source.indexOf('function mountPlacevalue'));
|
||||
assert.doesNotMatch(clockSource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|clockHour|clockMinute)/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue