v0.2.04-beta: mobiel- en tablet-vriendelijk maken van bord, widgets en beheer
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 14s

- Responsive breakpoints toegevoegd (tablet ≤1024px, telefoon ≤600px) en
  grotere tikzones op aanraakschermen (pointer:coarse) - er waren nog
  geen @media-regels in de hele stylesheet
- Bugfix: #boardsPanel had een vaste 300px-breedte en overflowde op een
  telefoon; nu net zo responsive als #boardStrip
- Widgets (letters, rekenen, schrijfblok, mindmap, ...) krijgen niet
  langer harde desktop-pixelmaten die van het scherm afvallen - grootte
  en positie worden nu geclampt aan de zichtbare bordgrootte
- Eigen, groter aanraakbare resize-greep voor widgets i.p.v. de
  piepkleine native browser-grip (resize:both)
- Beheerpaneel: dichte rijen (wachtwoordveld + knoppen) stapelen nu
  netjes op smalle schermen i.p.v. verdrukt te wrappen

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ramon 2026-07-13 07:53:52 +02:00
parent 64acb84f50
commit 05d4fc7c8e
3 changed files with 107 additions and 10 deletions

View file

@ -5,6 +5,8 @@
--yellow:#f7c948; --purple:#9b59b6; --orange:#f28c38; --yellow:#f7c948; --purple:#9b59b6; --orange:#f28c38;
--wood:#f6e7c9; --wood-d:#e8d2a4; --wood:#f6e7c9; --wood-d:#e8d2a4;
--radius:16px; --shadow:0 10px 30px rgba(30,50,90,.18); --radius:16px; --shadow:0 10px 30px rgba(30,50,90,.18);
/* minimale tikzones voor touch (WCAG/iOS-richtlijn) */
--tap:44px; --tap-sm:36px;
} }
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;} *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
*::-webkit-scrollbar{width:8px; height:8px;} *::-webkit-scrollbar{width:8px; height:8px;}
@ -310,7 +312,7 @@
backdrop-filter:blur(14px) saturate(1.15); -webkit-backdrop-filter:blur(14px) saturate(1.15); backdrop-filter:blur(14px) saturate(1.15); -webkit-backdrop-filter:blur(14px) saturate(1.15);
box-shadow:0 4px 14px rgba(30,50,90,.08); border:1px solid rgba(255,255,255,.6); box-shadow:0 4px 14px rgba(30,50,90,.08); border:1px solid rgba(255,255,255,.6);
display:flex; flex-direction:column; display:flex; flex-direction:column;
min-width:280px; min-height:200px; resize:both; overflow:hidden; min-width:280px; min-height:200px; resize:none; overflow:hidden;
transition:background .2s, box-shadow .2s, border-color .2s; transition:background .2s, box-shadow .2s, border-color .2s;
} }
.widget:hover,.widget:focus-within{ .widget:hover,.widget:focus-within{
@ -321,6 +323,17 @@
.widget .wmin,.widget .wclose{opacity:.4; transition:opacity .15s;} .widget .wmin,.widget .wclose{opacity:.4; transition:opacity .15s;}
.widget:hover .wmin,.widget:hover .wclose, .widget:hover .wmin,.widget:hover .wclose,
.widget:focus-within .wmin,.widget:focus-within .wclose{opacity:1;} .widget:focus-within .wmin,.widget:focus-within .wclose{opacity:1;}
/* eigen, groter aanraakbare resize-greep i.p.v. de piepkleine native
browser-grip (die met resize:none hierboven is uitgeschakeld) */
.widget-rs{
position:absolute; right:4px; bottom:4px; width:34px; height:34px;
border-radius:50%; background:var(--blue); border:3px solid #fff;
cursor:nwse-resize; box-shadow:0 2px 6px rgba(0,0,0,.3); touch-action:none;
opacity:0; transition:opacity .15s; z-index:2;
}
.widget:hover .widget-rs,.widget:focus-within .widget-rs{opacity:.6;}
.widget-rs:hover,.widget-rs:active{opacity:1 !important;}
.widget.bare .widget-rs,.widget[data-max="1"] .widget-rs{display:none;}
/* unified floating bar: pen palette + dock, draggable, combined when both active */ /* unified floating bar: pen palette + dock, draggable, combined when both active */
#hud{ #hud{
position:fixed; left:50%; bottom:16px; transform:translateX(-50%); z-index:5500; position:fixed; left:50%; bottom:16px; transform:translateX(-50%); z-index:5500;
@ -758,6 +771,7 @@
body.hc .widget{background:#fff; border:2.5px solid #000; box-shadow:0 4px 14px rgba(0,0,0,.35); backdrop-filter:none;} body.hc .widget{background:#fff; border:2.5px solid #000; box-shadow:0 4px 14px rgba(0,0,0,.35); backdrop-filter:none;}
body.hc .widget:hover,.body.hc .widget:focus-within{background:#fff;} body.hc .widget:hover,.body.hc .widget:focus-within{background:#fff;}
body.hc .widget .wmin,body.hc .widget .wclose{opacity:1;} body.hc .widget .wmin,body.hc .widget .wclose{opacity:1;}
body.hc .widget-rs{opacity:1; border:3px solid #000;}
body.hc .widget-head{background:#f2f2f2; border-bottom:2px solid #000;} body.hc .widget-head{background:#f2f2f2; border-bottom:2px solid #000;}
body.hc .slot{border-color:#000; background:#fff;} body.hc .slot{border-color:#000; background:#fff;}
body.hc .slot .ghost{color:#666;} body.hc .slot .ghost{color:#666;}
@ -987,6 +1001,7 @@
font-size:13px; width:110px; outline:none; font-size:13px; width:110px; outline:none;
} }
.am-code{font-size:12.5px; font-weight:800; color:var(--orange);} .am-code{font-size:12.5px; font-weight:800; color:var(--orange);}
.am-actions{display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.am-btn{ .am-btn{
border:none; background:#e8eef6; border-radius:8px; padding:6px 10px; border:none; background:#e8eef6; border-radius:8px; padding:6px 10px;
font:inherit; font-weight:800; font-size:12px; cursor:pointer; color:var(--ink); font:inherit; font-weight:800; font-size:12px; cursor:pointer; color:var(--ink);
@ -1022,3 +1037,48 @@
.am-reset-pw{font-weight:800; font-family:monospace; font-size:13px; color:var(--green);} .am-reset-pw{font-weight:800; font-family:monospace; font-size:13px; color:var(--green);}
body.hc .am-row,body.hc .am-chip{border:1.5px solid #000; background:#fff;} body.hc .am-row,body.hc .am-chip{border:1.5px solid #000; background:#fff;}
body.hc .am-reset,body.hc .am-reset-row{border:1.5px solid #000; background:#fff;} body.hc .am-reset,body.hc .am-reset-row{border:1.5px solid #000; background:#fff;}
/* ============================================================
Mobiel & tablet: responsieve aanpassingen. Alles hieronder is
additief - de bovenstaande desktopstijl blijft de standaard.
============================================================ */
/* ---------- tablet (≤1024px) ---------- */
@media (max-width:1024px){
/* #boardsPanel had een vaste 300px-breedte die op smallere
schermen overflowt; #boardStrip deed dit al goed. */
#boardsPanel{width:min(300px, calc(100vw - 32px));}
}
/* ---------- telefoon (≤600px) ---------- */
@media (max-width:600px){
#toolbar{padding:0 8px; gap:6px;}
.logo{font-size:22px;}
.logo .crayon{font-size:18px; margin-left:4px;}
#verTag{display:none;}
.tbtn{padding:8px 10px; font-size:13px;}
#boardsPanel{top:60px; max-height:min(66vh,480px);}
/* beheerpaneel: dichte rijen worden gestapeld i.p.v. verdrukt */
.am-row{flex-direction:column; align-items:stretch;}
.am-row .am-pw{width:100%;}
.am-row .am-actions{display:flex; flex-direction:row; justify-content:flex-end; gap:6px;}
.am-reset-row{flex-direction:column; align-items:stretch;}
.am-add .am-inp{flex:1 1 100%;}
}
/* ---------- aanraakschermen (los van breedte) ---------- */
@media (pointer:coarse){
.tbtn{min-height:var(--tap-sm);}
#langToggle button{min-height:var(--tap-sm);}
.hud-btn{width:var(--tap-sm); height:var(--tap-sm); font-size:15px;}
#penbar .wp-group button{width:var(--tap-sm); height:var(--tap-sm);}
#hud.vert{width:104px;}
#hud.vert #penbar .wp-group{grid-template-columns:repeat(2,36px);}
.am-btn,.am-copy{min-height:var(--tap-sm); padding:8px 12px;}
.am-del{width:var(--tap-sm); height:var(--tap-sm);}
.am-chip{padding:7px 14px;}
.am-chipx{padding:4px;}
/* geen hover op aanraakschermen: de greep moet altijd zichtbaar zijn */
.widget-rs{opacity:.7; width:38px; height:38px;}
}

View file

@ -79,6 +79,10 @@
function userRow(u){ function userRow(u){
const row = h("div","am-row"); const row = h("div","am-row");
/* actieknoppen (kopieer/genereer/verwijder) samen in één groep, zodat
ze op smalle schermen als blok rechts blijven staan i.p.v. los
tussen de andere rij-elementen te wrappen */
const actions = h("div","am-actions");
row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : ""))); row.appendChild(h("span","am-name", u.displayName + (u.displayName!==u.username ? ` (${u.username})` : "")));
row.appendChild(h("span","am-role", T(roleKey(u.role)))); row.appendChild(h("span","am-role", T(roleKey(u.role))));
if(u.role==="pupil"){ if(u.role==="pupil"){
@ -106,7 +110,7 @@
pwCopy.type = "button"; pwCopy.type = "button";
pwCopy.title = T("amCopy"); pwCopy.title = T("amCopy");
pwCopy.addEventListener("click", e=>{ e.preventDefault(); if(pw.value) copyText(pwCopy, pw.value); }); pwCopy.addEventListener("click", e=>{ e.preventDefault(); if(pw.value) copyText(pwCopy, pw.value); });
row.appendChild(pwCopy); actions.appendChild(pwCopy);
if(u.everLoggedIn){ if(u.everLoggedIn){
const gen = h("button","am-btn","🎲"); const gen = h("button","am-btn","🎲");
gen.title = T("amGenerate"); gen.title = T("amGenerate");
@ -118,7 +122,7 @@
msg(`${T("amPwSet")} ${password}`, password); msg(`${T("amPwSet")} ${password}`, password);
}catch(e){ msg(e.message); } }catch(e){ msg(e.message); }
}); });
row.appendChild(gen); actions.appendChild(gen);
} }
}else{ }else{
if(u.pending && u.linkCode){ if(u.pending && u.linkCode){
@ -135,7 +139,7 @@
msg(`${T("amCreated")} ${r.linkCode}`, r.linkCode); msg(`${T("amCreated")} ${r.linkCode}`, r.linkCode);
}catch(e){ msg(e.message); } }catch(e){ msg(e.message); }
}); });
row.appendChild(nc); actions.appendChild(nc);
/* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */ /* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */
const np = h("button","am-btn", T("amNewPw")); const np = h("button","am-btn", T("amNewPw"));
np.addEventListener("click", async ()=>{ np.addEventListener("click", async ()=>{
@ -145,7 +149,7 @@
msg(`${T("amPwSet")} ${r.password}`, r.password); msg(`${T("amPwSet")} ${r.password}`, r.password);
}catch(e){ msg(e.message); } }catch(e){ msg(e.message); }
}); });
row.appendChild(np); actions.appendChild(np);
} }
} }
if(u.id !== currentUser.id){ if(u.id !== currentUser.id){
@ -156,8 +160,9 @@
try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); } try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); }
catch(e){ msg(e.message); } catch(e){ msg(e.message); }
}); });
row.appendChild(del); actions.appendChild(del);
} }
if(actions.childNodes.length) row.appendChild(actions);
return row; return row;
} }

