Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
7 changed files with 230 additions and 8 deletions
Showing only changes of commit 88d1f9cb86 - Show all commits

View file

@ -1 +1 @@
0.3.68-beta
0.3.69-beta

View file

@ -1590,6 +1590,16 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.schedule{width:min(100%,450px);display:flex;flex-direction:column;gap:7px}.schedule>div{display:flex;align-items:center;gap:12px;padding:9px;background:var(--surface-2);border-radius:10px}.schedule time{font-weight:900;color:var(--blue)}.schedule span{flex:1;font-weight:800}.schedule .tbtn{height:30px;min-height:30px;padding:0 9px}.mood{display:flex;gap:8px;flex-wrap:wrap}.mood .tbtn{font-size:36px;height:auto;padding:12px}.mood span{display:block;font-size:14px}.poll{width:min(100%,440px);display:flex;flex-direction:column;gap:8px}.poll h3{text-align:center}.poll .tbtn{position:relative;overflow:hidden;text-align:left}.poll .tbtn i{position:absolute;inset:0 auto 0 0;background:var(--accent-soft);z-index:0}.poll .tbtn span{position:relative;float:right;font-weight:900}.poll .tbtn{isolation:isolate}
@media(max-width:600px){.gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.gcard{min-height:120px;padding:13px 8px}.gcard .gd{display:none}.widget .wdup,.widget .wpreview{display:none}.edu-stage{padding:10px}.place-grid b{font-size:9px}.place-grid output{padding:12px 2px}}
/* Wereldoriëntatie: natuurherkenning en topografie */
.world-app{width:100%;min-height:100%;align-self:flex-start;display:flex;flex-direction:column;justify-content:flex-start;color:var(--ink)}
.world-top{position:sticky;top:-16px;z-index:6;width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;padding:8px 4px;background:var(--surface);border-bottom:1px solid var(--line)}.world-top>strong{font-size:15px}.world-top>span{padding:6px 10px;border-radius:999px;background:var(--surface-2);color:var(--muted);font-size:12px;font-weight:900;white-space:nowrap}
.world-task-card{width:min(100%,650px);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px solid var(--line);border-radius:20px;background:var(--surface);box-shadow:var(--shadow-1)}.world-task-card h3{margin:0;text-align:center;font-size:clamp(19px,4cqi,26px)}.world-intro{width:100%;margin:0;padding:9px 12px;border-radius:12px;background:var(--accent-soft);color:var(--accent-ink);font-weight:800;text-align:center}
.world-visual{width:min(100%,330px);display:flex;align-items:center;justify-content:center;gap:10px}.world-visual.mixed{width:min(100%,500px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.world-picture{display:block;width:100%;max-height:230px;object-fit:contain;border-radius:16px;border:1px solid var(--line);background:var(--surface-2)}
.world-answers{width:min(100%,520px);display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.world-answers .tbtn{min-width:0;min-height:46px;padding:7px 10px}.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{max-width:520px;margin:0;padding:9px 12px;border-radius:12px;background:#eef7e9;color:#2f6f3e;font-weight:800;text-align:center}
.world-gallery{width:100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.world-specimen{min-width:0;display:flex;flex-direction:column;align-items:center;gap:7px;padding:10px;border:1px solid var(--line);border-radius:16px;background:var(--surface)}.world-specimen .world-visual{width:100%}.world-specimen .world-picture{max-height:160px}.world-specimen h3{margin:0;font-size:17px;text-align:center}.world-specimen p{margin:0;color:var(--muted);font-size:12px;font-weight:700;line-height:1.35;text-align:center}
.topo-card{width:min(100%,700px)}.topo-map-wrap{width:100%;display:flex;justify-content:center;overflow:auto}.topo-map{display:block;width:min(100%,430px);height:auto;max-height:500px;touch-action:manipulation}.topo-water{fill:#e4f4fb}.topo-province{stroke:var(--surface);stroke-width:3;vector-effect:non-scaling-stroke;transition:filter .15s,opacity .15s}.topo-province[role=button]{cursor:pointer}.topo-province[role=button]:hover,.topo-province[role=button]:focus{filter:brightness(.88);outline:none}.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:3px;stroke-linejoin:round;font-size:10px;font-weight:950;text-anchor:middle;pointer-events:none}.topo-city-label{text-anchor:start;font-size:9px}.topo-marker{cursor:pointer}.topo-hit{fill:transparent}.topo-dot{fill:var(--navy);stroke:#fff;stroke-width:2}.topo-marker:hover .topo-dot,.topo-marker:focus .topo-dot{fill:var(--orange);outline:none}.topo-marker.picked .topo-dot{fill:var(--red)}.topo-marker.target .topo-dot{fill:var(--green);r:9}
body.dark .world-fact{background:#1d3b2a;color:#a9e5b7}body.dark .topo-water{fill:#173145}body.dark .topo-label,body.dark .topo-city-label{fill:#f7fbff;stroke:#18324e}
.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}
.clock-task-card{width:min(100%,620px);margin:0 auto;display:flex;flex-direction:column;align-items:center;gap:12px;padding:16px;border:1px solid var(--line);border-radius:20px;background:var(--surface);box-shadow:var(--shadow-1)}
@ -1729,7 +1739,13 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
.mood .tbtn{font-size:30px;padding:9px}
.poll h3{margin:8px 0}
.money-task-card,.clock-task-card{gap:9px;padding:10px}
.money-task-card,.clock-task-card,.world-task-card{gap:9px;padding:10px}
.world-top{top:-10px;gap:5px}
.world-gallery{grid-template-columns:1fr}
.world-picture{max-height:190px}
.world-specimen .world-picture{max-height:150px}
.topo-map{max-height:450px}
.topo-label,.topo-city-label{font-size:8px}
.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}
@ -1760,6 +1776,8 @@ body.dark .bd-stage{background:radial-gradient(circle at 50% 20%,var(--surface-2
grid-template-columns:repeat(5,34px);gap:4px;padding:7px}
.clock-block-tray .clock-drag-block{width:34px;height:52px;min-height:52px}
.clock-type-display,.money-type-display{min-width:0;width:100%;font-size:30px}
.world-answers{grid-template-columns:1fr}
.world-visual.mixed{gap:5px}
.schedule>div{flex-wrap:wrap}
.schedule span{flex-basis:calc(100% - 70px)}
.sb-add input{flex-basis:100%}

View file

@ -284,6 +284,7 @@
<script src="js/widgets/numberline.js"></script>
<script src="js/widgets/sentence.js"></script>
<script src="js/widgets/learning.js"></script>
<script src="js/widgets/world.js"></script>
<script src="js/board.js"></script>
<script src="js/pupil.js"></script>
<script src="js/parent.js"></script>

View file

@ -46,6 +46,9 @@ const REGISTRY = [
{ id:"placevalue", cat:"rekenen", icon:"🔢", w:610, h:390, minW:390, minH:280, mount:mountPlacevalue },
{ id:"money", cat:"rekenen", icon:"🪙", w:580, h:560, minW:380, minH:360, mount:mountMoney },
{ id:"wordtypes", cat:"taal", icon:"🎨", w:600, h:410, minW:380, minH:290, mount:mountWordtypes },
{ id:"trees", cat:"world", icon:"🌳", w:620, h:600, minW:360, minH:400, mount:mountTrees },
{ id:"birds", cat:"world", icon:"🐦", w:620, h:600, minW:360, minH:400, mount:mountBirds },
{ id:"topography", cat:"world", icon:"🗺️", w:680, h:640, minW:380, minH:440, mount:mountTopography },
{ id:"write", cat:"tools", icon:"✏️", w:560, h:440, minW:360, minH:280, mount:mountWrite },
{ id:"mind", cat:"tools", icon:"🕸️", w:640, h:480, minW:420, minH:320, mount:mountMind },
{ id:"notes", cat:"tools", icon:"📝", w:340, h:300, minW:240, minH:180, mount:mountNotes },
@ -58,7 +61,7 @@ const REGISTRY = [
{ id:"poll", cat:"class", icon:"📊", w:540, h:500, minW:360, minH:330, mount:mountPoll },
{ id:"media", cat:"media", icon:"🎬", w:560, h:420, minW:320, minH:240, mount:mountMedia }
];
const WIDGET_CATS = [["favorites","galleryFavorites"],["taal","catTaal"],["rekenen","catRekenen"],["class","catClass"],["tools","catTools"],["media","catMedia"]];
const WIDGET_CATS = [["favorites","galleryFavorites"],["taal","catTaal"],["rekenen","catRekenen"],["world","catWorld"],["class","catClass"],["tools","catTools"],["media","catMedia"]];
/* houdt een (opgeslagen of standaard) widgetgrootte binnen het bord. Op een
telefoon is de beschikbare ruimte leidend: een desktop-minimum van bv.

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.65-beta";
const VERSION = "0.3.69-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -60,7 +60,7 @@ const I18N = {
anchorLibEmptyGrid:"Zet eerst woorden in het diagram.",
numWords:"Woorden per spel", all:"Alle", noThemes:"— nog geen thema's —",
muteAll:"Alle geluiden aan/uit", sndWidget:"Geluid van deze widget aan/uit",
catTaal:"Taal", catRekenen:"Rekenen", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
catTaal:"Taal", catRekenen:"Rekenen", catWorld:"Wereldoriëntatie", catClass:"Feest & klas", catTools:"Hulpmiddelen", catMedia:"Media",
wg_madd:"Plussommen", wg_madd_d:"Optellen tot 10, 20 of 100.",
wg_msub:"Minsommen", wg_msub_d:"Aftrekken tot 10, 20 of 100.",
wg_mmul:"Keersommen", wg_mmul_d:"De tafels van 1 t/m 12 oefenen.",
@ -119,6 +119,14 @@ 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:"Leer acht Nederlandse bomen herkennen aan hun vorm en bladeren.",
wg_birds:"Vogels herkennen", wg_birds_d:"Leer acht veelvoorkomende Nederlandse vogels herkennen aan kleur en vorm.",
wg_topography:"Topografie Nederland", wg_topography_d:"Oefen provincies, provinciehoofdsteden en belangrijke steden op de kaart.",
worldActivity:"Werkvorm", worldLearn:"Leren", worldQuiz:"Quiz", worldLevel:"Niveau", worldTreeView:"Wat zie je?", worldTreeWhole:"Hele boom", worldTreeLeaf:"Blad", worldTreeMixed:"Boom en blad",
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?",
worldCorrect:"Goed herkend! 🌟", worldTryAgain:"Kijk nog eens goed naar de kenmerken.", worldNext:"Volgende opdracht",
topoMode:"Oefenen met", topoProvinces:"Provincies", topoCapitals:"Provinciehoofdsteden", topoCities:"Belangrijke steden", topoMapLabel:"Kaart van Nederland",
topoExploreHint:"Bekijk waar de namen op de kaart liggen.", topoFindProvince:"Tik op de provincie {name}.", topoFindCapital:"Waar ligt provinciehoofdstad {name}?", topoFindCity:"Waar ligt {name}?",
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",
@ -319,7 +327,7 @@ const I18N = {
anchorLibEmptyGrid:"Put words in the diagram first.",
numWords:"Words per game", all:"All", noThemes:"— no themes yet —",
muteAll:"All sounds on/off", sndWidget:"This widget's sound on/off",
catTaal:"Language", catRekenen:"Maths", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",
catTaal:"Language", catRekenen:"Maths", catWorld:"World studies", catClass:"Celebration & class", catTools:"Tools", catMedia:"Media",
wg_madd:"Addition", wg_madd_d:"Adding up to 10, 20 or 100.",
wg_msub:"Subtraction", wg_msub_d:"Subtracting up to 10, 20 or 100.",
wg_mmul:"Multiplication", wg_mmul_d:"Practise the times tables 112.",
@ -378,6 +386,14 @@ 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:"Learn to recognise eight common trees by their shape and leaves.",
wg_birds:"Bird identification", wg_birds_d:"Learn to recognise eight common Dutch birds by colour and shape.",
wg_topography:"Map of the Netherlands", wg_topography_d:"Practise provinces, provincial capitals and important cities on the map.",
worldActivity:"Activity", worldLearn:"Learn", worldQuiz:"Quiz", worldLevel:"Level", worldTreeView:"What do you see?", worldTreeWhole:"Whole tree", worldTreeLeaf:"Leaf", worldTreeMixed:"Tree and leaf",
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?",
worldCorrect:"Well recognised! 🌟", worldTryAgain:"Take another close look at the features.", worldNext:"Next task",
topoMode:"Practise", topoProvinces:"Provinces", topoCapitals:"Provincial capitals", topoCities:"Important cities", topoMapLabel:"Map of the Netherlands",
topoExploreHint:"Study where the names belong on the map.", topoFindProvince:"Tap the province {name}.", topoFindCapital:"Where is the provincial capital {name}?", topoFindCity:"Where is {name}?",
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",
@ -1029,4 +1045,3 @@ function tone(freq, dur=0.12, delay=0, type="sine", vol=0.18){
const sndGood = ()=> tone(880,0.1);
const sndBad = ()=> tone(180,0.18,0,"square",0.10);
const sndWin = ()=>{ [523,659,784,1047].forEach((f,i)=>tone(f,0.16,i*0.12,"triangle",0.2)); };

143
public/js/widgets/world.js Normal file
View file

@ -0,0 +1,143 @@
/* Wereldoriëntatie: natuurherkenning en Nederlandse topografie. */
const WORLD_TREES=Object.freeze([
{id:'oak',nl:'Eik',en:'Oak',tier:1,hintNl:'Herkenbaar aan de ronde bladlobben en eikels.',hintEn:'Recognisable by rounded leaf lobes and acorns.'},
{id:'birch',nl:'Berk',en:'Birch',tier:1,hintNl:'Witte bast en driehoekige blaadjes met een gezaagde rand.',hintEn:'White bark and triangular leaves with a serrated edge.'},
{id:'horse_chestnut',nl:'Paardenkastanje',en:'Horse chestnut',tier:1,hintNl:'Vijf tot zeven grote blaadjes vormen samen één handvormig blad.',hintEn:'Five to seven large leaflets form one hand-shaped leaf.'},
{id:'pine',nl:'Grove den',en:'Scots pine',tier:1,hintNl:'Naalden staan per twee bij elkaar en de boom draagt kegels.',hintEn:'Needles grow in pairs and the tree carries cones.'},
{id:'beech',nl:'Beuk',en:'Beech',tier:2,hintNl:'Gladgrijze stam en ovale blaadjes met een golvende rand.',hintEn:'Smooth grey trunk and oval leaves with a wavy edge.'},
{id:'maple',nl:'Esdoorn',en:'Maple',tier:2,hintNl:'Puntig handvormig blad en gevleugelde zaden.',hintEn:'Pointed hand-shaped leaf and winged seeds.'},
{id:'willow',nl:'Wilg',en:'Willow',tier:3,hintNl:'Lange smalle blaadjes aan vaak hangende takken.',hintEn:'Long narrow leaves on often drooping branches.'},
{id:'alder',nl:'Els',en:'Alder',tier:3,hintNl:'Ronde blaadjes met een ingedeukte top en kleine houtige propjes.',hintEn:'Rounded leaves with a notched tip and small woody cones.'},
]);
const WORLD_BIRDS=Object.freeze([
{id:'robin',nl:'Roodborst',en:'European robin',tier:1,body:'#8a6a4b',head:'#786047',wing:'#6e543e',breast:'#ef6c32',bill:'#33261d',hintNl:'Oranjerode borst en gezicht, met een bruine rug.',hintEn:'Orange-red breast and face with a brown back.'},
{id:'great_tit',nl:'Koolmees',en:'Great tit',tier:1,body:'#f2cf3f',head:'#18222b',wing:'#4b6670',breast:'#f2cf3f',bill:'#222',hintNl:'Zwarte kop, witte wangen en een zwarte streep over de gele buik.',hintEn:'Black head, white cheeks and a black stripe down the yellow belly.'},
{id:'blackbird',nl:'Merel',en:'Common blackbird',tier:1,body:'#22262b',head:'#1b1e22',wing:'#30343a',breast:'#25292e',bill:'#f0a521',hintNl:'Het mannetje is zwart met een opvallend gele snavel.',hintEn:'The male is black with a striking yellow bill.'},
{id:'mallard',nl:'Wilde eend',en:'Mallard',tier:1,body:'#a8a9a2',head:'#14604e',wing:'#766953',breast:'#7b4832',bill:'#e2b43f',hintNl:'Het mannetje heeft een groene kop, witte halsring en bruine borst.',hintEn:'The male has a green head, white neck ring and brown breast.'},
{id:'blue_tit',nl:'Pimpelmees',en:'Blue tit',tier:2,body:'#f3d85a',head:'#3a78a7',wing:'#39759a',breast:'#f3d85a',bill:'#30343c',hintNl:'Blauw petje en vleugels, witte wangen en een gele buik.',hintEn:'Blue cap and wings, white cheeks and a yellow belly.'},
{id:'house_sparrow',nl:'Huismus',en:'House sparrow',tier:2,body:'#9a7654',head:'#80654e',wing:'#6e5038',breast:'#b79a7c',bill:'#403329',hintNl:'Bruin gestreepte rug; het mannetje heeft een zwarte bef.',hintEn:'Brown streaked back; the male has a black bib.'},
{id:'magpie',nl:'Ekster',en:'Eurasian magpie',tier:3,body:'#f4f5f5',head:'#182127',wing:'#172a33',breast:'#f4f5f5',bill:'#20262a',hintNl:'Zwart-wit verenkleed en een opvallend lange donkere staart.',hintEn:'Black-and-white plumage and a strikingly long dark tail.'},
{id:'woodpecker',nl:'Grote bonte specht',en:'Great spotted woodpecker',tier:3,body:'#f4f1e8',head:'#20262b',wing:'#1f252a',breast:'#eee9dc',bill:'#373a3b',hintNl:'Zwart-wit met een rode vlek onder de staart.',hintEn:'Black and white with a red patch beneath the tail.'},
]);
const NL_PROVINCES=Object.freeze([
{id:'friesland',nl:'Friesland',en:'Friesland',capital:'leeuwarden',tier:2,color:'#7bc8e8',d:'M90 85L95 55 145 38 205 45 235 75 220 115 175 135 125 115Z',x:158,y:82},
{id:'groningen',nl:'Groningen',en:'Groningen',capital:'groningen_city',tier:2,color:'#ef8d71',d:'M205 45L265 30 330 50 315 90 270 105 235 75Z',x:275,y:67},
{id:'drenthe',nl:'Drenthe',en:'Drenthe',capital:'assen',tier:3,color:'#e9c66b',d:'M220 115L270 105 310 125 300 180 245 190 220 160Z',x:266,y:145},
{id:'overijssel',nl:'Overijssel',en:'Overijssel',capital:'zwolle',tier:2,color:'#78c49b',d:'M245 190L300 180 330 210 310 260 260 270 225 235Z',x:282,y:222},
{id:'flevoland',nl:'Flevoland',en:'Flevoland',capital:'lelystad',tier:3,color:'#b5d969',d:'M150 170L215 160 245 190 225 235 175 245 145 210Z',x:195,y:202},
{id:'noord_holland',nl:'Noord-Holland',en:'North Holland',capital:'haarlem',tier:1,color:'#f3a9c6',d:'M90 85L125 115 150 170 145 210 165 245 145 285 105 270 85 205 75 145Z',x:112,y:198},
{id:'utrecht',nl:'Utrecht',en:'Utrecht',capital:'utrecht_city',tier:1,color:'#d0a6df',d:'M145 245L175 245 210 265 200 305 160 305 135 280Z',x:174,y:276},
{id:'gelderland',nl:'Gelderland',en:'Gelderland',capital:'arnhem',tier:1,color:'#f0aa62',d:'M210 265L260 270 310 260 315 310 275 345 205 335 200 305Z',x:258,y:302},
{id:'zuid_holland',nl:'Zuid-Holland',en:'South Holland',capital:'den_haag',tier:1,color:'#7fb2e6',d:'M105 270L145 285 160 305 205 335 175 355 115 345 75 315Z',x:132,y:318},
{id:'zeeland',nl:'Zeeland',en:'Zeeland',capital:'middelburg',tier:3,color:'#86cfbf',d:'M45 326L90 320 112 337 82 349 38 342ZM35 351L78 350 105 365 75 378 30 370ZM55 382L103 378 120 391 82 402 45 395Z',x:73,y:369},
{id:'noord_brabant',nl:'Noord-Brabant',en:'North Brabant',capital:'den_bosch',tier:1,color:'#e88c91',d:'M115 345L175 355 205 335 275 345 285 385 250 410 175 400 120 380Z',x:202,y:376},
{id:'limburg',nl:'Limburg',en:'Limburg',capital:'maastricht',tier:1,color:'#e6c85f',d:'M250 410L285 385 300 420 285 450 295 485 278 520 255 490 260 455 242 430Z',x:272,y:458},
]);
const NL_CITIES=Object.freeze([
{id:'leeuwarden',nl:'Leeuwarden',en:'Leeuwarden',province:'friesland',capital:true,tier:2,x:150,y:72},
{id:'groningen_city',nl:'Groningen',en:'Groningen',province:'groningen',capital:true,tier:2,x:281,y:66},
{id:'assen',nl:'Assen',en:'Assen',province:'drenthe',capital:true,tier:3,x:264,y:140},
{id:'zwolle',nl:'Zwolle',en:'Zwolle',province:'overijssel',capital:true,tier:2,x:266,y:218},
{id:'lelystad',nl:'Lelystad',en:'Lelystad',province:'flevoland',capital:true,tier:3,x:195,y:203},
{id:'haarlem',nl:'Haarlem',en:'Haarlem',province:'noord_holland',capital:true,tier:1,x:119,y:250},
{id:'utrecht_city',nl:'Utrecht',en:'Utrecht',province:'utrecht',capital:true,tier:1,x:173,y:278},
{id:'arnhem',nl:'Arnhem',en:'Arnhem',province:'gelderland',capital:true,tier:1,x:270,y:310},
{id:'den_haag',nl:'Den Haag',en:'The Hague',province:'zuid_holland',capital:true,tier:1,x:96,y:312},
{id:'middelburg',nl:'Middelburg',en:'Middelburg',province:'zeeland',capital:true,tier:3,x:69,y:374},
{id:'den_bosch',nl:"'s-Hertogenbosch",en:"'s-Hertogenbosch",province:'noord_brabant',capital:true,tier:1,x:207,y:369},
{id:'maastricht',nl:'Maastricht',en:'Maastricht',province:'limburg',capital:true,tier:1,x:275,y:493},
{id:'amsterdam',nl:'Amsterdam',en:'Amsterdam',province:'noord_holland',capital:false,tier:1,x:139,y:246},
{id:'rotterdam',nl:'Rotterdam',en:'Rotterdam',province:'zuid_holland',capital:false,tier:1,x:120,y:333},
{id:'eindhoven',nl:'Eindhoven',en:'Eindhoven',province:'noord_brabant',capital:false,tier:1,x:230,y:395},
{id:'nijmegen',nl:'Nijmegen',en:'Nijmegen',province:'gelderland',capital:false,tier:1,x:285,y:330},
{id:'enschede',nl:'Enschede',en:'Enschede',province:'overijssel',capital:false,tier:2,x:315,y:230},
{id:'alkmaar',nl:'Alkmaar',en:'Alkmaar',province:'noord_holland',capital:false,tier:2,x:105,y:185},
{id:'breda',nl:'Breda',en:'Breda',province:'noord_brabant',capital:false,tier:3,x:158,y:383},
{id:'tilburg',nl:'Tilburg',en:'Tilburg',province:'noord_brabant',capital:false,tier:3,x:193,y:391},
]);
const worldName=item=>LANG==='nl'?item.nl:item.en;
const worldHint=item=>LANG==='nl'?item.hintNl:item.hintEn;
const worldSvgUrl=svg=>'data:image/svg+xml;charset=utf-8,'+encodeURIComponent(svg);
const worldShuffle=items=>{const copy=[...items];for(let i=copy.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[copy[i],copy[j]]=[copy[j],copy[i]]}return copy};
const worldSelect=(value,items)=>{const select=document.createElement('select');items.forEach(([id,key])=>select.add(new Option(T(key),id)));select.value=value;return select};
const worldLevelPool=(items,level)=>items.filter(item=>item.tier<=level);
function treeSvg(item,view){
const crowns={
oak:'<circle cx="82" cy="82" r="42"/><circle cx="132" cy="64" r="48"/><circle cx="174" cy="90" r="40"/><circle cx="125" cy="103" r="54"/>',
birch:'<ellipse cx="105" cy="76" rx="50" ry="62"/><ellipse cx="150" cy="82" rx="45" ry="68"/>',
horse_chestnut:'<circle cx="78" cy="86" r="40"/><circle cx="120" cy="57" r="48"/><circle cx="168" cy="84" r="43"/><ellipse cx="122" cy="102" rx="72" ry="48"/>',
pine:'<path d="M120 25L58 132H182Z"/><path d="M120 55L45 155H195Z"/>',
beech:'<ellipse cx="120" cy="84" rx="78" ry="66"/>',
maple:'<circle cx="75" cy="83" r="41"/><circle cx="120" cy="62" r="52"/><circle cx="169" cy="85" r="42"/><circle cx="120" cy="103" r="55"/>',
willow:'<ellipse cx="120" cy="72" rx="72" ry="50"/><path d="M62 78Q55 132 72 160M88 75Q80 135 92 165M120 72Q116 140 122 168M151 76Q160 134 150 164M178 82Q188 132 174 157" fill="none" stroke="#4b8d54" stroke-width="9" stroke-linecap="round"/>',
alder:'<ellipse cx="90" cy="82" rx="52" ry="58"/><ellipse cx="150" cy="80" rx="55" ry="61"/><ellipse cx="120" cy="105" rx="65" ry="45"/>',
};
const leaves={
oak:'<path d="M120 24C105 34 110 50 96 53C80 57 88 73 76 81C65 91 80 103 78 116C76 132 96 132 105 143C112 152 116 162 120 171C124 158 130 149 138 141C148 131 166 131 163 115C160 102 175 92 164 81C153 72 161 57 145 53C132 50 136 34 120 24Z" fill="#5ca554"/><path d="M120 35V166M120 76L91 61M120 96L151 77M120 118L91 113M120 137L149 126" stroke="#35763d" stroke-width="4" fill="none"/>',
birch:'<path d="M120 25L178 122Q147 151 120 170Q91 151 62 122Z" fill="#72b85d"/><path d="M120 36V169M120 70L88 105M120 88L151 116M120 110L93 133" stroke="#397d42" stroke-width="4" fill="none"/>',
horse_chestnut:'<g fill="#4f9b4e" stroke="#34723b" stroke-width="3"><ellipse cx="120" cy="60" rx="18" ry="43"/><ellipse cx="83" cy="80" rx="17" ry="42" transform="rotate(-42 83 80)"/><ellipse cx="157" cy="80" rx="17" ry="42" transform="rotate(42 157 80)"/><ellipse cx="75" cy="122" rx="16" ry="38" transform="rotate(-68 75 122)"/><ellipse cx="165" cy="122" rx="16" ry="38" transform="rotate(68 165 122)"/></g><path d="M120 83V173" stroke="#34723b" stroke-width="5"/>',
pine:'<g stroke="#2f7747" stroke-width="4" stroke-linecap="round"><path d="M120 28V166"/><path d="M120 62L72 34M120 62L168 34M120 83L61 65M120 83L179 65M120 106L63 103M120 106L177 103M120 128L76 143M120 128L164 143"/></g><ellipse cx="155" cy="131" rx="13" ry="22" fill="#8b633e"/>',
beech:'<ellipse cx="120" cy="91" rx="58" ry="70" fill="#6eb05a"/><path d="M120 28V170M120 63L88 83M120 82L153 105M120 107L88 130M120 128L147 148" stroke="#3c7942" stroke-width="4" fill="none"/>',
maple:'<path d="M120 20L136 59L168 42L158 78L200 85L163 108L179 139L138 130L120 171L102 130L61 139L77 108L40 85L82 78L72 42L104 59Z" fill="#62a94e"/><path d="M120 38V169M120 94L73 80M120 94L168 81M120 111L82 133M120 111L158 133" stroke="#39743c" stroke-width="4" fill="none"/>',
willow:'<path d="M120 25C155 45 161 105 120 169C79 105 85 45 120 25Z" fill="#78b866"/><path d="M120 32V166M120 63L100 85M120 82L141 105M120 108L101 132" stroke="#447c44" stroke-width="4" fill="none"/>',
alder:'<path d="M120 29C163 36 181 75 168 120C158 151 135 163 120 170C105 163 82 151 72 120C59 75 77 36 120 29Z" fill="#6daa54"/><path d="M120 35V169M120 73L88 95M120 93L152 116M120 116L93 139" stroke="#3c7640" stroke-width="4" fill="none"/><path d="M105 29Q120 43 135 29" stroke="#3c7640" stroke-width="4" fill="none"/>',
};
if(view==='leaf')return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#edf7e8"/>${leaves[item.id]}</svg>`);
const trunk=item.id==='birch'?'#f2eee2':'#815939',marks=item.id==='birch'?'<path d="M108 78h24M105 105h29M109 132h21" stroke="#5e6060" stroke-width="4"/>':'';
return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#dff1fb"/><path d="M0 153Q70 135 120 149T240 150V180H0Z" fill="#b8d891"/><path d="M102 62L96 157H145L137 62Z" fill="${trunk}"/>${marks}<g fill="${item.id==='pine'?'#2f7547':'#5c9f4e'}">${crowns[item.id]}</g></svg>`);
}
function birdSvg(item){
const extras={
great_tit:'<circle cx="157" cy="61" r="15" fill="#fff"/><path d="M139 89Q151 118 151 148" stroke="#1e2427" stroke-width="8"/>',
blue_tit:'<circle cx="157" cy="62" r="14" fill="#fff"/><path d="M138 55Q154 42 171 52" stroke="#2f6f9d" stroke-width="8"/>',
house_sparrow:'<path d="M135 82Q149 92 154 117" stroke="#302b27" stroke-width="13"/><path d="M78 90l55 34M88 77l54 31" stroke="#d0b08b" stroke-width="4"/>',
mallard:'<path d="M136 79Q150 91 169 82" stroke="#fff" stroke-width="7"/>',
magpie:'<path d="M70 108L20 151L88 128Z" fill="#17323b"/><ellipse cx="115" cy="110" rx="32" ry="43" fill="#fff"/>',
woodpecker:'<path d="M97 82L126 119" stroke="#fff" stroke-width="14"/><path d="M82 80L104 95M94 68L118 83" stroke="#fff" stroke-width="7"/><path d="M88 130Q105 151 125 142" fill="#d94137"/>',
robin:'<ellipse cx="145" cy="97" rx="31" ry="39" fill="#ef6c32"/>',
blackbird:'',
}[item.id]||'';
const tail=item.id==='magpie'?'M82 111L26 158L101 132Z':'M84 112L45 142L98 130Z';
return worldSvgUrl(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 180"><rect width="240" height="180" rx="18" fill="#e7f3fa"/><path d="M22 150Q112 132 218 147" stroke="#7d5837" stroke-width="10" fill="none" stroke-linecap="round"/><path d="${tail}" fill="${item.id==='magpie'?'#172a33':item.wing}"/><ellipse cx="116" cy="105" rx="60" ry="43" fill="${item.body}" transform="rotate(7 116 105)"/><ellipse cx="95" cy="105" rx="34" ry="27" fill="${item.wing}" transform="rotate(12 95 105)"/><ellipse cx="139" cy="111" rx="31" ry="35" fill="${item.breast}"/><circle cx="157" cy="65" r="34" fill="${item.head}"/><path d="M186 62L218 71L187 79Z" fill="${item.bill}"/>${extras}<circle cx="168" cy="57" r="5" fill="#fff"/><circle cx="169" cy="58" r="2.5" fill="#15191c"/><path d="M112 141v18M137 141v18" stroke="#6b4b34" stroke-width="4"/><path d="M104 159h18M130 159h18" stroke="#6b4b34" stroke-width="3"/></svg>`);
}
function worldPicture(item,type,view){const img=document.createElement('img');img.className='world-picture';img.alt=worldName(item);img.draggable=false;img.src=type==='trees'?treeSvg(item,view):birdSvg(item);return img}
function worldVisual(item,type,view){const visual=document.createElement('div');visual.className='world-visual'+(view==='mixed'?' mixed':'');if(type==='trees'&&view==='mixed')visual.append(worldPicture(item,type,'tree'),worldPicture(item,type,'leaf'));else visual.append(worldPicture(item,type,view));return visual}
function mountWorldQuiz(root,init,config){
const S=Object.assign({activity:'quiz',level:1,view:config.type==='trees'?'mixed':'bird',score:0,streak:0,target:'',taskView:'',options:[],solved:false,feedback:''},init||{});S.level=Math.max(1,Math.min(3,+S.level||1));
const activity=worldSelect(S.activity,[['learn','worldLearn'],['quiz','worldQuiz']]),level=worldSelect(String(S.level),[['1','lvl1'],['2','lvl2'],['3','lvl3']]),stage=document.createElement('div'),settings=[field(T('worldActivity'),activity),field(T('worldLevel'),level)];
let view;if(config.type==='trees'){view=worldSelect(S.view,[['tree','worldTreeWhole'],['leaf','worldTreeLeaf'],['mixed','worldTreeMixed']]);settings.push(field(T('worldTreeView'),view))}
const pool=()=>worldLevelPool(config.items,S.level),current=()=>config.items.find(item=>item.id===S.target)||pool()[0];
const newTask=()=>{const items=pool(),previous=S.target;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.options=worldShuffle([target,...worldShuffle(items.filter(item=>item.id!==target.id)).slice(0,3)]).map(item=>item.id);S.solved=false;S.feedback='';draw()};
const answer=id=>{if(S.solved)return;if(id===S.target){S.solved=true;S.feedback='good';S.score++;S.streak++;sndGood()}else{S.feedback='bad';S.streak=0;sndBad()}draw()};
const gallery=()=>{const grid=document.createElement('div');grid.className='world-gallery';pool().forEach(item=>{const card=document.createElement('article');card.className='world-specimen';card.append(worldVisual(item,config.type,config.type==='trees'?S.view:'bird'));const name=document.createElement('h3');name.textContent=worldName(item);const hint=document.createElement('p');hint.textContent=worldHint(item);card.append(name,hint);grid.append(card)});return grid};
function draw(){S.activity=activity.value;S.level=+level.value;if(view)S.view=view.value;stage.className='world-app';stage.innerHTML='';const top=document.createElement('div');top.className='world-top';const title=document.createElement('strong');title.textContent=T('wg_'+config.type);const score=document.createElement('span');score.textContent=T('worldScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(title,score);stage.append(top);if(S.activity==='learn'){const intro=document.createElement('p');intro.className='world-intro';intro.textContent=T('worldExploreHint');stage.append(intro,gallery());return}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);const feedback=document.createElement('p');feedback.className='world-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'worldCorrect':'worldTryAgain');card.append(feedback);const actions=document.createElement('div');actions.className='world-actions';actions.append(btn(T('worldNext'),newTask,'ghost'));card.append(actions);if(S.solved){const fact=document.createElement('p');fact.className='world-fact';fact.textContent=worldHint(item);card.append(fact)}stage.append(card)}
activity.onchange=()=>{S.activity=activity.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);if(S.activity==='quiz')newTask();else draw();return{getState:()=>({...S,options:[...S.options]})};
}
function mountTrees(root,init){return mountWorldQuiz(root,init,{type:'trees',items:WORLD_TREES})}
function mountBirds(root,init){return mountWorldQuiz(root,init,{type:'birds',items:WORLD_BIRDS})}
const SVG_NS='http://www.w3.org/2000/svg';
function svgNode(name,attributes={}){const node=document.createElementNS(SVG_NS,name);Object.entries(attributes).forEach(([key,value])=>node.setAttribute(key,value));return node}
function svgAction(node,label,action){node.setAttribute('role','button');node.setAttribute('tabindex','0');node.setAttribute('aria-label',label);node.addEventListener('click',action);node.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();action()}})}
function makeTopoMap(mode,pool,onPick,showLabels,picked,target){
const wrap=document.createElement('div');wrap.className='topo-map-wrap';const svg=svgNode('svg',{class:'topo-map',viewBox:'20 20 330 510',role:'img','aria-label':T('topoMapLabel')});svg.append(svgNode('rect',{x:20,y:20,width:330,height:510,rx:22,class:'topo-water'}));
NL_PROVINCES.forEach(province=>{const path=svgNode('path',{d:province.d,fill:province.color,class:'topo-province'+(province.id===target?' target':'')+(province.id===picked?' picked':''),'data-id':province.id});const title=svgNode('title');title.textContent=worldName(province);path.append(title);if(onPick&&mode==='provinces')svgAction(path,worldName(province),()=>onPick(province.id));svg.append(path);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);svg.append(label)}});
if(mode!=='provinces')pool.forEach(city=>{const group=svgNode('g',{class:'topo-marker'+(city.id===target?' target':'')+(city.id===picked?' picked':'')});const hit=svgNode('circle',{cx:city.x,cy:city.y,r:15,class:'topo-hit'}),dot=svgNode('circle',{cx:city.x,cy:city.y,r:6,class:'topo-dot'});group.append(hit,dot);if(showLabels){const label=svgNode('text',{x:city.x+9,y:city.y-8,class:'topo-city-label'});label.textContent=worldName(city);group.append(label)}if(onPick)svgAction(group,worldName(city),()=>onPick(city.id));svg.append(group)});wrap.append(svg);return wrap;
}
function mountTopography(root,init){
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));
const activity=worldSelect(S.activity,[['learn','worldLearn'],['quiz','worldQuiz']]),mode=worldSelect(S.mode,[['provinces','topoProvinces'],['capitals','topoCapitals'],['cities','topoCities']]),level=worldSelect(String(S.level),[['1','lvl1'],['2','lvl2'],['3','lvl3']]),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),current=()=>pool().find(item=>item.id===S.target)||pool()[0];
const newTask=()=>{const items=pool(),previous=S.target;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.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{S.feedback='bad';S.streak=0;sndBad()}draw()};
function draw(){S.activity=activity.value;S.mode=mode.value;S.level=+level.value;stage.className='world-app topo-app';stage.innerHTML='';const top=document.createElement('div');top.className='world-top';const title=document.createElement('strong');title.textContent=T('wg_topography');const score=document.createElement('span');score.textContent=T('worldScore').replace('{score}',S.score).replace('{streak}',S.streak);top.append(title,score);stage.append(top);const card=document.createElement('section');card.className='world-task-card topo-card';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,'',''))}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 feedback=document.createElement('p');feedback.className='world-feedback '+(S.feedback==='good'?'ok':S.feedback?'bad':'');feedback.setAttribute('role','status');if(S.feedback)feedback.textContent=T(S.feedback==='good'?'worldCorrect':'worldTryAgain');const actions=document.createElement('div');actions.className='world-actions';actions.append(btn(T('worldNext'),newTask,'ghost'));card.append(feedback,actions)}stage.append(card)}
activity.onchange=()=>{S.activity=activity.value;if(S.activity==='quiz')newTask();else draw()};mode.onchange=()=>{S.mode=mode.value;if(S.activity==='quiz')newTask();else draw()};level.onchange=()=>{S.level=+level.value;if(S.activity==='quiz')newTask();else draw()};learningShell(root,[field(T('worldActivity'),activity),field(T('topoMode'),mode),field(T('worldLevel'),level)],stage);if(S.activity==='quiz')newTask();else draw();return{getState:()=>({...S})};
}

