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

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-65 into main AGit
7 changed files with 373 additions and 66 deletions
Showing only changes of commit 93c287979c - Show all commits

View file

@ -1 +1 @@
0.3.70-beta
0.3.71-beta

View file

@ -1597,9 +1597,15 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.world-visual{width:min(100%,460px);display:flex;align-items:flex-start;justify-content:center;gap:12px}.world-visual.mixed{width:min(100%,620px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.world-photo{position:relative;min-width:0;margin:0;overflow:hidden;border:1px solid color-mix(in srgb,var(--line) 75%,var(--ink));border-radius:19px;background:#152432;box-shadow:0 10px 24px rgba(20,36,52,.18)}.world-picture{display:block;width:100%;height:auto;aspect-ratio:4/3;object-fit:cover}.world-photo-badge{position:absolute;left:9px;top:9px;padding:5px 9px;border:1px solid rgba(255,255,255,.35);border-radius:999px;background:rgba(10,27,42,.78);color:#fff;font-size:10px;font-weight:950;letter-spacing:.45px;text-transform:uppercase;box-shadow:0 2px 8px rgba(0,0,0,.18);backdrop-filter:blur(6px)}.world-photo-credit{position:absolute;right:7px;bottom:7px;max-width:calc(100% - 14px);overflow:hidden;padding:4px 7px;border-radius:8px;background:rgba(10,27,42,.8);color:#fff!important;font-size:9px;font-weight:750;line-height:1.2;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;backdrop-filter:blur(6px)}.world-photo-credit:hover,.world-photo-credit:focus-visible{text-decoration:underline;outline:2px solid #fff;outline-offset:1px}
.world-identity{display:flex;flex-direction:column;align-items:center;gap:2px;text-align:center}.world-identity h3{margin:0;font-size:18px;line-height:1.2}.world-identity em{color:var(--muted);font-size:12px;font-family:Georgia,serif;font-weight:650}
.world-answers{width:min(100%,560px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.world-answers .tbtn{min-width:0;min-height:48px;padding:8px 11px;border:1px solid var(--line);background:var(--surface);box-shadow:0 3px 8px rgba(20,36,52,.06)}.world-answers .tbtn:hover{border-color:var(--accent);background:var(--accent-soft)}.world-answers .tbtn.correct{background:var(--green);color:#fff;border-color:transparent}.world-feedback{min-height:24px;margin:0;text-align:center;font-weight:900}.world-feedback.ok{color:var(--green)}.world-feedback.bad{color:var(--red)}.world-actions{display:flex;justify-content:center}.world-fact{width:min(100%,560px);margin:0;padding:11px 14px;border:1px solid color-mix(in srgb,var(--green) 28%,var(--line));border-radius:15px;background:#eef7e9;color:#2f6f3e;text-align:center}.world-fact .world-identity em{color:#527c5b}.world-fact p{margin:6px 0 0;font-weight:800;line-height:1.4}
.world-answers .tbtn.wrong{background:color-mix(in srgb,var(--red) 14%,var(--surface));color:var(--red);border-color:var(--red)}.world-clue{width:min(100%,560px);padding:18px 20px;border:1px solid color-mix(in srgb,var(--accent) 35%,var(--line));border-radius:18px;background:linear-gradient(135deg,var(--accent-soft),var(--surface));color:var(--accent-ink);font-size:clamp(16px,3.2cqi,21px);font-weight:850;line-height:1.5;text-align:center}.world-clue-latin{font-family:Georgia,serif;font-size:clamp(23px,4.6cqi,34px);font-style:italic;font-weight:700}
.world-photo-answers{width:min(100%,640px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.world-photo-choice{position:relative;min-width:0;margin:0;overflow:hidden;border:3px solid transparent;border-radius:18px;background:#152432;box-shadow:0 8px 18px rgba(20,36,52,.16);transition:border-color .15s,transform .15s,box-shadow .15s}.world-image-button{display:block;width:100%;margin:0;padding:0;border:0;background:#152432;cursor:pointer}.world-image-button .world-picture{transition:transform .18s,filter .18s}.world-photo-choice:hover{transform:translateY(-2px);box-shadow:0 12px 22px rgba(20,36,52,.2)}.world-image-button:hover .world-picture{transform:scale(1.025)}.world-image-button:focus-visible{outline:4px solid var(--accent);outline-offset:-4px}.world-choice-number{position:absolute;left:8px;top:8px;display:grid;width:28px;height:28px;place-items:center;border:2px solid rgba(255,255,255,.7);border-radius:50%;background:rgba(10,27,42,.86);color:#fff;font-size:13px;font-weight:950;pointer-events:none}.world-photo-choice.correct{border-color:var(--green);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 25%,transparent)}.world-photo-choice.wrong{border-color:var(--red)}.world-photo-choice.wrong .world-picture{filter:saturate(.65) brightness(.82)}
.world-gallery{width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.world-specimen{min-width:0;display:flex;flex-direction:column;align-items:center;gap:9px;padding:11px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,var(--surface),var(--surface-2));box-shadow:0 8px 22px rgba(20,36,52,.08)}.world-specimen .world-visual{width:100%}.world-specimen p{margin:0;padding:0 5px 3px;color:var(--muted);font-size:12px;font-weight:720;line-height:1.4;text-align:center}
.topo-card{width:min(100%,740px)}.topo-map-wrap{width:100%;display:flex;flex-direction:column;align-items:center;gap:7px;overflow:hidden;padding:10px;border:1px solid var(--line);border-radius:20px;background:linear-gradient(155deg,#edf8fd,#dff2fb)}.topo-map{display:block;width:min(100%,510px);height:auto;max-height:590px;touch-action:manipulation;filter:drop-shadow(0 9px 13px rgba(29,60,76,.13))}.topo-water{fill:#dff3fb}.topo-province{stroke:#fff;stroke-width:4.5;stroke-linejoin:round;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s,fill .15s}.topo-province[role=button]{cursor:pointer}.topo-province[role=button]:hover,.topo-province[role=button]:focus{filter:brightness(.88) saturate(1.12);outline:none}.topo-province[role=button]:focus-visible{stroke:var(--navy);stroke-width:8}.topo-province.picked{fill:var(--red)!important}.topo-province.target{fill:var(--green)!important}.topo-label,.topo-city-label{fill:#18324e;paint-order:stroke;stroke:#fff;stroke-width:5px;stroke-linejoin:round;font-size:18px;font-weight:950;text-anchor:middle;pointer-events:none}.topo-city-label{text-anchor:start;font-size:16px}.topo-city-label.left{text-anchor:end}.topo-marker{cursor:pointer}.topo-hit{fill:transparent}.topo-dot{fill:var(--navy);stroke:#fff;stroke-width:4;transition:fill .15s,stroke-width .15s}.topo-marker:hover .topo-dot,.topo-marker:focus .topo-dot{fill:var(--orange);outline:none}.topo-marker:focus-visible .topo-dot{stroke:var(--navy);stroke-width:8}.topo-marker.picked .topo-dot{fill:var(--red)}.topo-marker.target .topo-dot{fill:var(--green);stroke-width:7}.topo-source{align-self:flex-end;color:#3b657b;font-size:9px;font-weight:750;text-decoration:none}.topo-source:hover,.topo-source:focus-visible{text-decoration:underline}
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{fill:#f7fbff;stroke:#18324e}body.dark .topo-source{color:#a9cadb}
.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}
.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}
.clock-app{width:100%;min-height:100%;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)}
.clock-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.clock-level-control{display:flex;align-items:center;gap:6px}.clock-level-control>span{color:var(--muted);font-size:12px;font-weight:900}.clock-levels button{min-width:31px;padding:6px 9px;white-space:nowrap}.clock-mode{width:36px;padding:0;font-size:18px}.clock-score{margin-left:auto;padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900}
@ -1746,8 +1752,15 @@ body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .world-fact .wo
.world-visual.mixed{gap:6px}
.world-photo-badge{left:6px;top:6px;padding:4px 7px;font-size:8px}
.world-photo-credit{right:5px;bottom:5px;max-width:calc(100% - 10px);font-size:8px}
.world-photo-answers{gap:6px}
.world-clue{padding:13px 12px}
.topo-map-wrap{padding:6px}
.topo-map{max-height:450px}
.world-map-wrap{padding:4px}
.world-map{max-height:none}
.world-continent-label{font-size:12px}
.topo-world-legend{gap:4px}
.topo-world-legend span{padding:4px 6px;font-size:10px}
.money-top,.clock-top{top:-10px;gap:5px}
.money-level-control>span,.clock-level-control>span{display:none}
.money-shop-task{grid-template-columns:minmax(0,.8fr) minmax(0,1.2fr);gap:7px}

View file

@ -54,3 +54,15 @@ provinciecontouren worden onder dezelfde licentie verspreid.
De plaatsing van stadspunten is daarnaast gecontroleerd met
[Map provinces Netherlands-en.svg](https://commons.wikimedia.org/wiki/File:Map_provinces_Netherlands-en.svg)
door Alphathon ([CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)).
`world-continents.svg` is gegenereerd uit de fysieke
[Natural Earth 1:110m Geography Regions Polygons](https://www.naturalearthdata.com/downloads/110m-physical-vectors/110m-physical-labels/)
en, voor de eilanden van Oceanië,
[Admin 0 Countries](https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/).
Natural Earth stelt deze geografische brondata beschikbaar als publiek domein.
De lokale afgeleide bevat zeven afzonderlijk bedienbare werelddeelcontouren.
De vier Nederlandse landsdelen volgen de
[CBS-indeling voor Gebieden in Nederland 2026](https://www.cbs.nl/nl-nl/cijfers/detail/86247NED).
De selectie en loop van de grote rivieren zijn gecontroleerd aan de hand van het
[Rijkswaterstaat-overzicht Rivieren](https://www.rijkswaterstaat.nl/water/waterbeheer/beheer-en-ontwikkeling-rijkswateren/rivieren).

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 154 KiB

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.70-beta";
const VERSION = "0.3.71-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -119,15 +119,18 @@ const I18N = {
wg_clock:"Oefenklok", wg_clock_d:"Leer kloktijden op vier niveaus met meerdere leerwijzen en spellen.",
wg_placevalue:"Plaatswaardetabel", wg_placevalue_d:"Ontdek de waarde van cijfers tot honderdduizend.",
wg_money:"Geldrekenen", wg_money_d:"Oefen bedragen maken, geld tellen en wisselgeld op drie niveaus.",
wg_trees:"Bomen & bladeren", wg_trees_d:"Herken acht Nederlandse boomsoorten op echte foto's van bomen en bladeren.",
wg_birds:"Vogels herkennen", wg_birds_d:"Herken acht veelvoorkomende Nederlandse vogels op echte natuurfoto's.",
wg_topography:"Topografie Nederland", wg_topography_d:"Oefen provincies, provinciehoofdsteden en belangrijke steden op een nauwkeurige kaart.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
wg_trees:"Bomen & bladeren", wg_trees_d:"Leer acht Nederlandse boomsoorten met vijf foto-, kenmerk- en koppeloefeningen.",
wg_birds:"Vogels herkennen", wg_birds_d:"Leer acht Nederlandse vogels met vier foto-, kenmerk- en naamoefeningen.",
wg_topography:"Topografie Nederland", wg_topography_d:"Oefen provincies, plaatsen, landsdelen, rivieren en werelddelen op nauwkeurige kaarten.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldExercise:"Oefening", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
worldExerciseRecognize:"Herken de soort", worldExercisePair:"Koppel boom en blad", worldExerciseClue:"Raad met kenmerken", worldExercisePhoto:"Kies de juiste foto", worldExerciseLatin:"Wetenschappelijke naam",
worldScore:"Score: {score} · reeks: {streak}", worldExploreHint:"Bekijk de kenmerken en vergelijk de soorten.", worldQuestionTree:"Welke boom zie je?", worldQuestionLeaf:"Bij welke boom hoort dit blad?", worldQuestionBird:"Welke vogel zie je?",
worldQuestionPair:"Bij welke boom horen deze boom en dit blad?", worldQuestionClue:"Welke soort hoort bij deze kenmerken?", worldQuestionLatin:"Welke soort hoort bij deze wetenschappelijke naam?", worldFindTreePhoto:"Tik op de foto van de {name}.", worldFindBirdPhoto:"Tik op de foto van de {name}.",
worldCorrect:"Goed herkend! 🌟", worldTryAgain:"Kijk nog eens goed naar de kenmerken.", worldNext:"Volgende opdracht",
worldPhotoTree:"Boom", worldPhotoLeaf:"Blad of naalden", worldPhotoBird:"Vogel", worldPhotoCredit:"Foto: {author} · {license}",
topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoMapLabel:"Kaart van Nederland",
topoExploreHint:"Bekijk de echte provinciegrenzen en vergelijk de ligging van de plaatsen.", topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?", topoMapSource:"Kaart: Gust Justice · CC BY-SA 4.0",
worldPhotoTree:"Boom", worldPhotoLeaf:"Blad of naalden", worldPhotoBird:"Vogel", worldPhotoCredit:"Foto: {author} · {license}", worldPhotoOption:"Afbeelding {number}",
topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoRegions:"Landsdelen", topoRivers:"Rivieren", topoContinents:"Werelddelen", topoMapLabel:"Kaart van Nederland", topoWorldMapLabel:"Kaart van de wereld",
topoExploreHint:"Bekijk de echte provinciegrenzen en vergelijk de ligging van de plaatsen.", topoExploreRegions:"Bekijk welke provincies samen Noord-, Oost-, West- en Zuid-Nederland vormen.", topoExploreRivers:"Volg de loop van de grote Nederlandse rivieren van grens tot delta.", topoExploreContinents:"Vergelijk de vorm en ligging van de zeven werelddelen.",
topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?", topoFindRegion:"Tik op {name}.", topoFindRiver:"Tik op de rivier {name}.", topoFindContinent:"Tik op het werelddeel {name}.", topoMapSource:"Kaart: Gust Justice · CC BY-SA 4.0", topoWorldMapSource:"Wereldkaart: Natural Earth · publiek domein",
moneyLearningStyle:"Opdracht tonen", moneyStyleAmount:"Als bedrag", moneyStyleWords:"In woorden", moneyStyleBoth:"Bedrag en woorden",
moneyGame:"Spel", moneyGameMake:"Bedrag maken", moneyGameCount:"Geld tellen", moneyGameChange:"Wisselgeld",
moneyCategory:"Koopcategorie", moneyCategoryAll:"Alle producten", moneyCategoryFood:"Eten en drinken", moneyCategorySchool:"Schoolspullen", moneyCategoryPlay:"Spelen en sport",
@ -387,15 +390,18 @@ const I18N = {
wg_clock:"Practice clock", wg_clock_d:"Learn time at four levels with multiple learning styles and games.",
wg_placevalue:"Place value", wg_placevalue_d:"Explore digit values up to hundred thousands.",
wg_money:"Money", wg_money_d:"Practise making amounts, counting money and change at three levels.",
wg_trees:"Trees & leaves", wg_trees_d:"Recognise eight Dutch tree species from real photographs of trees and leaves.",
wg_birds:"Bird identification", wg_birds_d:"Recognise eight common Dutch birds from real nature photographs.",
wg_topography:"Map of the Netherlands", wg_topography_d:"Practise provinces, provincial capitals and important cities on an accurate map.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
wg_trees:"Trees & leaves", wg_trees_d:"Learn eight Dutch tree species with five photo, feature and matching exercises.",
wg_birds:"Bird identification", wg_birds_d:"Learn eight Dutch birds with four photo, feature and naming exercises.",
wg_topography:"Map of the Netherlands", wg_topography_d:"Practise provinces, places, regions, rivers and continents on accurate maps.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldExercise:"Exercise", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
worldExerciseRecognize:"Recognise the species", worldExercisePair:"Match tree and leaf", worldExerciseClue:"Guess from features", worldExercisePhoto:"Choose the correct photo", worldExerciseLatin:"Scientific name",
worldScore:"Score: {score} · streak: {streak}", worldExploreHint:"Study the features and compare the species.", worldQuestionTree:"Which tree do you see?", worldQuestionLeaf:"Which tree does this leaf belong to?", worldQuestionBird:"Which bird do you see?",
worldQuestionPair:"Which species do this tree and leaf belong to?", worldQuestionClue:"Which species matches these features?", worldQuestionLatin:"Which species matches this scientific name?", worldFindTreePhoto:"Tap the photograph of the {name}.", worldFindBirdPhoto:"Tap the photograph of the {name}.",
worldCorrect:"Well recognised! 🌟", worldTryAgain:"Take another close look at the features.", worldNext:"Next task",
worldPhotoTree:"Tree", worldPhotoLeaf:"Leaf or needles", worldPhotoBird:"Bird", worldPhotoCredit:"Photo: {author} · {license}",
topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoMapLabel:"Map of the Netherlands",
topoExploreHint:"Study the real provincial boundaries and compare the locations of the places.", topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?", topoMapSource:"Map: Gust Justice · CC BY-SA 4.0",
worldPhotoTree:"Tree", worldPhotoLeaf:"Leaf or needles", worldPhotoBird:"Bird", worldPhotoCredit:"Photo: {author} · {license}", worldPhotoOption:"Image {number}",
topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoRegions:"Regions", topoRivers:"Rivers", topoContinents:"Continents", topoMapLabel:"Map of the Netherlands", topoWorldMapLabel:"Map of the world",
topoExploreHint:"Study the real provincial boundaries and compare the locations of the places.", topoExploreRegions:"Study which provinces make up the northern, eastern, western and southern Netherlands.", topoExploreRivers:"Follow the major Dutch rivers from the border to the delta.", topoExploreContinents:"Compare the shapes and locations of the seven continents.",
topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?", topoFindRegion:"Tap {name}.", topoFindRiver:"Tap the river {name}.", topoFindContinent:"Tap the continent {name}.", topoMapSource:"Map: Gust Justice · CC BY-SA 4.0", topoWorldMapSource:"World map: Natural Earth · public domain",
moneyLearningStyle:"Show task as", moneyStyleAmount:"As an amount", moneyStyleWords:"In words", moneyStyleBoth:"Amount and words",
moneyGame:"Game", moneyGameMake:"Make an amount", moneyGameCount:"Count money", moneyGameChange:"Change",
moneyCategory:"Shopping category", moneyCategoryAll:"All products", moneyCategoryFood:"Food and drinks", moneyCategorySchool:"School supplies", moneyCategoryPlay:"Play and sports",

View file

@ -106,6 +106,31 @@ const NL_CITIES=Object.freeze([
{id:"tilburg",nl:"Tilburg",en:"Tilburg",province:"noord_brabant",capital:false,tier:3,x:347,y:625},
]);
const NL_REGIONS=Object.freeze([
{id:"north",nl:"Noord-Nederland",en:"Northern Netherlands",tier:1,color:"#60a5fa",provinces:["friesland","groningen","drenthe"],x:565,y:172},
{id:"east",nl:"Oost-Nederland",en:"Eastern Netherlands",tier:1,color:"#a78bfa",provinces:["overijssel","gelderland","flevoland"],x:548,y:406},
{id:"west",nl:"West-Nederland",en:"Western Netherlands",tier:1,color:"#22d3ee",provinces:["noord_holland","zuid_holland","utrecht","zeeland"],x:252,y:454},
{id:"south",nl:"Zuid-Nederland",en:"Southern Netherlands",tier:1,color:"#fbbf24",provinces:["noord_brabant","limburg"],x:417,y:648},
]);
const NL_RIVERS=Object.freeze([
{id:"rijn",nl:"Rijn",en:"Rhine",tier:1,color:"#2563eb",labelX:399,labelY:477,path:"M 567 510 C 535 508 515 493 489 491 C 452 488 429 472 394 474 C 353 476 322 494 280 502 C 257 507 238 508 218 514"},
{id:"maas",nl:"Maas",en:"Meuse",tier:1,color:"#0891b2",labelX:401,labelY:581,path:"M 463 850 C 445 817 469 773 490 730 C 511 688 536 639 519 596 C 504 565 472 556 439 570 C 398 588 362 574 320 580 C 280 586 247 578 211 566"},
{id:"ijssel",nl:"IJssel",en:"IJssel",tier:2,color:"#0ea5e9",labelX:536,labelY:389,path:"M 497 489 C 516 459 525 426 526 390 C 527 356 520 329 502 306 C 484 282 463 270 447 250"},
{id:"waal",nl:"Waal",en:"Waal",tier:2,color:"#1d4ed8",labelX:421,labelY:548,path:"M 557 519 C 527 530 503 539 470 548 C 435 558 406 562 373 553 C 340 544 310 548 280 558 C 253 566 230 560 210 550"},
{id:"schelde",nl:"Schelde",en:"Scheldt",tier:3,color:"#0284c7",labelX:139,labelY:641,path:"M 41 660 C 82 654 119 649 151 642 C 176 637 191 624 207 609"},
]);
const WORLD_CONTINENTS=Object.freeze([
{id:"europe",shape:"europe",nl:"Europa",en:"Europe",tier:1,color:"#fbbf24",x:382,y:96},
{id:"africa",shape:"africa",nl:"Afrika",en:"Africa",tier:1,color:"#fb923c",x:383,y:190},
{id:"asia",shape:"asia",nl:"Azië",en:"Asia",tier:1,color:"#f87171",x:505,y:112},
{id:"north_america",shape:"north-america",nl:"Noord-Amerika",en:"North America",tier:2,color:"#60a5fa",x:154,y:112},
{id:"south_america",shape:"south-america",nl:"Zuid-Amerika",en:"South America",tier:2,color:"#34d399",x:245,y:252},
{id:"oceania",shape:"oceania",nl:"Oceanië",en:"Oceania",tier:2,color:"#c084fc",x:593,y:248},
{id:"antarctica",shape:"antarctica",nl:"Antarctica",en:"Antarctica",tier:3,color:"#cbd5e1",x:360,y:337},
]);
const worldName=item=>LANG==="nl"?item.nl:item.en;
const worldHint=item=>LANG==="nl"?item.hintNl:item.hintEn;
const worldShuffle=items=>{
@ -157,6 +182,37 @@ function worldVisual(item,type,view){
return visual;
}
function worldPhotoOption(item,type,view,index,onChoose,status){
const kind=type==="birds"?"bird":view;
const media=WORLD_MEDIA[item.id]?.[kind];
const figure=document.createElement("figure");
figure.className="world-photo-choice"+(status?" "+status:"");
const button=document.createElement("button");
button.type="button";
button.className="world-image-button";
button.setAttribute("aria-label",T("worldPhotoOption").replace("{number}",index+1));
const image=document.createElement("img");
image.className="world-picture";
image.alt="";
image.draggable=false;
image.loading="lazy";
image.decoding="async";
image.src=WORLD_MEDIA_ROOT+media.file;
const number=document.createElement("span");
number.className="world-choice-number";
number.textContent=String(index+1);
button.append(image,number);
button.onclick=onChoose;
const credit=document.createElement("a");
credit.className="world-photo-credit";
credit.href=media.source;
credit.target="_blank";
credit.rel="noopener noreferrer";
credit.textContent=T("worldPhotoCredit").replace("{author}",media.author).replace("{license}",media.license);
figure.append(button,credit);
return figure;
}
function worldIdentity(item){
const identity=document.createElement("div");
identity.className="world-identity";
@ -169,19 +225,26 @@ function worldIdentity(item){
}
function mountWorldQuiz(root,init,config){
const exerciseOptions=config.type==="trees"
?[["recognize","worldExerciseRecognize"],["pair","worldExercisePair"],["clue","worldExerciseClue"],["photo","worldExercisePhoto"],["latin","worldExerciseLatin"]]
:[["recognize","worldExerciseRecognize"],["clue","worldExerciseClue"],["photo","worldExercisePhoto"],["latin","worldExerciseLatin"]];
const S=Object.assign({
activity:"quiz",level:1,view:config.type==="trees"?"mixed":"bird",
score:0,streak:0,target:"",taskView:"",options:[],solved:false,feedback:"",
activity:"quiz",exercise:"recognize",level:1,view:config.type==="trees"?"mixed":"bird",
score:0,streak:0,target:"",taskView:"",options:[],picked:"",solved:false,feedback:"",
},init||{});
S.level=Math.max(1,Math.min(3,+S.level||1));
if(!exerciseOptions.some(([id])=>id===S.exercise))S.exercise="recognize";
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
const exercise=worldSelect(S.exercise,exerciseOptions);
const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
const stage=document.createElement("div");
const settings=[field(T("worldActivity"),activity),field(T("worldLevel"),level)];
let view;
const exerciseSetting=field(T("worldExercise"),exercise);
const settings=[field(T("worldActivity"),activity),exerciseSetting,field(T("worldLevel"),level)];
let view,viewSetting;
if(config.type==="trees"){
view=worldSelect(S.view,[["tree","worldTreeWhole"],["leaf","worldTreeLeaf"],["mixed","worldTreeMixed"]]);
settings.push(field(T("worldTreeView"),view));
viewSetting=field(T("worldTreeView"),view);
settings.push(viewSetting);
}
const pool=()=>worldLevelPool(config.items,S.level);
const current=()=>config.items.find(item=>item.id===S.target)||pool()[0];
@ -190,14 +253,18 @@ function mountWorldQuiz(root,init,config){
let target=items[Math.floor(Math.random()*items.length)];
if(items.length>1)while(target.id===previous)target=items[Math.floor(Math.random()*items.length)];
S.target=target.id;
S.taskView=config.type==="trees"?(S.view==="mixed"?(Math.random()<.5?"tree":"leaf"):S.view):"bird";
S.taskView=config.type==="trees"
?S.exercise==="pair"?"mixed":S.view==="mixed"?(Math.random()<.5?"tree":"leaf"):S.view
:"bird";
S.options=worldShuffle([target,...worldShuffle(items.filter(item=>item.id!==target.id)).slice(0,3)]).map(item=>item.id);
S.picked="";
S.solved=false;
S.feedback="";
draw();
};
const answer=id=>{
if(S.solved)return;
S.picked=id;
if(id===S.target){
S.solved=true;S.feedback="good";S.score++;S.streak++;sndGood();
}else{
@ -221,8 +288,11 @@ function mountWorldQuiz(root,init,config){
};
function draw(){
S.activity=activity.value;
S.exercise=exercise.value;
S.level=+level.value;
if(view)S.view=view.value;
exerciseSetting.hidden=S.activity==="learn";
if(viewSetting)viewSetting.hidden=S.activity==="quiz"&&!["recognize","photo"].includes(S.exercise);
stage.className="world-app";
stage.innerHTML="";
const top=document.createElement("div");
@ -243,18 +313,49 @@ function mountWorldQuiz(root,init,config){
const item=current(),card=document.createElement("section");
card.className="world-task-card";
const question=document.createElement("h3");
question.textContent=T(config.type==="birds"?"worldQuestionBird":S.taskView==="leaf"?"worldQuestionLeaf":"worldQuestionTree");
card.append(question,worldVisual(item,config.type,S.taskView));
const answers=document.createElement("div");
answers.className="world-answers";
S.options.forEach(id=>{
const option=config.items.find(entry=>entry.id===id);
if(!option)return;
const button=btn(worldName(option),()=>answer(id));
if(S.solved&&id===S.target)button.classList.add("correct");
answers.append(button);
});
card.append(answers);
if(S.exercise==="photo"){
question.textContent=T(config.type==="birds"?"worldFindBirdPhoto":"worldFindTreePhoto").replace("{name}",worldName(item));
card.append(question);
const answers=document.createElement("div");
answers.className="world-photo-answers";
S.options.forEach((id,index)=>{
const option=config.items.find(entry=>entry.id===id);
if(!option)return;
const status=S.solved&&id===S.target?"correct":S.feedback==="bad"&&id===S.picked?"wrong":"";
const optionView=config.type==="trees"?(S.taskView==="mixed"?"tree":S.taskView):"bird";
answers.append(worldPhotoOption(option,config.type,optionView,index,()=>answer(id),status));
});
card.append(answers);
}else{
const prompt=document.createElement("div");
if(S.exercise==="clue"){
question.textContent=T("worldQuestionClue");
prompt.className="world-clue";
prompt.textContent=worldHint(item);
}else if(S.exercise==="latin"){
question.textContent=T("worldQuestionLatin");
prompt.className="world-clue world-clue-latin";
prompt.textContent=item.latin;
}else if(S.exercise==="pair"){
question.textContent=T("worldQuestionPair");
prompt.append(worldVisual(item,config.type,"mixed"));
}else{
question.textContent=T(config.type==="birds"?"worldQuestionBird":S.taskView==="leaf"?"worldQuestionLeaf":"worldQuestionTree");
prompt.append(worldVisual(item,config.type,S.taskView));
}
card.append(question,prompt);
const answers=document.createElement("div");
answers.className="world-answers";
S.options.forEach(id=>{
const option=config.items.find(entry=>entry.id===id);
if(!option)return;
const button=btn(worldName(option),()=>answer(id));
if(S.solved&&id===S.target)button.classList.add("correct");
else if(S.feedback==="bad"&&id===S.picked)button.classList.add("wrong");
answers.append(button);
});
card.append(answers);
}
const feedback=document.createElement("p");
feedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
feedback.setAttribute("role","status");
@ -276,6 +377,7 @@ function mountWorldQuiz(root,init,config){
stage.append(card);
}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==="quiz")newTask();else draw()};
exercise.onchange=()=>{S.exercise=exercise.value;if(S.activity==="quiz")newTask();else draw()};
level.onchange=()=>{S.level=+level.value;if(S.activity==="quiz")newTask();else draw()};
if(view)view.onchange=()=>{S.view=view.value;if(S.activity==="quiz")newTask();else draw()};
learningShell(root,settings,stage);
@ -289,6 +391,8 @@ function mountBirds(root,init){return mountWorldQuiz(root,init,{type:"birds",ite
const SVG_NS="http://www.w3.org/2000/svg";
const TOPO_MAP_ASSET=WORLD_MEDIA_ROOT+"netherlands-provinces.svg";
const TOPO_MAP_SOURCE="https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg";
const TOPO_WORLD_ASSET=WORLD_MEDIA_ROOT+"world-continents.svg";
const TOPO_WORLD_SOURCE="https://www.naturalearthdata.com/downloads/";
function svgNode(name,attributes={}){
const node=document.createElementNS(SVG_NS,name);
Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));
@ -315,25 +419,71 @@ function makeTopoMap(mode,pool,onPick,showLabels,picked,target){
role:onPick?"group":"img","aria-label":T("topoMapLabel"),
});
svg.append(svgNode("rect",{x:0,y:0,width:743,height:881,rx:34,class:"topo-water"}));
NL_PROVINCES.forEach(province=>{
const shape=svgNode("use",{
href:TOPO_MAP_ASSET+"#"+province.shape,
fill:province.color,
class:"topo-province"+(province.id===target?" target":"")+(province.id===picked?" picked":""),
"data-id":province.id,
const active=new Set(pool.map(item=>item.id));
if(mode==="regions"){
NL_REGIONS.forEach(region=>{
const group=svgNode("g",{
class:"topo-region"+(region.id===target?" target":"")+(region.id===picked?" picked":""),
"data-id":region.id,
});
region.provinces.forEach(provinceId=>{
const province=NL_PROVINCES.find(item=>item.id===provinceId);
if(province)group.append(svgNode("use",{href:TOPO_MAP_ASSET+"#"+province.shape,fill:region.color,class:"topo-province"}));
});
const title=svgNode("title");
title.textContent=worldName(region);
group.append(title);
if(onPick&&active.has(region.id))svgAction(group,worldName(region),()=>onPick(region.id));
svg.append(group);
if(showLabels&&active.has(region.id)){
const label=svgNode("text",{x:region.x,y:region.y,class:"topo-label topo-region-label"});
label.textContent=worldName(region);
svg.append(label);
}
});
}else{
NL_PROVINCES.forEach(province=>{
const selectable=mode==="provinces"&&active.has(province.id);
const shape=svgNode("use",{
href:TOPO_MAP_ASSET+"#"+province.shape,
fill:mode==="rivers"?"#dce8df":province.color,
class:"topo-province"+(province.id===target?" target":"")+(province.id===picked?" picked":"")+(mode==="provinces"&&!selectable?" inactive":""),
"data-id":province.id,
});
const title=svgNode("title");
title.textContent=worldName(province);
shape.append(title);
if(onPick&&selectable)svgAction(shape,worldName(province),()=>onPick(province.id));
svg.append(shape);
if(showLabels&&mode==="provinces"&&selectable){
const label=svgNode("text",{x:province.x,y:province.y,class:"topo-label"});
label.textContent=worldName(province);
svg.append(label);
}
});
}
if(mode==="rivers")NL_RIVERS.forEach(river=>{
const selectable=active.has(river.id);
const group=svgNode("g",{
class:"topo-river"+(river.id===target?" target":"")+(river.id===picked?" picked":"")+(!selectable?" inactive":""),
"data-id":river.id,style:"--river-color:"+river.color,
});
group.append(
svgNode("path",{d:river.path,class:"topo-river-hit"}),
svgNode("path",{d:river.path,class:"topo-river-line"}),
);
const title=svgNode("title");
title.textContent=worldName(province);
shape.append(title);
if(onPick&&mode==="provinces")svgAction(shape,worldName(province),()=>onPick(province.id));
svg.append(shape);
if(showLabels&&mode==="provinces"&&pool.some(item=>item.id===province.id)){
const label=svgNode("text",{x:province.x,y:province.y,class:"topo-label"});
label.textContent=worldName(province);
title.textContent=worldName(river);
group.append(title);
if(onPick&&selectable)svgAction(group,worldName(river),()=>onPick(river.id));
svg.append(group);
if(showLabels&&selectable){
const label=svgNode("text",{x:river.labelX,y:river.labelY,class:"topo-river-label"});
label.textContent=worldName(river);
svg.append(label);
}
});
if(mode!=="provinces")pool.forEach(city=>{
if(mode==="capitals"||mode==="cities")pool.forEach(city=>{
const group=svgNode("g",{
class:"topo-marker"+(city.id===target?" target":"")+(city.id===picked?" picked":""),
});
@ -362,21 +512,82 @@ function makeTopoMap(mode,pool,onPick,showLabels,picked,target){
return wrap;
}
function makeWorldMap(pool,onPick,showLabels,picked,target){
const wrap=document.createElement("div");
wrap.className="topo-map-wrap world-map-wrap";
const svg=svgNode("svg",{
class:"topo-map world-map",viewBox:"0 0 720 360",
role:onPick?"group":"img","aria-label":T("topoWorldMapLabel"),
});
svg.append(svgNode("rect",{x:0,y:0,width:720,height:360,rx:24,class:"topo-water"}));
const active=new Set(pool.map(item=>item.id));
WORLD_CONTINENTS.forEach(continent=>{
const selectable=active.has(continent.id);
const shape=svgNode("use",{
href:TOPO_WORLD_ASSET+"#"+continent.shape,
fill:continent.color,
class:"world-continent"+(continent.id===target?" target":"")+(continent.id===picked?" picked":"")+(!selectable?" inactive":""),
"data-id":continent.id,
});
const title=svgNode("title");
title.textContent=worldName(continent);
shape.append(title);
if(onPick&&selectable)svgAction(shape,worldName(continent),()=>onPick(continent.id));
svg.append(shape);
if(showLabels&&selectable){
const label=svgNode("text",{x:continent.x,y:continent.y,class:"world-continent-label"});
label.textContent=worldName(continent);
svg.append(label);
}
});
const source=document.createElement("a");
source.className="topo-source";
source.href=TOPO_WORLD_SOURCE;
source.target="_blank";
source.rel="noopener noreferrer";
source.textContent=T("topoWorldMapSource");
wrap.append(svg);
if(showLabels){
const legend=document.createElement("div");
legend.className="topo-world-legend";
pool.forEach(continent=>{
const entry=document.createElement("span");
const swatch=document.createElement("i");
swatch.style.background=continent.color;
entry.append(swatch,worldName(continent));
legend.append(entry);
});
wrap.append(legend);
}
wrap.append(source);
return wrap;
}
function mountTopography(root,init){
const modeOptions=[
["provinces","topoProvinces"],["capitals","topoCapitals"],["cities","topoCities"],
["regions","topoRegions"],["rivers","topoRivers"],["continents","topoContinents"],
];
const S=Object.assign({
activity:"quiz",mode:"provinces",level:1,score:0,streak:0,
target:"",picked:"",solved:false,feedback:"",
},init||{});
S.level=Math.max(1,Math.min(3,+S.level||1));
if(!modeOptions.some(([id])=>id===S.mode))S.mode="provinces";
const activity=worldSelect(S.activity,[["learn","worldLearn"],["quiz","worldQuiz"]]);
const mode=worldSelect(S.mode,[["provinces","topoProvinces"],["capitals","topoCapitals"],["cities","topoCities"]]);
const mode=worldSelect(S.mode,modeOptions);
const level=worldSelect(String(S.level),[["1","lvl1"],["2","lvl2"],["3","lvl3"]]);
const stage=document.createElement("div");
const pool=()=>S.mode==="provinces"
?NL_PROVINCES.filter(item=>item.tier<=S.level)
:S.mode==="capitals"
?NL_CITIES.filter(item=>item.capital&&item.tier<=S.level)
:NL_CITIES.filter(item=>!item.capital&&item.tier<=S.level);
const levelSetting=field(T("worldLevel"),level);
const pool=()=>{
const items=S.mode==="provinces"?NL_PROVINCES
:S.mode==="capitals"?NL_CITIES.filter(item=>item.capital)
:S.mode==="cities"?NL_CITIES.filter(item=>!item.capital)
:S.mode==="regions"?NL_REGIONS
:S.mode==="rivers"?NL_RIVERS
:WORLD_CONTINENTS;
return items.filter(item=>item.tier<=S.level);
};
const current=()=>pool().find(item=>item.id===S.target)||pool()[0];
const newTask=()=>{
const items=pool(),previous=S.target;
@ -397,6 +608,7 @@ function mountTopography(root,init){
};
function draw(){
S.activity=activity.value;S.mode=mode.value;S.level=+level.value;
levelSetting.hidden=S.mode==="regions";
stage.className="world-app topo-app";
stage.innerHTML="";
const top=document.createElement("div");
@ -409,15 +621,24 @@ function mountTopography(root,init){
stage.append(top);
const card=document.createElement("section");
card.className="world-task-card topo-card";
const renderMap=(interactive,labels,pickedId,targetId)=>S.mode==="continents"
?makeWorldMap(pool(),interactive,labels,pickedId,targetId)
:makeTopoMap(S.mode,pool(),interactive,labels,pickedId,targetId);
if(S.activity==="learn"){
const intro=document.createElement("p");
intro.className="world-intro";
intro.textContent=T("topoExploreHint");
card.append(intro,makeTopoMap(S.mode,pool(),null,true,"",""));
intro.textContent=T(S.mode==="regions"?"topoExploreRegions":S.mode==="rivers"?"topoExploreRivers":S.mode==="continents"?"topoExploreContinents":"topoExploreHint");
card.append(intro,renderMap(null,true,"",""));
}else{
const item=current(),question=document.createElement("h3");
question.textContent=T(S.mode==="provinces"?"topoFindProvince":S.mode==="capitals"?"topoFindCapital":"topoFindCity").replace("{name}",worldName(item));
card.append(question,makeTopoMap(S.mode,pool(),answer,S.solved,S.picked,S.solved?S.target:""));
const questionKey=S.mode==="provinces"?"topoFindProvince"
:S.mode==="capitals"?"topoFindCapital"
:S.mode==="cities"?"topoFindCity"
:S.mode==="regions"?"topoFindRegion"
:S.mode==="rivers"?"topoFindRiver"
:"topoFindContinent";
question.textContent=T(questionKey).replace("{name}",worldName(item));
card.append(question,renderMap(answer,S.solved,S.picked,S.solved?S.target:""));
const feedback=document.createElement("p");
feedback.className="world-feedback "+(S.feedback==="good"?"ok":S.feedback?"bad":"");
feedback.setAttribute("role","status");
@ -435,7 +656,7 @@ function mountTopography(root,init){
learningShell(root,[
field(T("worldActivity"),activity),
field(T("topoMode"),mode),
field(T("worldLevel"),level),
levelSetting,
],stage);
if(S.activity==="quiz")newTask();else draw();
return{getState:()=>({...S})};

View file

@ -66,28 +66,34 @@ test('wereldoriëntatiewidgets laden, registreren en vertalen', async()=>{
assert.equal((core.match(new RegExp('wg_'+id+'_d:','g'))||[]).length,2,id);
}
assert.equal((core.match(/catWorld:/g)||[]).length,2);
for(const key of ['worldActivity','worldLearn','worldQuiz','worldTreeView','worldQuestionLeaf','worldQuestionBird','worldPhotoTree','worldPhotoLeaf','worldPhotoBird','worldPhotoCredit','topoProvinces','topoCapitals','topoCities','topoFindProvince','topoFindCapital','topoFindCity','topoMapSource'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
for(const key of ['worldActivity','worldLearn','worldQuiz','worldExercise','worldExerciseRecognize','worldExercisePair','worldExerciseClue','worldExercisePhoto','worldExerciseLatin','worldTreeView','worldQuestionLeaf','worldQuestionBird','worldQuestionPair','worldQuestionClue','worldQuestionLatin','worldFindTreePhoto','worldFindBirdPhoto','worldPhotoTree','worldPhotoLeaf','worldPhotoBird','worldPhotoCredit','worldPhotoOption','topoProvinces','topoCapitals','topoCities','topoRegions','topoRivers','topoContinents','topoFindProvince','topoFindCapital','topoFindCity','topoFindRegion','topoFindRiver','topoFindContinent','topoMapSource','topoWorldMapSource'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
for(const exercise of ['recognize','pair','clue','photo','latin'])assert.ok(source.includes('"'+exercise+'"'),exercise);
for(const mode of ['regions','rivers','continents'])assert.ok(source.includes('["'+mode+'","topo'),mode);
assert.match(source,/getState/);
assert.ok(source.includes('name.textContent=worldName(item)'));
assert.ok(source.includes('hint.textContent=worldHint(item)'));
assert.doesNotMatch(source,/innerHTML\s*=\s*(?:worldName|worldHint)\s*\(/);
});
test('wereldoriëntatiedata bevat echte soortmedia en volledige Nederlandse topografie', async()=>{
const [source,map,attribution]=await Promise.all([
test('wereldoriëntatiedata bevat echte soortmedia en uitgebreide topografie', async()=>{
const [source,map,worldMap,attribution]=await Promise.all([
readFile('public/js/widgets/world.js','utf8'),
readFile('public/img/world/netherlands-provinces.svg','utf8'),
readFile('public/img/world/world-continents.svg','utf8'),
readFile('public/img/world/ATTRIBUTION.md','utf8'),
]);
const context={};
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,media:WORLD_MEDIA,provinces:NL_PROVINCES,cities:NL_CITIES}',context);
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,media:WORLD_MEDIA,provinces:NL_PROVINCES,cities:NL_CITIES,regions:NL_REGIONS,rivers:NL_RIVERS,continents:WORLD_CONTINENTS}',context);
const data=JSON.parse(JSON.stringify(context.__world));
assert.equal(data.trees.length,8);
assert.equal(data.birds.length,8);
assert.equal(data.provinces.length,12);
assert.equal(data.cities.filter(city=>city.capital).length,12);
assert.equal(data.cities.filter(city=>!city.capital).length,8);
for(const collection of [data.trees,data.birds,data.provinces,data.cities])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length);
assert.equal(data.regions.length,4);
assert.equal(data.rivers.length,5);
assert.equal(data.continents.length,7);
for(const collection of [data.trees,data.birds,data.provinces,data.cities,data.regions,data.rivers,data.continents])assert.equal(new Set(collection.map(item=>item.id)).size,collection.length);
for(const item of [...data.trees,...data.birds])assert.ok(item.nl&&item.en&&item.latin&&item.hintNl&&item.hintEn&&item.tier>=1&&item.tier<=3,item.id);
const assetPaths=[];
for(const tree of data.trees){
@ -113,10 +119,25 @@ test('wereldoriëntatiedata bevat echte soortmedia en volledige Nederlandse topo
});
for(const province of data.provinces){
assert.ok(province.shape&&Number.isFinite(province.x)&&Number.isFinite(province.y)&&cities.has(province.capital),province.id);
assert.match(map,new RegExp('<path id="'+province.shape+'"[^>]+d="[^"]+"'));
assert.match(map,new RegExp('<path id="'+province.shape+'"[^>]+d="[^"]+"'));
}
assert.equal((map.match(/<path id=/g)||[]).length,12);
const regionProvinces=data.regions.flatMap(region=>region.provinces);
assert.equal(regionProvinces.length,12);
assert.equal(new Set(regionProvinces).size,12);
assert.deepEqual([...regionProvinces].sort(),data.provinces.map(province=>province.id).sort());
for(const river of data.rivers)assert.ok(/^M [\d ]+/.test(river.path)&&river.color&&Number.isFinite(river.labelX)&&Number.isFinite(river.labelY),river.id);
for(const continent of data.continents){
assert.ok(continent.shape&&continent.color&&Number.isFinite(continent.x)&&Number.isFinite(continent.y),continent.id);
assert.match(worldMap,new RegExp('<g id="'+continent.shape+'">'));
}
assert.equal((worldMap.match(/<g id=/g)||[]).length,7);
assert.ok(Buffer.byteLength(worldMap)<200_000,'de wereldkaart moet compact genoeg blijven voor mobiel gebruik');
assert.ok(attribution.includes('Natural Earth 1:110m Geography Regions Polygons'));
assert.ok(attribution.includes('CBS-indeling voor Gebieden in Nederland 2026'));
assert.ok(attribution.includes('Rijkswaterstaat-overzicht Rivieren'));
assert.match(source,/href:TOPO_MAP_ASSET\+"#"\+province\.shape/);
assert.match(source,/href:TOPO_WORLD_ASSET\+"#"\+continent\.shape/);
});
test('widgetbibliotheek bevat zoeken favorieten sjablonen en borddata-acties', async()=>{
@ -338,7 +359,10 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
'.money-task-card,.clock-task-card,.world-task-card',
'.world-gallery{grid-template-columns:1fr}',
'.world-photo-credit',
'.world-photo-answers{gap:6px}',
'.topo-map-wrap{padding:6px}',
'.world-map-wrap{padding:4px}',
'.topo-world-legend{gap:4px}',
]){
assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels');
}