From 05d4fc7c8e42dc960bf784bd69f06aa6139615b7 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 13 Jul 2026 07:53:52 +0200 Subject: [PATCH] v0.2.04-beta: mobiel- en tablet-vriendelijk maken van bord, widgets en beheer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- public/css/teach.css | 62 +++++++++++++++++++++++++++++++++++++++++++- public/js/admin.js | 15 +++++++---- public/js/board.js | 40 +++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/public/css/teach.css b/public/css/teach.css index bbdf9e1..3d3d1c5 100644 --- a/public/css/teach.css +++ b/public/css/teach.css @@ -5,6 +5,8 @@ --yellow:#f7c948; --purple:#9b59b6; --orange:#f28c38; --wood:#f6e7c9; --wood-d:#e8d2a4; --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;} *::-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); box-shadow:0 4px 14px rgba(30,50,90,.08); border:1px solid rgba(255,255,255,.6); 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; } .widget:hover,.widget:focus-within{ @@ -321,6 +323,17 @@ .widget .wmin,.widget .wclose{opacity:.4; transition:opacity .15s;} .widget:hover .wmin,.widget:hover .wclose, .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 */ #hud{ 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:hover,.body.hc .widget:focus-within{background:#fff;} 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 .slot{border-color:#000; background:#fff;} body.hc .slot .ghost{color:#666;} @@ -987,6 +1001,7 @@ font-size:13px; width:110px; outline:none; } .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{ border:none; background:#e8eef6; border-radius:8px; padding:6px 10px; 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);} 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;} + +/* ============================================================ + 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;} +} diff --git a/public/js/admin.js b/public/js/admin.js index a253058..5c56965 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -79,6 +79,10 @@ function userRow(u){ 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-role", T(roleKey(u.role)))); if(u.role==="pupil"){ @@ -106,7 +110,7 @@ pwCopy.type = "button"; pwCopy.title = T("amCopy"); pwCopy.addEventListener("click", e=>{ e.preventDefault(); if(pw.value) copyText(pwCopy, pw.value); }); - row.appendChild(pwCopy); + actions.appendChild(pwCopy); if(u.everLoggedIn){ const gen = h("button","am-btn","🎲"); gen.title = T("amGenerate"); @@ -118,7 +122,7 @@ msg(`${T("amPwSet")} ${password}`, password); }catch(e){ msg(e.message); } }); - row.appendChild(gen); + actions.appendChild(gen); } }else{ if(u.pending && u.linkCode){ @@ -135,7 +139,7 @@ msg(`${T("amCreated")} ${r.linkCode}`, r.linkCode); }catch(e){ msg(e.message); } }); - row.appendChild(nc); + actions.appendChild(nc); /* direct een nieuw wachtwoord zetten, zonder de koppelcode-activatie */ const np = h("button","am-btn", T("amNewPw")); np.addEventListener("click", async ()=>{ @@ -145,7 +149,7 @@ msg(`${T("amPwSet")} ${r.password}`, r.password); }catch(e){ msg(e.message); } }); - row.appendChild(np); + actions.appendChild(np); } } if(u.id !== currentUser.id){ @@ -156,8 +160,9 @@ try{ await api("/admin/users/"+u.id, {method:"DELETE"}); reload(); } catch(e){ msg(e.message); } }); - row.appendChild(del); + actions.appendChild(del); } + if(actions.childNodes.length) row.appendChild(actions); return row; } diff --git a/public/js/board.js b/public/js/board.js index f657bc4..c3faa7c 100644 --- a/public/js/board.js +++ b/public/js/board.js @@ -28,15 +28,26 @@ const REGISTRY = [ ]; 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){ const win = document.createElement("div"); win.className = "widget"; - win.style.width = (saved && saved.w ? saved.w : def.w)+"px"; - win.style.height = (saved && saved.h ? saved.h : def.h)+"px"; + const size = clampWidgetSize(saved && saved.w ? saved.w : def.w, saved && saved.h ? saved.h : def.h, def); + win.style.width = size.w+"px"; + win.style.height = size.h+"px"; if(def.minW) win.style.minWidth = def.minW+"px"; if(def.minH) win.style.minHeight = def.minH+"px"; - win.style.left = (saved && saved.x!=null ? saved.x : 40 + (widgetCount*36)%240) + "px"; - win.style.top = (saved && saved.y!=null ? saved.y : 30 + (widgetCount*30)%160) + "px"; + const cx = saved && saved.x!=null ? saved.x : 40 + (widgetCount*36)%240; + 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; widgetCount++; if(saved && saved.bare){ win.classList.add("bare"); win.dataset.bare = "1"; } @@ -61,6 +72,27 @@ function makeWidget(def, saved){ body.className = "widget-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; function setMin(m){ if(m && !pill){