From f3a2fe7a055ef6ade7204bb13f95add53d4fe996 Mon Sep 17 00:00:00 2001 From: Louis King Date: Sat, 4 Jul 2026 10:38:06 +0100 Subject: [PATCH 1/2] fix(web): 2x2 dashboard chart grid on mobile landscape Charts jumped from a single column straight to N-across at the md breakpoint, cramming them on landscape phones. Introduce a 2-column layout at the sm breakpoint (2x2 for 4 charts; 3 charts flow to 2-on-top + 1 bottom-left with a gap on the right) and defer the full N-across row to lg. Co-Authored-By: Claude Opus 4.8 --- src/meshcore_hub/web/static/js/spa/pages/dashboard.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/meshcore_hub/web/static/js/spa/pages/dashboard.js b/src/meshcore_hub/web/static/js/spa/pages/dashboard.js index 5e6110a..472a815 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/dashboard.js +++ b/src/meshcore_hub/web/static/js/spa/pages/dashboard.js @@ -111,11 +111,11 @@ function renderChannelMessages(channelMessages, channelLabels) { `; } -/** Return a Tailwind grid-cols class for the given visible column count. */ +/** Return responsive Tailwind grid-cols classes for the given visible column count. */ function gridCols(count) { - if (count === 2) return 'md:grid-cols-2'; - if (count === 3) return 'md:grid-cols-3'; - if (count === 4) return 'md:grid-cols-4'; + if (count === 2) return 'sm:grid-cols-2'; + if (count === 3) return 'sm:grid-cols-2 lg:grid-cols-3'; + if (count === 4) return 'sm:grid-cols-2 lg:grid-cols-4'; return ''; } From 5dbf0697cee5e48a5bfbac48f34a4f4698b6155f Mon Sep 17 00:00:00 2001 From: Louis King Date: Sat, 4 Jul 2026 10:38:08 +0100 Subject: [PATCH 2/2] fix(web): path-node popover follows page scroll The popover used position: fixed with viewport-relative coordinates, so it stayed pinned to the viewport while the page scrolled away beneath it. Switch to absolute positioning anchored to the document (adding scrollX/scrollY) and read the badge's live rect on each reposition so it tracks the anchor after the async node fetch resolves. Co-Authored-By: Claude Opus 4.8 --- .../static/js/spa/pages/packet-group-detail.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js b/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js index 1044c48..2bb76f2 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js +++ b/src/meshcore_hub/web/static/js/spa/pages/packet-group-detail.js @@ -126,8 +126,9 @@ export async function render(container, params, router) { return tagName || n.name || truncateKey(n.public_key, 12); } - function positionPopover(rect) { + function positionPopover(badgeEl) { if (!popoverEl) return; + const rect = badgeEl.getBoundingClientRect(); const margin = 8; const pw = popoverEl.offsetWidth || 256; const ph = popoverEl.offsetHeight || 0; @@ -137,8 +138,9 @@ export async function render(container, params, router) { if (top + ph + margin > window.innerHeight && rect.top - ph - 4 > margin) { top = rect.top - ph - 4; } - popoverEl.style.left = `${left}px`; - popoverEl.style.top = `${top}px`; + // Anchor to the document (position: absolute) so the popover scrolls with the page. + popoverEl.style.left = `${left + window.scrollX}px`; + popoverEl.style.top = `${top + window.scrollY}px`; } function popoverShell(hashLabel, body) { @@ -153,15 +155,15 @@ export async function render(container, params, router) { async function openPathPopover(e, ph) { e.preventDefault(); e.stopPropagation(); - const rect = e.currentTarget.getBoundingClientRect(); + const badgeEl = e.currentTarget; closePopover(); popoverEl = document.createElement('div'); - popoverEl.className = 'path-node-popover fixed z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300'; + popoverEl.className = 'path-node-popover absolute z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300'; document.body.appendChild(popoverEl); litRender(popoverShell(ph, html`
${loading()}
`), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); const onDocClick = (ev) => { if (popoverEl && !popoverEl.contains(ev.target)) closePopover(); }; const onKey = (ev) => { if (ev.key === 'Escape') closePopover(); }; @@ -194,11 +196,11 @@ export async function render(container, params, router) { ` : nothing} `; litRender(popoverShell(ph, body), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); } catch (err) { if (isAbortError(err) || !popoverEl) return; litRender(popoverShell(ph, html`
${warningBadge(err.message)}
`), popoverEl); - positionPopover(rect); + positionPopover(badgeEl); } }