Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-66 into main AGit
5 changed files with 250 additions and 148 deletions
Showing only changes of commit 6d5d4ef520 - Show all commits

View file

@ -1 +1 @@
0.3.29-beta
0.3.30-beta

View file

@ -1015,9 +1015,29 @@ function switchTo(f, b){
loadCurrentSlot();
}
const btnFolders = document.getElementById("btnFolders");
/* ankerdiagram uit de bibliotheek openen vanuit de hoofdverkenner: plaatst
een nieuw ankerwidget op het huidige bord met dat diagram erin geladen */
function openAnchorOnBoard(entry){
const def = REGISTRY.find(d=>d.id==="anchor");
makeWidget(def, { state: {
cells: structuredClone(entry.cells || {}),
pics: structuredClone(entry.pics || {}),
}});
markBoardChange();
return null;
}
btnFolders.addEventListener("click", ()=>{
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
openExplorer(withSharedBranches("board", boardsExplorerAdapter(), boardsSharedHooks()));
openExplorer(withBranch(
"⚓ " + T("anchorLibTitle"),
withSharedBranches("board", boardsExplorerAdapter(), boardsSharedHooks()),
withSharedBranches("anchor", anchorLibraryAdapter(openAnchorOnBoard), anchorLibrarySharedHooks(openAnchorOnBoard)),
{
wrongTypeMsg: T("exWrongBranch"),
branchCreateMsg: T("anchorLibCreateHint"),
branchCount: ()=>ANCHOR_LIB.length,
}
));
});
/* koppelstuk voor de gedeelde bibliotheek (zie withSharedBranches in
explorer.js): borden exporteren/importeren als {name, data}. Een kopie

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.29-beta";
const VERSION = "0.3.30-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -90,6 +90,8 @@ const I18N = {
shNoCreateHere:"Nieuw maken kan alleen in je eigen mappen - sleep het daarna hierheen om te delen.",
shReadonly:"Alleen lezen - kopieer het naar je eigen mappen om te bewerken.",
exLock:"Alleen lezen", shLockedOk:"Vergrendeld ✓", shUnlockedOk:"Ontgrendeld ✓",
exWrongBranch:"Dat hoort niet in deze map (borden en ankerdiagrammen zijn gescheiden).",
anchorLibCreateHint:"Nieuw diagram maken: gebruik 💾 op een ankerwidget.",
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
wFull:"Maximaliseren", wUnfull:"Terugzetten",
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
@ -271,6 +273,8 @@ const I18N = {
shNoCreateHere:"Create it in your own folders first - then drag it here to share.",
shReadonly:"Read-only - copy it to your own folders to edit.",
exLock:"Read-only", shLockedOk:"Locked ✓", shUnlockedOk:"Unlocked ✓",
exWrongBranch:"That doesn't belong in this folder (boards and anchor diagrams are separate).",
anchorLibCreateHint:"To create a new diagram: use 💾 on an anchor widget.",
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
wFull:"Maximize", wUnfull:"Restore",
wZoom:"Content bigger or smaller (low vision)",

View file

@ -333,6 +333,82 @@
}
})();
/* =========================================================
Combineert twee adapters tot één boom: sub verschijnt als vaste map
(rootLabel) in de root van main. Slepen tussen de twee takken wordt
geweigerd (ander soort inhoud). Item-sleutels van sub worden geprefixt
zodat ze nooit botsen met die van main.
opts: { wrongTypeMsg, branchCreateMsg, branchCount() }
==========================================================*/
function withBranch(rootLabel, main, sub, opts){
const ENC = "br|";
const enc = k => ENC + String(k);
const dec = k => { const raw = String(k).slice(ENC.length); return /^\d+$/.test(raw) ? +raw : raw; };
const isEnc = k => typeof k === "string" && k.startsWith(ENC);
const inBranch = path => path.length > 0 && path[0] === rootLabel;
const wrongType = ()=>(opts && opts.wrongTypeMsg) || T("exExists");
const shiftSel = sel => (sel && sel.type === "item" && isEnc(sel.key)) ? { type:"item", key: dec(sel.key) } : sel;
return {
title: main.title,
allowRootItems: main.allowRootItems,
newItemLabel: main.newItemLabel,
async init(){
if(main.init) await main.init();
if(sub.init) await sub.init();
},
list(path){
if(inBranch(path)){
const r = sub.list(path.slice(1));
return { folders: r.folders, items: r.items.map(it=>({ ...it, key: enc(it.key) })) };
}
const r = main.list(path);
if(path.length === 0){
r.folders.push({ name: rootLabel, count: opts && opts.branchCount ? opts.branchCount() : "", fixed: true });
}
return r;
},
createItem: main.createItem ? (path)=>{
if(inBranch(path)) return (opts && opts.branchCreateMsg) || T("shNoCreateHere");
return main.createItem(path);
} : undefined,
createFolder(path, name){
if(inBranch(path)) return sub.createFolder(path.slice(1), name);
if(path.length === 0 && name === rootLabel) return T("exExists");
return main.createFolder(path, name);
},
renameFolder(path, oldName, newName){
if(path.length === 0 && oldName === rootLabel) return T("exFixedFolder");
return inBranch(path) ? sub.renameFolder(path.slice(1), oldName, newName) : main.renameFolder(path, oldName, newName);
},
moveFolder(srcPath, targetPath, newName){
if(srcPath.length === 1 && srcPath[0] === rootLabel) return T("exFixedFolder");
const s = inBranch(srcPath), t = inBranch(targetPath);
if(s !== t) return wrongType();
return s ? sub.moveFolder(srcPath.slice(1), targetPath.slice(1), newName) : main.moveFolder(srcPath, targetPath, newName);
},
deleteFolder(path, name){
if(path.length === 0 && name === rootLabel) return T("exFixedFolder");
return inBranch(path) ? sub.deleteFolder(path.slice(1), name) : main.deleteFolder(path, name);
},
renameItem(k, name){ return isEnc(k) ? sub.renameItem(dec(k), name) : main.renameItem(k, name); },
moveItem(k, dest){
const s = isEnc(k), t = inBranch(dest);
if(s !== t) return wrongType();
return s ? sub.moveItem(dec(k), dest.slice(1)) : main.moveItem(k, dest);
},
deleteItem(k){ return isEnc(k) ? sub.deleteItem(dec(k)) : main.deleteItem(k); },
openItem(k){ return isEnc(k) ? sub.openItem(dec(k)) : main.openItem(k); },
lockLabel(path){
if(inBranch(path)) return sub.lockLabel ? sub.lockLabel(path.slice(1)) : null;
return main.lockLabel ? main.lockLabel(path) : null;
},
toggleLock(sel, path){
if(inBranch(path)) return sub.toggleLock ? sub.toggleLock(shiftSel(sel), path.slice(1)) : null;
return main.toggleLock ? main.toggleLock(sel, path) : null;
},
};
}
/* =========================================================
Gedeelde bibliotheek in de verkenner: wikkelt een "eigen inhoud"-adapter
en voegt in de root twee vaste takken toe: 🌐 Systeem (systeembreed,

View file

@ -228,10 +228,54 @@ function mountAnchor(root, initState, opts){
inp.addEventListener("blur", commit);
inp.addEventListener("pointerdown", ev=>ev.stopPropagation());
}
/* ---- ankerbibliotheek: diagram los van het bord bewaren en hergebruiken.
Opslag in ANCHOR_LIB (eigen save-sectie "anchors"), beheer via dezelfde
Windows-stijl mappenverkenner als de borden en thema's. */
function anchorLibAdapter(){
/* een bewaard/gedeeld diagram in dít widget laden (dubbelklik in de
verkenner die via het 🗂-knopje van dit widget is geopend) */
function loadDiagram(entry){
S.cells = structuredClone(entry.cells || {});
S.pics = structuredClone(entry.pics || {});
S.pan = {x:0, y:0};
syncSource();
render();
persistWords();
/* programmatisch geladen: de bord-autosave-listeners zien dit niet */
markBoardChange();
return null;
}
if(!readonly){
saveBtn.title = T("anchorLibSave");
libBtn.title = T("anchorLibOpen");
saveBtn.addEventListener("click", ()=>{
if(!Object.keys(S.cells).length){ hint.textContent = T("anchorLibEmptyGrid"); return; }
ANCHOR_LIB.push({
id: "al-"+Math.random().toString(36).slice(2,10),
name: reg.name,
cells: structuredClone(S.cells),
pics: structuredClone(S.pics),
});
scheduleSave("anchors");
const orig = saveBtn.textContent;
saveBtn.textContent = "✓";
setTimeout(()=>{ saveBtn.textContent = orig; }, 1200);
});
libBtn.addEventListener("click", ()=>openExplorer(
withSharedBranches("anchor", anchorLibraryAdapter(loadDiagram), anchorLibrarySharedHooks(loadDiagram))));
}
new ResizeObserver(render).observe(stage);
document.addEventListener("langchange", render);
syncSource(true);
render();
if(reg.words.length) document.dispatchEvent(new CustomEvent("wordschange"));
return { getState: ()=>({ cells:S.cells, pan:S.pan, pics:S.pics, themeId:S.themeId }) };
}
/* ---- ankerbibliotheek: diagrammen los van een bord bewaren en hergebruiken.
Opslag in ANCHOR_LIB (eigen save-sectie "anchors"), beheer via de
Windows-stijl mappenverkenner. Op module-niveau zodat zowel het 🗂-knopje
op een widget (openen = in dát widget laden) als de vaste -tak in de
borden-verkenner (openen = nieuw widget op het huidige bord) dezelfde
adapter gebruiken - alleen de onOpen-actie verschilt. */
function anchorLibraryAdapter(onOpen){
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
const join = p => p.join("/");
const wordCount = it => Object.keys(it.cells||{}).length;
@ -313,39 +357,14 @@ function mountAnchor(root, initState, opts){
changed();
return null;
},
/* dubbelklik: bewaard diagram in dít widget laden */
openItem(i){
const it = ANCHOR_LIB[i];
if(!it) return;
S.cells = structuredClone(it.cells || {});
S.pics = structuredClone(it.pics || {});
S.pan = {x:0, y:0};
syncSource();
render();
persistWords();
/* programmatisch geladen: de bord-autosave-listeners zien dit niet */
markBoardChange();
return it ? onOpen(it) : null;
},
};
}
if(!readonly){
saveBtn.title = T("anchorLibSave");
libBtn.title = T("anchorLibOpen");
saveBtn.addEventListener("click", ()=>{
if(!Object.keys(S.cells).length){ hint.textContent = T("anchorLibEmptyGrid"); return; }
ANCHOR_LIB.push({
id: "al-"+Math.random().toString(36).slice(2,10),
name: reg.name,
cells: structuredClone(S.cells),
pics: structuredClone(S.pics),
});
scheduleSave("anchors");
const orig = saveBtn.textContent;
saveBtn.textContent = "✓";
setTimeout(()=>{ saveBtn.textContent = orig; }, 1200);
});
/* koppelstuk voor de gedeelde bibliotheek: diagrammen als {name, data} */
const anchorSharedHooks = ()=>{
function anchorLibrarySharedHooks(onOpen){
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
const join = p => p.join("/");
return {
@ -378,25 +397,8 @@ function mountAnchor(root, initState, opts){
return null;
},
openShared(item){
S.cells = structuredClone(item.data.cells || {});
S.pics = structuredClone(item.data.pics || {});
S.pan = {x:0, y:0};
syncSource();
render();
persistWords();
markBoardChange();
return null;
return onOpen({ name: item.name, cells: item.data.cells || {}, pics: item.data.pics || {} });
},
};
};
libBtn.addEventListener("click", ()=>openExplorer(withSharedBranches("anchor", anchorLibAdapter(), anchorSharedHooks())));
}
new ResizeObserver(render).observe(stage);
document.addEventListener("langchange", render);
syncSource(true);
render();
if(reg.words.length) document.dispatchEvent(new CustomEvent("wordschange"));
return { getState: ()=>({ cells:S.cells, pan:S.pan, pics:S.pics, themeId:S.themeId }) };
}