View file

@ -5,6 +5,7 @@ import {runInNewContext} from 'node:vm';
const ids=['birthday','numberline','sentence'];
const learningIds=['fractions','clock','placevalue','money','wordtypes','schedule','mood','poll'];
const worldIds=['trees','birds','topography'];
test('nieuwe widgets zijn geladen vóór het widgetregister', async()=>{
const html=await readFile('public/index.html','utf8');
@ -54,6 +55,43 @@ test('nieuwe widgets zijn tweetalig beschreven', async()=>{
}
});
test('wereldoriëntatiewidgets laden, registreren en vertalen', async()=>{
const [html,board,source,core]=await Promise.all([readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8'),readFile('public/js/widgets/world.js','utf8'),readFile('public/js/core.js','utf8')]);
assert.ok(html.indexOf('js/widgets/world.js')<html.indexOf('js/board.js'));
for(const [id,mount] of [['trees','mountTrees'],['birds','mountBirds'],['topography','mountTopography']]){
assert.ok(worldIds.includes(id));
assert.match(board,new RegExp('id:"'+id+'"[^\\n]*cat:"world"[^\\n]*mount:'+mount));
assert.match(source,new RegExp('function '+mount+'\\('));
assert.equal((core.match(new RegExp('wg_'+id+':','g'))||[]).length,2,id);
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','topoProvinces','topoCapitals','topoCities','topoFindProvince','topoFindCapital','topoFindCity'])assert.equal((core.match(new RegExp(key+':','g'))||[]).length,2,key);
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 volledige soorten en Nederlandse topografie', async()=>{
const source=await readFile('public/js/widgets/world.js','utf8'),context={};
runInNewContext(source+';globalThis.__world={trees:WORLD_TREES,birds:WORLD_BIRDS,provinces:NL_PROVINCES,cities:NL_CITIES}',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);
for(const item of [...data.trees,...data.birds])assert.ok(item.nl&&item.en&&item.hintNl&&item.hintEn&&item.tier>=1&&item.tier<=3,item.id);
const cities=new Map(data.cities.map(city=>[city.id,city]));
const capitals=Object.fromEntries(data.provinces.map(province=>[province.nl,cities.get(province.capital)?.nl]));
assert.deepEqual(capitals,{
Friesland:'Leeuwarden',Groningen:'Groningen',Drenthe:'Assen',Overijssel:'Zwolle',Flevoland:'Lelystad','Noord-Holland':'Haarlem',Utrecht:'Utrecht',Gelderland:'Arnhem','Zuid-Holland':'Den Haag',Zeeland:'Middelburg','Noord-Brabant':"'s-Hertogenbosch",Limburg:'Maastricht',
});
for(const province of data.provinces)assert.ok(province.d&&Number.isFinite(province.x)&&Number.isFinite(province.y)&&cities.has(province.capital),province.id);
});
test('widgetbibliotheek bevat zoeken favorieten sjablonen en borddata-acties', async()=>{
const [html,board]=await Promise.all([readFile('public/index.html','utf8'),readFile('public/js/board.js','utf8')]);
for(const id of ['gallerySearch','galleryFavOnly','galleryTemplates','boardUndo','boardRedo','boardExport','boardImport'])assert.match(html,new RegExp('id="'+id+'"'));
@ -231,6 +269,9 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
msub:'.mt-pad',
mmul:'.mt-pad',
mdiv:'.mt-pad',
trees:'.world-gallery',
birds:'.world-picture',
topography:'.topo-map',
mpct:'.mt-pad',
numberline:'.nlw-settings',
fractions:'.fraction-wall',
@ -267,7 +308,8 @@ test('alle geregistreerde widgets hebben mobiele dekking en passen op een 320px-
'.bd{grid-template-columns:1fr}',
'.sb-banks{grid-template-columns:1fr}',
'.edu-stage{align-items:safe center;justify-content:safe center',
'.money-task-card,.clock-task-card',
'.money-task-card,.clock-task-card,.world-task-card',
'.world-gallery{grid-template-columns:1fr}',
]){
assert.ok(compact.includes(selector),selector+' mist in de compacte containerregels');
}