View file

@ -28,15 +28,26 @@ const REGISTRY = [
]; ];
const WIDGET_CATS = [["taal","catTaal"],["rekenen","catRekenen"],["tools","catTools"],["media","catMedia"]]; const WIDGET_CATS = [["taal","catTaal"],["rekenen","catRekenen"],["tools","catTools"],["media","catMedia"]];
/* houdt een (opgeslagen of standaard) widgetgrootte binnen het bord - zonder
dit passen desktop-formaten (tot 760px breed) niet op een telefoon/tablet */
function clampWidgetSize(w, h, def){
const fw = Math.max(def.minW || 240, Math.min(w, board.clientWidth));
const fh = Math.max(def.minH || 180, Math.min(h, board.clientHeight));
return { w: fw, h: fh };
}
function makeWidget(def, saved){ function makeWidget(def, saved){
const win = document.createElement("div"); const win = document.createElement("div");
win.className = "widget"; win.className = "widget";
win.style.width = (saved && saved.w ? saved.w : def.w)+"px"; const size = clampWidgetSize(saved && saved.w ? saved.w : def.w, saved && saved.h ? saved.h : def.h, def);
win.style.height = (saved && saved.h ? saved.h : def.h)+"px"; win.style.width = size.w+"px";
win.style.height = size.h+"px";
if(def.minW) win.style.minWidth = def.minW+"px"; if(def.minW) win.style.minWidth = def.minW+"px";
if(def.minH) win.style.minHeight = def.minH+"px"; if(def.minH) win.style.minHeight = def.minH+"px";
win.style.left = (saved && saved.x!=null ? saved.x : 40 + (widgetCount*36)%240) + "px"; const cx = saved && saved.x!=null ? saved.x : 40 + (widgetCount*36)%240;
win.style.top = (saved && saved.y!=null ? saved.y : 30 + (widgetCount*30)%160) + "px"; const cy = saved && saved.y!=null ? saved.y : 30 + (widgetCount*30)%160;
win.style.left = Math.max(0, Math.min(cx, board.clientWidth - size.w - 8)) + "px";
win.style.top = Math.max(0, Math.min(cy, board.clientHeight - size.h - 8)) + "px";
win.style.zIndex = ++zTop; win.style.zIndex = ++zTop;
widgetCount++; widgetCount++;
if(saved && saved.bare){ win.classList.add("bare"); win.dataset.bare = "1"; } if(saved && saved.bare){ win.classList.add("bare"); win.dataset.bare = "1"; }
@ -61,6 +72,27 @@ function makeWidget(def, saved){
body.className = "widget-body"; body.className = "widget-body";
win.appendChild(head); win.appendChild(body); win.appendChild(head); win.appendChild(body);
/* eigen resize-greep (touch-vriendelijk, vervangt de native browser-grip) */
const rs = document.createElement("div");
rs.className = "widget-rs";
rs.addEventListener("pointerdown", e=>{
e.preventDefault(); e.stopPropagation();
win.style.zIndex = ++zTop;
const zf = VZ();
const sw = win.offsetWidth, sh = win.offsetHeight, sx = e.clientX, sy = e.clientY;
const mv = ev=>{
const maxW = board.clientWidth - win.offsetLeft - 8;
const maxH = board.clientHeight - win.offsetTop - 8;
let nw = sw + (ev.clientX - sx)/zf, nh = sh + (ev.clientY - sy)/zf;
nw = Math.max(def.minW || 240, Math.min(nw, maxW));
nh = Math.max(def.minH || 180, Math.min(nh, maxH));
win.style.width = nw+"px"; win.style.height = nh+"px";
};
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up);
});
win.appendChild(rs);
let pill = null; let pill = null;
function setMin(m){ function setMin(m){
if(m && !pill){ if(m && !pill){