From 84fd02d135ae4acd54d4fe98e6e470eb17d26a12 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 30 Jul 2026 21:39:30 +0200 Subject: [PATCH] fix: libelle en dierenvormen herstellen, instellingen omdraaien (v0.4.81-beta) - Libelle in de vijverplaat kreeg weer vier symmetrische vleugels - Zeepaardje in het koraalrif herbouwd van een onherkenbare buis naar een duidelijk zeepaardje, verplaatst zodat het niet meer over de clownvis valt - Nieuwe coloringLegPath()-vorm: giraffe en zebra hebben nu getapte, licht geknikte poten in plaats van rechte kokerpoten - Instellingenindeling omgedraaid: de kleurplaat (thema/plaat) is weer direct zichtbaar, kleurhulp/reset gaan achter het tandwiel - Letterblokken en rekenwidgets: niveauknoppen naar het tandwiel, het woord/thema blijft direct zichtbaar - Klok, geldrekenen, plaatswaardetabel, boom&blad/vogels/planten en topografie: spel/activiteit (en bij geld/plaatswaarde het bijpassende veld) staan weer in de vaste bovenbalk, niveau staat achter het tandwiel - Plaatswaardetabel toont nu ook in verkennen-stand een vaste bovenbalk (spel, getal, dobbelsteen) in plaats van alleen in bouwen-stand - Bugfix onderweg: niveauwissel in verkennen-stand van de plaatswaardetabel sprong per ongeluk naar bouwen-stand - nu blijft de stand behouden Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011YErxhic5w9K57zmt8Q1uS --- VERSION | 2 +- public/css/coloring.css | 23 +++-- public/css/teach.css | 8 ++ public/js/core.js | 6 +- public/js/widgets/coloring-scenes-advanced.js | 35 ++++---- public/js/widgets/coloring-scenes-senior.js | 21 ++--- public/js/widgets/coloring.js | 19 ++++- public/js/widgets/learning.js | 44 ++++++---- public/js/widgets/letters.js | 14 ++-- public/js/widgets/math.js | 12 ++- public/js/widgets/world.js | 84 +++++++++---------- test/widgets.test.js | 6 +- 12 files changed, 161 insertions(+), 113 deletions(-) diff --git a/VERSION b/VERSION index da7f3b8..a8ef683 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.80-beta +0.4.81-beta diff --git a/public/css/coloring.css b/public/css/coloring.css index d0986bd..d964524 100644 --- a/public/css/coloring.css +++ b/public/css/coloring.css @@ -6,12 +6,11 @@ bestand zet nooit een `fill` op een kleurvak - dat zou van de vulling van de verfemmer winnen en de verf onzichtbaar maken. */ .coloring-app{--coloring-ink:#1c293d;--coloring-detail:#2d3e56;height:100%;min-height:0;display:flex;flex-direction:column;background:linear-gradient(180deg,color-mix(in srgb,var(--orange) 4%,var(--surface)),var(--surface));color:var(--ink)} -/* de veelgebruikte rij (kwast/emmer/gum, kleur, dikte) blijft altijd - zichtbaar; thema, plaat, kleurhulp en reset - die je zelden aanpast - - zitten achter het ⚙-tandwiel (.edu-settings, zie teach.css) zodat ze niet - permanent ruimte innemen naast wat je continu gebruikt */ -.coloring-tools{display:flex;align-items:center;gap:7px;flex-wrap:wrap;padding:9px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));background:color-mix(in srgb,var(--surface) 96%,var(--orange))} -.coloring-choices{gap:8px} +/* de kleurplaat zelf moet snel te kiezen zijn: thema en plaat blijven dus + altijd zichtbaar, samen met kwast/emmer/gum. Kleurhulp en reset - die je + zelden aanpast - zitten achter het ⚙-tandwiel (.edu-settings, zie + teach.css) zodat ze niet permanent ruimte innemen. */ +.coloring-tools,.coloring-choices{display:flex;align-items:center;gap:7px;flex-wrap:wrap;padding:9px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));background:color-mix(in srgb,var(--surface) 96%,var(--orange))} .coloring-choices label{display:grid;grid-template-columns:auto minmax(120px,1fr);align-items:center;gap:6px;color:var(--muted);font-size:11px;font-weight:850} .coloring-choices select{height:36px;max-width:230px;border:1px solid color-mix(in srgb,var(--line) 72%,var(--orange));border-radius:11px;padding:0 30px 0 10px;background:var(--surface);color:var(--ink);box-shadow:0 2px 8px rgba(30,50,90,.07);font:inherit;font-weight:750} .coloring-fit{padding:6px 10px;border:1px solid color-mix(in srgb,var(--line) 75%,var(--orange));border-radius:999px;background:color-mix(in srgb,var(--surface) 92%,var(--orange));color:var(--muted);box-shadow:0 2px 8px rgba(30,50,90,.06);font-size:11px;font-weight:800;white-space:nowrap} @@ -28,9 +27,9 @@ .coloring-size.on{outline:2px solid var(--orange);outline-offset:-2px} .coloring-custom{display:flex;align-items:center;gap:5px;color:var(--muted);font-size:10px;font-weight:800} .coloring-custom input{width:36px;height:36px;padding:3px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 2px 7px rgba(30,50,90,.08);cursor:pointer} -.coloring-choices .coloring-guide-btn{background:color-mix(in srgb,var(--teal) 14%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--teal) 20%,transparent)} -.coloring-choices .coloring-guide-btn.on{outline:2px solid var(--teal);outline-offset:-2px} -.coloring-choices .coloring-reset{background:var(--red-soft);color:var(--red);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--red) 18%,transparent)} +.coloring-guide-btn{width:36px;height:36px;border:0;border-radius:10px;font-size:18px;cursor:pointer;background:color-mix(in srgb,var(--teal) 14%,var(--surface));box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--teal) 20%,transparent)} +.coloring-guide-btn.on{outline:2px solid var(--teal);outline-offset:-2px} +.coloring-reset{width:36px;height:36px;border:0;border-radius:10px;font-size:18px;cursor:pointer;background:var(--red-soft);color:var(--red);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--red) 18%,transparent)} .coloring-tools .coloring-done{width:auto;min-width:112px;margin-left:auto;padding:0 14px;color:#fff;box-shadow:0 4px 10px color-mix(in srgb,var(--orange) 24%,transparent);font-size:13px} .coloring-stage{flex:1;min-height:0;overflow:auto;display:grid;place-items:center;padding:18px;background:radial-gradient(circle at 12% 18%,color-mix(in srgb,var(--orange) 13%,transparent),transparent 31%),radial-gradient(circle at 88% 82%,color-mix(in srgb,var(--purple) 10%,transparent),transparent 34%),linear-gradient(145deg,var(--surface-2),color-mix(in srgb,var(--purple) 5%,var(--surface)))} /* het papier vangt alle aanwijzing op; binnen de SVG wordt meetkundig @@ -65,11 +64,11 @@ widget-venster (dat kan smal staan op een brede monitor), niet op de breedte van de hele browser - zie .widget-body{container-name:widget} */ @container widget (max-width:620px){ - .coloring-tools{padding:6px;gap:4px} + .coloring-tools,.coloring-choices{padding:6px;gap:4px} .coloring-choices label{grid-template-columns:1fr;gap:2px;flex:1;min-width:125px} .coloring-choices select{width:100%;max-width:none;height:32px} .coloring-fit{width:100%;text-align:center} - .coloring-tools button{width:32px;height:32px;font-size:16px} + .coloring-tools button,.coloring-guide-btn,.coloring-reset{width:32px;height:32px;font-size:16px} .coloring-tool-buttons button{min-width:36px;width:36px;padding:0}.coloring-tool-label{display:none} .coloring-color i{width:18px;height:18px} .coloring-custom span{display:none}.coloring-custom input{width:32px;height:32px} @@ -78,5 +77,5 @@ .coloring-paper{min-width:360px;border-radius:10px} .coloring-hint{font-size:10px} } -@media(pointer:coarse){.coloring-tools button,.coloring-custom input{min-width:40px;min-height:40px}.coloring-choices select{min-height:40px}} +@media(pointer:coarse){.coloring-tools button,.coloring-custom input,.coloring-guide-btn,.coloring-reset{min-width:40px;min-height:40px}.coloring-choices select{min-height:40px}} @media(prefers-reduced-motion:reduce){.coloring-app *{scroll-behavior:auto!important;transition:none!important}} diff --git a/public/css/teach.css b/public/css/teach.css index 41041ec..f742943 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -1559,6 +1559,10 @@ body.tb-left #rolePreviewBanner,body.tb-right #rolePreviewBanner{top:8px} .managed-learning-level .clock-level-control, .managed-learning-level .place-level-control, .managed-learning-level .money-level-control{display:none;} +/* niveau kan ook als tandwielveld staan (gearField/field rond .lg-levels) - + dan moet het hele label (incl. "Niveau"-tekst) verdwijnen, niet alleen de + knoppenrij erin, anders blijft er een lege regel in het tandwielpaneel over */ +.managed-learning-level .edu-controls label:has(.lg-levels){display:none;} .board-learning-level{display:grid;grid-template-columns:auto auto minmax(130px,1fr);align-items:end;gap:8px;padding:8px 10px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--accent) 7%,var(--surface));font-size:11px} .board-learning-level label{display:grid;gap:3px;color:var(--muted);font-weight:800} .board-learning-select{min-width:104px;border:1px solid var(--line);border-radius:8px;padding:6px 8px;background:var(--surface);color:var(--ink);font:inherit;font-weight:750} @@ -1994,6 +1998,10 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2 .topo-province.inactive{opacity:.32}.topo-region[role=button],.topo-river[role=button],.world-continent[role=button]{cursor:pointer}.topo-region.picked .topo-province{fill:var(--red)!important}.topo-region.target .topo-province{fill:var(--green)!important}.topo-region:hover .topo-province,.topo-region:focus .topo-province{filter:brightness(.88) saturate(1.12)}.topo-region:focus-visible .topo-province{stroke:var(--navy);stroke-width:8}.topo-region-label{font-size:20px} .topo-river.inactive{opacity:.22}.topo-river-hit,.topo-river-line{fill:none;stroke-linecap:round;stroke-linejoin:round}.topo-river-hit{stroke:transparent;stroke-width:38;pointer-events:stroke}.topo-river-line{stroke:var(--river-color);stroke-width:10;pointer-events:none;transition:stroke .15s,stroke-width .15s}.topo-river:hover .topo-river-line,.topo-river:focus .topo-river-line{stroke-width:15}.topo-river:focus-visible .topo-river-line{stroke:var(--navy);stroke-width:18}.topo-river.picked .topo-river-line{stroke:var(--red)}.topo-river.target .topo-river-line{stroke:var(--green);stroke-width:16}.topo-river-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:5px;stroke-linejoin:round;font-size:20px;font-style:italic;font-weight:950;text-anchor:middle;pointer-events:none} .world-map-wrap{padding:12px}.world-map{width:100%;max-width:680px;max-height:none}.world-continent{stroke:#fff;stroke-width:1.4;stroke-linejoin:round;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s,fill .15s}.world-continent.inactive{opacity:.28}.world-continent:hover,.world-continent:focus{filter:brightness(.88) saturate(1.12);outline:none}.world-continent:focus-visible{stroke:var(--navy);stroke-width:4}.world-continent.picked{fill:var(--red)!important}.world-continent.target{fill:var(--green)!important}.world-continent-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:3px;stroke-linejoin:round;font-size:14px;font-weight:950;text-anchor:middle;pointer-events:none} +/* een veelgebruikte keuze (spel/activiteit) die in de vaste bovenbalk van + klok/geld/plaatswaarde/wereld staat i.p.v. achter het tandwiel */ +.top-field{display:flex;flex-direction:column;gap:2px;font-size:11px;font-weight:800;color:var(--muted)} +.top-field select{height:36px;border:1px solid var(--line);border-radius:9px;padding:0 8px;background:var(--surface);color:var(--ink);font:inherit;font-weight:750} .topo-world-legend{width:100%;display:flex;flex-wrap:wrap;justify-content:center;gap:6px}.topo-world-legend span{display:flex;align-items:center;gap:5px;padding:5px 8px;border:1px solid color-mix(in srgb,var(--line) 75%,transparent);border-radius:999px;background:var(--surface);color:var(--ink);font-size:11px;font-weight:850}.topo-world-legend i{width:10px;height:10px;flex:none;border:1px solid rgba(24,50,78,.18);border-radius:50%} body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .world-identity em{color:#a9e5b7}body.dark .topo-map-wrap{background:linear-gradient(155deg,#193446,#122c3e)}body.dark .topo-water{fill:#173145}body.dark .topo-label,body.dark .topo-city-label,body.dark .topo-river-label,body.dark .world-continent-label{fill:#f7fbff;stroke:#18324e}body.dark .topo-source{color:#a9cadb} diff --git a/public/js/core.js b/public/js/core.js index f6de1ad..9bae53b 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -2,7 +2,7 @@ "use strict"; /* version — shown until /api/version resolves (or if the fetch fails, e.g. offline). Kept in sync by hand with the VERSION file at the repo root on every release. */ -const VERSION = "0.4.80-beta"; +const VERSION = "0.4.81-beta"; /* Safari en oudere mobiele browsers behandelen 100vh/100% soms als de hoogte achter hun adres- en navigatiebalk. visualViewport geeft de werkelijk zichtbare hoogte; de CSS gebruikt deze pixelwaarde als betrouwbare bron en @@ -201,7 +201,7 @@ const I18N = { avatarAcc_backpack:"Rugtas", avatarAcc_satchel:"Schoudertas", avatarAcc_wings:"Vleugels", avatarAcc_star:"Gouden ster", avatarAcc_medal:"Medaille", avatarSave:"Opslaan", avatarSaved:"Personage opgeslagen ✓", clock24Hour:"24-uursklok", - widgetSettings:"Instellingen", clockWelcome:"Oefenen met de klok", clockWelcomeHint:"Kies bovenaan niveau 1, 2 of 3. Kies bij Instellingen hoe de opdracht wordt getoond en start daarna.", clockSetTitle:"Zet de wijzers op de goede tijd", clockReadTitle:"Bekijk de opdracht en maak de tijd", clockChooseTitle:"Kies het juiste tijdblok", clockYourTask:"Jouw opdracht", clockLinkHands:"Grote wijzer draait mee", clockTypeHint:"Voer de tijd in als UU:MM.", clockLevel1Hint:"Niveau 1: hele en halve uren", clockLevel2Hint:"Niveau 2: stappen van vijf minuten", clockLevel3Hint:"Niveau 3: alle minuten, met optioneel 24 uur en dagdelen", clockBuildHint:"Sleep vier cijfers naar de vakken en maak UU:MM.", clockDigitGhost:"?", clockDigit:"Cijfer {digit}", clockIncomplete:"Maak eerst de volledige tijd af.", 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:"Opdracht tonen", clockStyleAnalogue:"Analoge klok", clockStyleDigital:"Grote digitale tijd", clockStyleWords:"In woorden", clockStyleBoth:"Klok, cijfers en woorden", clockDragHint:"Sleep de wijzers om de tijd te veranderen.", 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}", clockMorning:"in de ochtend", clockAfternoon:"in de middag", clockEvening:"in de avond", clockNight:"in de nacht", fractionRows:"Rijen", fractionLabels:"Breuken tonen", fractionHint:"Tik in een rij tot waar je de breuk wilt kleuren.", fractionSelected:"Gekozen breuk: {fraction}", fractionClear:"Leegmaken", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", placeGame:"Spel", placeGameExplore:"Verkennen", placeGameBuild:"Bouwen", placeLevel:"Niveau", placeLevel1Hint:"Niveau 1: getallen tot 100 (T en E)", placeLevel2Hint:"Niveau 2: getallen tot 1000 (H, T en E)", placeLevel3Hint:"Niveau 3: getallen tot 1.000.000 (HD, TD, D, H, T en E)", placeBuildTitle:"Bouw het getal", placeYourTask:"Jouw opdracht", placeBuildHint:"Sleep de cijfers naar de vakken en bouw het getal.", placeDigitGhost:"?", placeIncomplete:"Maak eerst het hele getal af.", placeCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", placeTryAgain:"Bijna! Probeer het nog een keer.", placeNextTask:"Volgende opdracht", placeScore:"Score: {score} · reeks: {streak}", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", scheduleCount:"{count} momenten op de planning", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", pollTotal:"{count} stemmen", moodTotal:"{count} kinderen hebben gekozen", moodVote:"Kies {face}", 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", levelWidget:"Niveau", clockWelcome:"Oefenen met de klok", clockWelcomeHint:"Kies bovenaan niveau 1, 2 of 3. Kies bij Instellingen hoe de opdracht wordt getoond en start daarna.", clockSetTitle:"Zet de wijzers op de goede tijd", clockReadTitle:"Bekijk de opdracht en maak de tijd", clockChooseTitle:"Kies het juiste tijdblok", clockYourTask:"Jouw opdracht", clockLinkHands:"Grote wijzer draait mee", clockTypeHint:"Voer de tijd in als UU:MM.", clockLevel1Hint:"Niveau 1: hele en halve uren", clockLevel2Hint:"Niveau 2: stappen van vijf minuten", clockLevel3Hint:"Niveau 3: alle minuten, met optioneel 24 uur en dagdelen", clockBuildHint:"Sleep vier cijfers naar de vakken en maak UU:MM.", clockDigitGhost:"?", clockDigit:"Cijfer {digit}", clockIncomplete:"Maak eerst de volledige tijd af.", 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:"Opdracht tonen", clockStyleAnalogue:"Analoge klok", clockStyleDigital:"Grote digitale tijd", clockStyleWords:"In woorden", clockStyleBoth:"Klok, cijfers en woorden", clockDragHint:"Sleep de wijzers om de tijd te veranderen.", 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}", clockMorning:"in de ochtend", clockAfternoon:"in de middag", clockEvening:"in de avond", clockNight:"in de nacht", fractionRows:"Rijen", fractionLabels:"Breuken tonen", fractionHint:"Tik in een rij tot waar je de breuk wilt kleuren.", fractionSelected:"Gekozen breuk: {fraction}", fractionClear:"Leegmaken", clockTask:"Oefenopdracht", clockSet:"Zet de klok op {time}", clockHour:"Uur", clockMinute:"Minuten", placeNumber:"Getal", placeGame:"Spel", placeGameExplore:"Verkennen", placeGameBuild:"Bouwen", placeLevel:"Niveau", placeLevel1Hint:"Niveau 1: getallen tot 100 (T en E)", placeLevel2Hint:"Niveau 2: getallen tot 1000 (H, T en E)", placeLevel3Hint:"Niveau 3: getallen tot 1.000.000 (HD, TD, D, H, T en E)", placeBuildTitle:"Bouw het getal", placeYourTask:"Jouw opdracht", placeBuildHint:"Sleep de cijfers naar de vakken en bouw het getal.", placeDigitGhost:"?", placeIncomplete:"Maak eerst het hele getal af.", placeCorrect:"Goed gedaan! Kies de volgende opdracht. 🌟", placeTryAgain:"Bijna! Probeer het nog een keer.", placeNextTask:"Volgende opdracht", placeScore:"Score: {score} · reeks: {streak}", moneyMake:"Maak € {amount}", wordtypeSentence:"Zin", wordtypeHint:"Tik: blauw zelfstandig naamwoord · groen werkwoord · geel bijvoeglijk naamwoord", scheduleActivity:"activiteit…", scheduleEmpty:"Voeg de eerste activiteit toe.", scheduleCount:"{count} momenten op de planning", pollReset:"Opnieuw", pollQuestion:"Vraag", pollOptions:"Antwoorden (één per regel)", pollTotal:"{count} stemmen", moodTotal:"{count} kinderen hebben gekozen", moodVote:"Kies {face}", 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", sentenceSurprise:"✨ Verrassingszin", @@ -634,7 +634,7 @@ const I18N = { avatarAcc_backpack:"Backpack", avatarAcc_satchel:"Satchel", avatarAcc_wings:"Wings", avatarAcc_star:"Golden star", avatarAcc_medal:"Medal", avatarSave:"Save", avatarSaved:"Character saved ✓", clock24Hour:"24-hour clock", - widgetSettings:"Settings", clockWelcome:"Practise telling time", clockWelcomeHint:"Choose level 1, 2 or 3 at the top. In Settings, choose how the task is shown, then start.", clockSetTitle:"Move the hands to the correct time", clockReadTitle:"View the task and make the time", clockChooseTitle:"Choose the correct time block", clockYourTask:"Your task", clockLinkHands:"Move the minute hand along", clockTypeHint:"Enter the time as HH:MM.", clockLevel1Hint:"Level 1: whole and half hours", clockLevel2Hint:"Level 2: five-minute steps", clockLevel3Hint:"Level 3: every minute, with optional 24-hour time and day parts", clockBuildHint:"Drag four digits into the slots to make HH:MM.", clockDigitGhost:"?", clockDigit:"Digit {digit}", clockIncomplete:"Complete the full time first.", 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:"Show task as", clockStyleAnalogue:"Analogue clock", clockStyleDigital:"Large digital time", clockStyleWords:"In words", clockStyleBoth:"Clock, digits and words", clockDragHint:"Drag the hands to change the time.", 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}", clockMorning:"in the morning", clockAfternoon:"in the afternoon", clockEvening:"in the evening", clockNight:"at night", fractionRows:"Rows", fractionLabels:"Show fractions", fractionHint:"Tap a row up to the part you want to colour.", fractionSelected:"Selected fraction: {fraction}", fractionClear:"Clear", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", placeGame:"Game", placeGameExplore:"Explore", placeGameBuild:"Build", placeLevel:"Level", placeLevel1Hint:"Level 1: numbers up to 100 (T and E)", placeLevel2Hint:"Level 2: numbers up to 1000 (H, T and E)", placeLevel3Hint:"Level 3: numbers up to 1,000,000 (HD, TD, D, H, T and E)", placeBuildTitle:"Build the number", placeYourTask:"Your task", placeBuildHint:"Drag the digits into the slots to build the number.", placeDigitGhost:"?", placeIncomplete:"Complete the whole number first.", placeCorrect:"Well done! Choose the next task. 🌟", placeTryAgain:"Almost! Try again.", placeNextTask:"Next task", placeScore:"Score: {score} · streak: {streak}", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", scheduleCount:"{count} moments in the schedule", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", pollTotal:"{count} votes", moodTotal:"{count} children have chosen", moodVote:"Choose {face}", 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", levelWidget:"Level", clockWelcome:"Practise telling time", clockWelcomeHint:"Choose level 1, 2 or 3 at the top. In Settings, choose how the task is shown, then start.", clockSetTitle:"Move the hands to the correct time", clockReadTitle:"View the task and make the time", clockChooseTitle:"Choose the correct time block", clockYourTask:"Your task", clockLinkHands:"Move the minute hand along", clockTypeHint:"Enter the time as HH:MM.", clockLevel1Hint:"Level 1: whole and half hours", clockLevel2Hint:"Level 2: five-minute steps", clockLevel3Hint:"Level 3: every minute, with optional 24-hour time and day parts", clockBuildHint:"Drag four digits into the slots to make HH:MM.", clockDigitGhost:"?", clockDigit:"Digit {digit}", clockIncomplete:"Complete the full time first.", 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:"Show task as", clockStyleAnalogue:"Analogue clock", clockStyleDigital:"Large digital time", clockStyleWords:"In words", clockStyleBoth:"Clock, digits and words", clockDragHint:"Drag the hands to change the time.", 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}", clockMorning:"in the morning", clockAfternoon:"in the afternoon", clockEvening:"in the evening", clockNight:"at night", fractionRows:"Rows", fractionLabels:"Show fractions", fractionHint:"Tap a row up to the part you want to colour.", fractionSelected:"Selected fraction: {fraction}", fractionClear:"Clear", clockTask:"Practice task", clockSet:"Set the clock to {time}", clockHour:"Hour", clockMinute:"Minutes", placeNumber:"Number", placeGame:"Game", placeGameExplore:"Explore", placeGameBuild:"Build", placeLevel:"Level", placeLevel1Hint:"Level 1: numbers up to 100 (T and E)", placeLevel2Hint:"Level 2: numbers up to 1000 (H, T and E)", placeLevel3Hint:"Level 3: numbers up to 1,000,000 (HD, TD, D, H, T and E)", placeBuildTitle:"Build the number", placeYourTask:"Your task", placeBuildHint:"Drag the digits into the slots to build the number.", placeDigitGhost:"?", placeIncomplete:"Complete the whole number first.", placeCorrect:"Well done! Choose the next task. 🌟", placeTryAgain:"Almost! Try again.", placeNextTask:"Next task", placeScore:"Score: {score} · streak: {streak}", moneyMake:"Make € {amount}", wordtypeSentence:"Sentence", wordtypeHint:"Tap: blue noun · green verb · yellow adjective", scheduleActivity:"activity…", scheduleEmpty:"Add the first activity.", scheduleCount:"{count} moments in the schedule", pollReset:"Reset", pollQuestion:"Question", pollOptions:"Answers (one per line)", pollTotal:"{count} votes", moodTotal:"{count} children have chosen", moodVote:"Choose {face}", 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", sentenceSurprise:"✨ Surprise sentence", diff --git a/public/js/widgets/coloring-scenes-advanced.js b/public/js/widgets/coloring-scenes-advanced.js index 6f0c4c5..c38a08a 100644 --- a/public/js/widgets/coloring-scenes-advanced.js +++ b/public/js/widgets/coloring-scenes-advanced.js @@ -238,14 +238,18 @@ function plateEcosystem(){ coloringFlat("frog-belly","M 266 496 C 282 526 302 540 334 542 C 366 540 388 526 402 496 C 400 532 372 548 334 548 C 296 548 268 532 266 496 Z","#f7c948"),2)+ coloringLine("M 306 448 C 320 458 348 458 362 448",2)+ coloringLine("M 274 480 C 296 490 372 490 394 480 M 260 524 C 284 536 384 536 408 524",3))+ + /* vier vleugels symmetrisch aan weerszijden van het lijf in plaats van + twee dunne slierten aan één kant - dat las niet als vleugels */ coloringGroup("dragonfly", - coloringFlat("dragonfly-wing-upper","M 596 236 C 636 214 690 212 720 226 C 690 250 638 256 600 248 Z","#2fb7a8")+ - coloringFlat("dragonfly-wing-lower","M 596 252 C 636 268 686 276 714 290 C 682 302 632 292 598 268 Z","#2fb7a8")+ - coloringContour("dragonfly-body","M 566 232 C 578 226 592 226 600 234 C 602 262 598 296 588 322 C 578 300 570 266 566 232 Z","#7557c8")+ - coloringCircle("dragonfly-head",582,220,20,"#7557c8","coloring-contour")+ + coloringFlat("dragonfly-wing-front-right",coloringPetalPath(600,250,-15,95,26),"#2fb7a8")+ + coloringFlat("dragonfly-wing-back-right",coloringPetalPath(600,250,25,85,24),"#2fb7a8")+ + coloringFlat("dragonfly-wing-front-left",coloringPetalPath(600,250,195,95,26),"#2fb7a8")+ + coloringFlat("dragonfly-wing-back-left",coloringPetalPath(600,250,155,85,24),"#2fb7a8")+ + coloringContour("dragonfly-body","M 588 232 C 588 224 596 220 600 220 C 604 220 612 224 612 232 C 616 260 614 300 606 336 C 604 342 596 342 594 336 C 586 300 584 260 588 232 Z","#7557c8")+ + coloringCircle("dragonfly-head",600,200,19,"#7557c8","coloring-contour")+ coloringGroup("dragonfly-detail", - coloringDot(574,214,5)+coloringDot(590,214,5)+ - coloringLine("M 572 254 L 596 254 M 574 274 L 594 274 M 578 294 L 592 294",2)))+ + coloringDot(592,196,5)+coloringDot(608,196,5)+ + coloringLine("M 590 250 L 610 250 M 592 275 L 608 275 M 594 300 L 606 300",2)))+ coloringGroup("front-reeds", coloringLine("M 160 590 C 168 556 172 534 170 512 M 190 594 C 200 562 204 542 204 520 M 640 588 C 648 556 652 534 650 514",2),2) ); @@ -272,15 +276,16 @@ function plateSavanna(){ coloringLine("M 166 232 C 162 288 164 344 172 392 M 186 232 C 182 288 186 344 194 392",2))+ coloringLine("M 80 206 C 120 202 240 202 280 206 M 100 154 C 130 148 200 148 240 154",3))+ coloringGroup("giraffe", - /* vier poten met een knik en een hoef; de nek loopt breed uit in de + /* getapte poten met een lichte knik ipv rechte kokers, en een hoef + die uit de poot zelf voortkomt; de nek loopt breed uit in de schouder in plaats van als losse balk op het lichaam te staan */ - coloringFlat("giraffe-hind-leg-far","M 648 462 L 678 462 L 674 516 L 686 566 L 658 570 L 646 516 Z","#f7c948")+ - coloringFlat("giraffe-fore-leg-far","M 540 458 L 568 458 L 568 514 L 576 566 L 548 570 L 540 514 Z","#f7c948")+ + coloringFlat("giraffe-hind-leg-far",coloringLegPath(663,462,566,30,15,26,5),"#f7c948")+ + coloringFlat("giraffe-fore-leg-far",coloringLegPath(554,458,566,28,14,24,-4),"#f7c948")+ coloringFlat("giraffe-tail","M 686 424 C 706 442 718 466 720 492 C 706 488 692 472 682 450 Z","#f7c948")+ coloringContour("giraffe-body","M 502 448 C 500 402 540 378 596 378 C 654 378 692 402 694 448 C 696 486 652 502 594 502 C 536 502 504 486 502 448 Z","#f7c948")+ coloringContour("giraffe-neck","M 496 424 C 480 356 474 280 480 200 L 530 192 C 532 272 542 352 558 416 C 538 432 512 434 496 424 Z","#f7c948")+ - coloringFlat("giraffe-hind-leg-near","M 612 468 L 646 468 L 642 522 L 654 576 L 620 580 L 608 522 Z","#f7c948")+ - coloringFlat("giraffe-fore-leg-near","M 506 464 L 540 464 L 538 520 L 546 576 L 512 580 L 504 520 Z","#f7c948")+ + coloringFlat("giraffe-hind-leg-near",coloringLegPath(629,468,578,32,16,28,7),"#f7c948")+ + coloringFlat("giraffe-fore-leg-near",coloringLegPath(523,464,578,32,15,28,-6),"#f7c948")+ coloringContour("giraffe-head","M 476 204 C 460 176 474 146 504 140 C 536 134 560 154 562 182 C 546 206 506 216 476 204 Z","#f7c948")+ coloringFlat("giraffe-muzzle","M 478 174 C 452 166 432 176 426 194 C 442 210 466 214 486 206 Z","#f2b38f")+ coloringGroup("giraffe-detail", @@ -305,13 +310,13 @@ function plateSavanna(){ coloringLine("M 434 190 C 438 200 446 206 456 208 M 682 450 C 692 462 700 476 704 490",2)+ coloringLine("M 512 444 C 548 458 630 462 684 452 M 494 256 C 510 262 526 270 536 276 M 548 520 C 560 526 574 526 584 522",3))+ coloringGroup("zebra", - coloringFlat("zebra-hind-leg-far","M 352 458 L 380 458 L 376 508 L 386 556 L 360 560 L 350 508 Z","#ffffff")+ - coloringFlat("zebra-fore-leg-far","M 254 456 L 280 456 L 280 506 L 286 556 L 260 560 L 254 506 Z","#ffffff")+ + coloringFlat("zebra-hind-leg-far",coloringLegPath(366,458,560,28,14,24,5),"#ffffff")+ + coloringFlat("zebra-fore-leg-far",coloringLegPath(267,456,560,26,13,22,-4),"#ffffff")+ coloringFlat("zebra-tail","M 384 428 C 404 442 416 462 420 486 C 406 482 392 468 382 448 Z","#ffffff")+ coloringContour("zebra-body","M 222 446 C 220 404 258 380 306 380 C 356 380 392 404 394 446 C 396 480 356 496 306 496 C 254 496 224 480 222 446 Z","#ffffff")+ coloringContour("zebra-neck","M 222 428 C 212 388 202 352 196 322 L 248 306 C 260 348 272 388 284 424 C 264 442 240 444 222 428 Z","#ffffff")+ - coloringFlat("zebra-hind-leg-near","M 322 464 L 352 464 L 348 514 L 358 566 L 328 570 L 318 514 Z","#ffffff")+ - coloringFlat("zebra-fore-leg-near","M 224 462 L 254 462 L 252 512 L 258 566 L 228 570 L 222 512 Z","#ffffff")+ + coloringFlat("zebra-hind-leg-near",coloringLegPath(337,464,570,30,15,26,6),"#ffffff")+ + coloringFlat("zebra-fore-leg-near",coloringLegPath(239,462,570,30,14,26,-5),"#ffffff")+ coloringContour("zebra-head","M 186 324 C 168 298 182 268 210 262 C 240 256 264 276 266 304 C 248 326 214 336 186 324 Z","#ffffff")+ coloringFlat("zebra-muzzle","M 188 292 C 164 284 144 294 138 312 C 154 328 176 332 196 324 Z","#26344a")+ coloringGroup("zebra-stripes", diff --git a/public/js/widgets/coloring-scenes-senior.js b/public/js/widgets/coloring-scenes-senior.js index 1b60bfd..9064c3f 100644 --- a/public/js/widgets/coloring-scenes-senior.js +++ b/public/js/widgets/coloring-scenes-senior.js @@ -190,18 +190,19 @@ function plateReef(){ coloringDot(498,278,6),2)+ coloringLine("M 520 250 C 532 270 536 288 536 298 C 536 310 532 328 520 346",2)+ coloringLine("M 318 292 C 336 284 352 280 368 278 M 318 308 C 336 314 352 318 368 320",3))+ + /* paardenkop met snuit, een gewelfde rug en een duidelijk gekrulde staart + - los van elkaar getekend zodat elk onderdeel scherp herkenbaar blijft, + in plaats van één doorlopend buis-silhouet dat als worm of blob las */ coloringGroup("seahorse", - coloringFlat("seahorse-fin","M 646 264 C 668 256 688 262 696 278 C 680 292 656 292 642 282 Z","#f7c948")+ - /* stevige romp met een aparte gekrulde staart: het smalle buisje dat er - eerst stond las als een worm */ - coloringContour("seahorse-body","M 586 208 C 626 202 654 228 654 268 C 654 302 638 330 620 352 L 580 336 C 598 310 614 286 614 262 C 614 238 602 226 582 228 Z","#f7c948")+ - coloringContour("seahorse-tail","M 580 336 L 620 352 C 608 376 612 396 630 402 C 648 408 662 396 664 380 C 674 410 646 434 618 424 C 586 412 572 374 580 336 Z","#f7c948")+ - coloringFlat("seahorse-snout","M 574 202 C 548 194 528 202 522 218 C 536 234 560 240 582 232 Z","#f7c948")+ + coloringContour("seahorse-head","M 731 190 C 731 168 715 152 693 152 C 673 152 659 166 657 186 C 637 182 619 190 613 206 C 627 220 649 222 665 214 C 671 226 683 234 697 234 C 717 234 733 218 733 198 C 733 195 732 192 731 190 Z","#f7c948")+ + coloringContour("seahorse-body","M 681 232 C 665 255 660 285 670 312 L 690 322 C 700 292 713 262 717 234 C 707 226 691 226 681 232 Z","#f7c948")+ + coloringContour("seahorse-tail","M 670 312 L 690 322 C 701 348 695 376 671 388 C 647 400 621 388 615 364 C 611 348 621 332 637 328 C 645 326 653 328 657 334 L 670 312 Z","#f7c948")+ + coloringFlat("seahorse-fin",coloringPetalPath(713,244,-40,42,17),"#2fb7a8")+ coloringGroup("seahorse-detail", - coloringFlat("seahorse-crown","M 600 208 C 596 182 606 162 624 154 C 628 178 620 198 610 212 Z","#f28c3c")+ - coloringCircle("seahorse-eye",594,224,11,"#ffffff")+ - coloringDot(594,224,5)+ - coloringLine("M 606 254 C 620 258 632 256 642 250 M 600 288 C 614 292 626 290 636 284 M 588 320 C 600 326 612 326 620 322 M 596 372 C 608 378 620 378 628 374",2)))+ + coloringFlat("seahorse-crown","M 695 154 C 691 136 697 122 709 116 C 715 132 711 148 701 158 Z","#f28c3c")+ + coloringCircle("seahorse-eye",705,180,11,"#ffffff")+ + coloringDot(705,180,5)+ + coloringLine("M 689 244 C 697 248 707 246 715 240 M 677 274 C 685 278 695 276 703 270 M 637 362 C 643 368 653 370 661 366",2)))+ coloringGroup("starfish", coloringFlat("starfish",coloringStarPath(456,528,50),"#e0554d")+ coloringGroup("starfish-detail", diff --git a/public/js/widgets/coloring.js b/public/js/widgets/coloring.js index dd92647..f0393b0 100644 --- a/public/js/widgets/coloring.js +++ b/public/js/widgets/coloring.js @@ -100,6 +100,19 @@ function coloringPetalPath(cx,cy,angle,len,wide){ return `M ${n(cx)} ${n(cy)} C ${n(cx+nx+ux*len*.28)} ${n(cy+ny+uy*len*.28)} ${n(tipX+nx*.52)} ${n(tipY+ny*.52)} ${n(tipX)} ${n(tipY)}`+ ` C ${n(tipX-nx*.52)} ${n(tipY-ny*.52)} ${n(cx-nx+ux*len*.28)} ${n(cy-ny+uy*len*.28)} ${n(cx)} ${n(cy)} Z`; } +/* Getapt dierenbeen: schouderbreedte boven, smal bij de enkel, breder bij de + hoef, met een lichte knik zodat het geen rechte kokerpoot wordt. */ +function coloringLegPath(cx,topY,hoofY,topW,kneeW,hoofW,bend=0){ + const n=coloringRound,kneeY=topY+(hoofY-topY)*.58; + const l1=cx-topW/2,l2=cx-kneeW/2+bend,l3=cx-hoofW/2+bend; + const r1=cx+topW/2,r2=cx+kneeW/2+bend,r3=cx+hoofW/2+bend; + const midTop=topY+(kneeY-topY)*.5,midHoof=kneeY+(hoofY-kneeY)*.5; + return `M ${n(l1)} ${n(topY)} C ${n(l1)} ${n(midTop)} ${n(l2)} ${n(kneeY-(kneeY-topY)*.3)} ${n(l2)} ${n(kneeY)}`+ + ` C ${n(l2)} ${n(midHoof)} ${n(l3)} ${n(hoofY-(hoofY-kneeY)*.3)} ${n(l3)} ${n(hoofY)}`+ + ` L ${n(r3)} ${n(hoofY)}`+ + ` C ${n(r3)} ${n(hoofY-(hoofY-kneeY)*.3)} ${n(r2)} ${n(midHoof)} ${n(r2)} ${n(kneeY)}`+ + ` C ${n(r2)} ${n(kneeY-(kneeY-topY)*.3)} ${n(r1)} ${n(midTop)} ${n(r1)} ${n(topY)} Z`; +} function coloringScene(id,content){ return `
-
-
+
+
+
+
diff --git a/public/js/widgets/learning.js b/public/js/widgets/learning.js index d9f9843..16ce7d6 100644 --- a/public/js/widgets/learning.js +++ b/public/js/widgets/learning.js @@ -669,6 +669,13 @@ function mountClock(root,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));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.clockLevel;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}; + /* het spel (zetten/aflezen/kiezen) verandert per opgave en hoort dus in de + vaste bovenbalk; het niveau staat meestal vast (en is bij beheerd niveau + zelfs uitgeschakeld) en gaat daarom net als de weergavestijl achter het + tandwiel */ + const gameField=field(T('clockGame'),game);gameField.classList.add('top-field'); + const levels=document.createElement('div');levels.className='lg-levels clock-levels'; + const levelButtons=[1,2,3].map(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('clockLevel'+value+'Hint');button.onclick=()=>chooseLevel(value);levels.append(button);return button}); const minuteValues=()=>assignedRules?.clockMinutes||(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; @@ -711,12 +718,12 @@ function mountClock(root,init){ 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); + 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';levelButtons.forEach((button,index)=>button.classList.toggle('active',S.level===index+1));top.append(gameField);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');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}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);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(S,{choices:S.choices.map(x=>({...x})),answerDigits:[...S.answerDigits]})}; + style.onchange=draw;game.onchange=()=>{syncSettings();newTask()};learningShell(root,[field(T('clockLevel'),levels),field(T('clockLearningStyle'),style),linkSetting,hour24Setting],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(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; @@ -725,17 +732,22 @@ function mountPlacevalue(root,init){ S.level=Math.max(1,Math.min(3,+S.level||1));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.placeLevel;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; + /* spel en getal kies je elke keer opnieuw, dus die staan in de vaste + bovenbalk; niveau staat meestal vast (en is bij beheerd niveau + uitgeschakeld) en gaat achter het tandwiel */ 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'}; + gameSetting.classList.add('top-field');numberSetting.classList.add('top-field'); + const levels=document.createElement('div');levels.className='lg-levels place-levels'; + const levelButtons=[1,2,3].map(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('placeLevel'+value+'Hint');button.onclick=()=>chooseLevel(value);levels.append(button);return button}); 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 drawExplore=()=>{S.number=Math.max(0,Math.min(999999,+input.value||0));const digits=String(S.number).padStart(6,'0').split('');stage.className='place-app';stage.innerHTML='';levelButtons.forEach((button,index)=>button.classList.toggle('active',S.level===index+1));const top=document.createElement('div');top.className='place-top';top.append(gameSetting,numberSetting,diceBtn);stage.append(top);const grid=document.createElement('div');grid.className='place-grid';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);grid.append(d)});stage.append(grid)}; 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 chooseLevel=value=>{S.level=value;S.score=0;S.streak=0;if(S.game==='build')newTask();else drawExplore()}; 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(); @@ -755,10 +767,8 @@ function mountPlacevalue(root,init){ }; 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); + levelButtons.forEach((button,index)=>button.classList.toggle('active',S.level===index+1)); + top.append(gameSetting); 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'); @@ -767,8 +777,8 @@ function mountPlacevalue(root,init){ if(S.feedback)feedback.textContent=T(S.feedback==='good'?'placeCorrect':S.feedback==='incomplete'?'placeIncomplete':'placeTryAgain');if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}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);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings(); + input.oninput=drawExplore;game.onchange=()=>{S.game=game.value;if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild()}; + learningShell(root,[field(T('placeLevel'),levels)],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel); if(S.game!=='build')drawExplore();else if(needsNewTask())newTask();else drawBuild(); return{getState:()=>persistedLearningState(S,{answerDigits:S.answerDigits?[...S.answerDigits]:null})}; } @@ -778,7 +788,13 @@ function mountMoney(root,init){ S.level=Math.max(1,Math.min(3,+S.level||1));const assignedRules=mathTaskRules(S.yearGroup,S.level);if(S.managedLearningLevel&&assignedRules)S.level=assignedRules.moneyLevel;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 styleSetting=field(T('moneyLearningStyle'),style),gameSetting=field(T('moneyGame'),game),categorySetting=field(T('moneyCategory'),category),syncSettings=()=>{styleSetting.hidden=S.game==='count'}; + /* spel (en bij het wisselgeld-spel: categorie) kies je elke keer opnieuw en + staat dus in de vaste bovenbalk; niveau staat meestal vast (en is bij + beheerd niveau uitgeschakeld) en gaat achter het tandwiel */ + gameSetting.classList.add('top-field');categorySetting.classList.add('top-field'); + const levels=document.createElement('div');levels.className='lg-levels money-levels'; + const levelButtons=[1,2,3].map(value=>{const button=document.createElement('button');button.type='button';button.textContent=T('lvl'+value);button.title=T('moneyLevel'+value+'Hint');button.onclick=()=>chooseLevel(value);levels.append(button);return button}); 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']; @@ -809,13 +825,13 @@ function mountMoney(root,init){ 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 draw=()=>{S.style=style.value;S.game=game.value;stage.className='money-app';stage.innerHTML='';const top=document.createElement('div');top.className='money-top';levelButtons.forEach((button,index)=>button.classList.toggle('active',S.level===index+1));top.append(gameSetting);if(S.game==='change')top.append(categorySetting);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():'';if(S.feedback==='good'&¤tUser&¤tUser.avatar){const cheer=renderAvatarFace(currentUser.avatar,'md');cheer.classList.add('av-cheer');feedback.append(cheer)}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);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(S,{selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})}; + style.onchange=draw;game.onchange=()=>{S.game=game.value;syncSettings();newTask()};category.onchange=()=>{S.category=category.value;newTask()};learningShell(root,[field(T('moneyLevel'),levels),styleSetting],stage);root.classList.toggle('managed-learning-level',!!S.managedLearningLevel);syncSettings();newTask();return{getState:()=>persistedLearningState(S,{selected:[...S.selected],given:[...S.given],answerDigits:[...S.answerDigits]})}; } function mountWordtypes(root,init){ diff --git a/public/js/widgets/letters.js b/public/js/widgets/letters.js index 3139cf0..a396ce8 100644 --- a/public/js/widgets/letters.js +++ b/public/js/widgets/letters.js @@ -21,9 +21,6 @@ function mountLetterGame(root, initState, opts){ root.innerHTML = `
-
- -
@@ -86,14 +83,17 @@ function mountLetterGame(root, initState, opts){ instrEl = el(".lg-instr"), nextBtn = el(".lg-next"), doneEl = el(".lg-done"), countEl = el(".lg-count"), starsEl = el(".lg-stars"), cardsEl = el(".lg-cards"); - /* zeldzaam gebruikte besturing (geluid, invoerwijze, aantal kaarten) achter - een tandwiel: die concurreerden eerder met de niveauknoppen om ruimte in - één brede rij, terwijl ze maar zelden worden aangepast */ + /* de kaartjes (het woord/thema) moeten snel te kiezen zijn - dat verandert + per opdracht. Niveau staat meestal vast (en is bij beheerd niveau zelfs + uitgeschakeld), dus dat gaat samen met geluid/invoerwijze/aantal achter + het tandwiel in plaats van in de brede rij te concurreren om ruimte. */ + const levels=document.createElement("div");levels.className="lg-levels"; + levels.innerHTML=``; const numSel=document.createElement("select");numSel.className="lg-num"; const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd"; const modeBtn=document.createElement("button");modeBtn.type="button";modeBtn.className="lg-mode"; el(".lg-top").insertAdjacentElement("beforebegin",widgetGear([ - gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn),gearField(T("numWords"),numSel) + gearField(T("levelWidget"),levels),gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn),gearField(T("numWords"),numSel) ])); /* card buttons (built dynamically: N cards + star + edit) */ function buildNumSel(){ diff --git a/public/js/widgets/math.js b/public/js/widgets/math.js index adb255d..a4f85d8 100644 --- a/public/js/widgets/math.js +++ b/public/js/widgets/math.js @@ -21,9 +21,6 @@ function mountMath(root, initState, mode, opts){ root.innerHTML = `
-
- -
@@ -54,9 +51,16 @@ function mountMath(root, initState, mode, opts){ fbEl.appendChild(cheer); } } + /* niveau staat meestal vast (en is bij beheerd niveau uitgeschakeld), dus + dat gaat net als geluid/invoerwijze achter het tandwiel in plaats van + permanent ruimte te vragen naast de opgave zelf */ + const levels=document.createElement("div");levels.className="lg-levels"; + levels.innerHTML=``; const sndBtn=document.createElement("button");sndBtn.type="button";sndBtn.className="lg-snd"; const modeBtn=document.createElement("button");modeBtn.type="button";modeBtn.className="lg-mode"; - el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn)])); + el(".mt-top").insertAdjacentElement("beforebegin",widgetGear([ + gearField(T("levelWidget"),levels),gearField(T("sndWidget"),sndBtn),gearField(T("amode"),modeBtn) + ])); const mtEl = el(".mt"), qEl = el(".mt-q"), aEl = el(".mt-a"), fbEl = el(".mt-fb"), scoreEl = el(".mt-score"), doneEl = el(".mt-done"), padEl = el(".mt-pad"), slotsEl = el(".mt-slots"), trayEl = el(".mt-tray"); diff --git a/public/js/widgets/world.js b/public/js/widgets/world.js index 07b6d56..973f3e2 100644 --- a/public/js/widgets/world.js +++ b/public/js/widgets/world.js @@ -521,8 +521,24 @@ function mountWorldQuiz(root,init,config){ const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]); const exercise=worldSelect(S.exercise,exerciseOptions); const stage=document.createElement("div"); + /* activiteit en oefening kies je elke keer opnieuw, dus die staan in de + vaste bovenbalk; niveau staat meestal vast en gaat achter het tandwiel */ + const activitySetting=field(T("worldActivity"),activity); const exerciseSetting=field(T("worldExercise"),exercise); - const settings=[field(T("worldActivity"),activity),exerciseSetting]; + activitySetting.classList.add("top-field"); + exerciseSetting.classList.add("top-field"); + const levels=document.createElement("div"); + levels.className="lg-levels world-levels"; + const levelButtons=[1,2,3].map(value=>{ + const button=document.createElement("button"); + button.type="button"; + button.textContent=T("lvl"+value); + button.title=T("worldLevel"+value+"Hint"); + button.onclick=()=>chooseLevel(value); + levels.append(button); + return button; + }); + const settings=[field(T("worldLevel"),levels)]; let view,viewSetting; if(config.type==="trees"){ view=worldSelect(S.view,[["tree","worldTreeWhole"],["leaf","worldTreeLeaf"],["mixed","worldTreeMixed"]]); @@ -587,29 +603,14 @@ function mountWorldQuiz(root,init,config){ S.activity=activity.value; S.exercise=exercise.value; if(view)S.view=view.value; - exerciseSetting.hidden=S.activity!=="quiz"; if(viewSetting)viewSetting.hidden=S.activity==="quiz"&&!["recognize","photo"].includes(S.exercise); stage.className="world-app"; stage.innerHTML=""; const top=document.createElement("div"); top.className="world-top"; - const levelControl=document.createElement("div"); - levelControl.className="world-level-control"; - const levelLabel=document.createElement("span"); - levelLabel.textContent=T("worldLevel"); - const levels=document.createElement("div"); - levels.className="lg-levels world-levels"; - [1,2,3].forEach(value=>{ - const button=document.createElement("button"); - button.type="button"; - button.textContent=T("lvl"+value); - button.title=T("worldLevel"+value+"Hint"); - button.classList.toggle("active",S.level===value); - button.onclick=()=>chooseLevel(value); - levels.append(button); - }); - levelControl.append(levelLabel,levels); - top.append(levelControl); + levelButtons.forEach((button,index)=>button.classList.toggle("active",S.level===index+1)); + top.append(activitySetting); + if(S.activity==="quiz")top.append(exerciseSetting); const title=document.createElement("strong"); title.textContent=T("wg_"+config.type); const score=document.createElement("span"); @@ -950,6 +951,24 @@ function mountTopography(root,init){ const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"],["flash","worldFlash"],["spell","worldSpell"]]); const mode=worldSelect(S.mode,modeOptions); const stage=document.createElement("div"); + /* activiteit en werkgebied kies je elke keer opnieuw, dus die staan in de + vaste bovenbalk; niveau staat meestal vast en gaat achter het tandwiel */ + const activitySetting=field(T("worldActivity"),activity); + const modeSetting=field(T("topoMode"),mode); + activitySetting.classList.add("top-field"); + modeSetting.classList.add("top-field"); + const levels=document.createElement("div"); + levels.className="lg-levels world-levels"; + const levelButtons=[1,2,3].map(value=>{ + const button=document.createElement("button"); + button.type="button"; + button.textContent=T("lvl"+value); + button.title=T("worldLevel"+value+"Hint"); + button.onclick=()=>chooseLevel(value); + levels.append(button); + return button; + }); + const levelField=field(T("worldLevel"),levels); const pool=()=>{ const items=S.mode==="provinces"?NL_PROVINCES :S.mode==="capitals"?NL_CITIES.filter(item=>item.capital) @@ -992,25 +1011,9 @@ function mountTopography(root,init){ stage.innerHTML=""; const top=document.createElement("div"); top.className="world-top"; - if(S.mode!=="regions"){ - const levelControl=document.createElement("div"); - levelControl.className="world-level-control"; - const levelLabel=document.createElement("span"); - levelLabel.textContent=T("worldLevel"); - const levels=document.createElement("div"); - levels.className="lg-levels world-levels"; - [1,2,3].forEach(value=>{ - const button=document.createElement("button"); - button.type="button"; - button.textContent=T("lvl"+value); - button.title=T("worldLevel"+value+"Hint"); - button.classList.toggle("active",S.level===value); - button.onclick=()=>chooseLevel(value); - levels.append(button); - }); - levelControl.append(levelLabel,levels); - top.append(levelControl); - } + levelField.hidden=S.mode==="regions"; + levelButtons.forEach((button,index)=>button.classList.toggle("active",S.level===index+1)); + top.append(activitySetting,modeSetting); const title=document.createElement("strong"); title.textContent=T("wg_topography"); const score=document.createElement("span"); @@ -1085,10 +1088,7 @@ function mountTopography(root,init){ const isTaskActivity=()=>["quiz","flash","spell"].includes(S.activity); activity.onchange=()=>{S.activity=activity.value;if(isTaskActivity())newTask();else draw()}; mode.onchange=()=>{S.mode=mode.value;if(isTaskActivity())newTask();else draw()}; - learningShell(root,[ - field(T("worldActivity"),activity), - field(T("topoMode"),mode), - ],stage); + learningShell(root,[levelField],stage); if(isTaskActivity())newTask();else draw(); return{getState:()=>({...S})}; } diff --git a/test/widgets.test.js b/test/widgets.test.js index 6ab7c4f..28268a9 100644 --- a/test/widgets.test.js +++ b/test/widgets.test.js @@ -164,7 +164,7 @@ test('analoge oefenklok is bedienbaar en houdt opdrachtacties buiten instellinge const [source,css,core]=await Promise.all([readFile('public/js/widgets/learning.js','utf8'),readFile('public/css/teach.css','utf8'),readFile('public/js/core.js','utf8')]); 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\('clockLearningStyle'\),style\),field\(T\('clockGame'\),game\),linkSetting,hour24Setting\],stage\)/); + assert.match(source,/learningShell\(root,\[field\(T\('clockLevel'\),levels\),field\(T\('clockLearningStyle'\),style\),linkSetting,hour24Setting\],stage\)/); const clockSource=source.slice(source.indexOf('function mountClock'),source.indexOf('function mountPlacevalue')); assert.doesNotMatch(clockSource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|clockHour|clockMinute)/); for(const feature of ['answerDigits','clock-answer-slots','clock-block-tray','dragClone','droptarget',"'0123456789'.split('')",'clock-keypad',"S.amode==='blocks'?'🧱':'⌨️'"])assert.ok(clockSource.includes(feature),feature); @@ -227,11 +227,11 @@ test('geldrekenen biedt drie niveaus, leerwijzen, spellen en directe bediening', assert.ok(moneySource.includes("denominations=()=>levelRule().denominations")); assert.ok(moneySource.includes("S.game==='change'")); for(const feature of ['money-wallet','money-cash-tray','money-answer-slots','money-digit-tray','money-keypad','dragClone','droptarget','getBoundingClientRect','amountWords','money-piece-image','money-product-icon','money-product-name','money-level-hint'])assert.ok(moneySource.includes(feature),feature); - assert.ok(moneySource.includes("learningShell(root,[styleSetting,gameSetting,categorySetting],stage)"),'alleen voorkeuren staan onder instellingen'); + assert.ok(moneySource.includes("learningShell(root,[field(T('moneyLevel'),levels),styleSetting],stage)"),'spel en categorie kies je elke keer opnieuw en staan dus zichtbaar; niveau staat achter instellingen'); assert.ok(moneySource.includes("style.onchange=draw")); assert.ok(moneySource.includes("game.onchange=()=>{S.game=game.value;syncSettings();newTask()}")); assert.ok(moneySource.includes("category.onchange=()=>{S.category=category.value;newTask()}")); - assert.ok(moneySource.includes("categorySetting.hidden=S.game!=='change'")); + assert.ok(moneySource.includes("if(S.game==='change')top.append(categorySetting)")); assert.doesNotMatch(moneySource,/learningShell\(root,\[[^\]]*(?:numberlineCheck|numberlineNew|moneyClear|moneyNextTask)/); for(const key of ['moneyLearningStyle','moneyStyleAmount','moneyStyleWords','moneyStyleBoth','moneyGame','moneyGameMake','moneyGameCount','moneyGameChange','moneyLevel','moneyLevel1Hint','moneyLevel2Hint','moneyLevel3Hint','moneyScore','moneyMakeTitle','moneyCountTitle','moneyChangeTitle','moneyWallet','moneyTray','moneyBuildHint','moneyTypeHint','moneyCorrect','moneyNextTask','moneyRemove','moneyCategory','moneyCategoryAll','moneyCategoryFood','moneyCategorySchool','moneyCategoryPlay'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key); assert.match(css,/money-app\{[^}]*align-self:flex-start[^}]*justify-content:flex-start/);