From 6f4a315947334e4f2d7b9e5ede836bb4467d3970 Mon Sep 17 00:00:00 2001 From: Jon Vanvik Date: Tue, 23 Jun 2026 16:33:07 +0200 Subject: [PATCH] mobile: fix edge beam (transform-rotate) + ONU master-detail layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edge glow: iOS Safari won't animate an @property-driven conic-gradient angle, so the beam sat static / off the border on mobile. Reworked to the robust technique: a .beam child masked to the border ring contains an oversized conic-gradient ::before spun with transform: rotate 0→360deg; the glow halo is a drop-shadow on the parent (survives the child mask). Works on iOS/Safari; honors prefers-reduced-motion. ONU info on narrow screens: was stacking the picker above the detail, so selecting an ONU meant a long scroll to find it. Now master-detail — show the picker OR the detail (not both), toggled by .show-detail on #view-onu-info via a shared @media(max-width:860px) rule; selecting shows the detail with a "← ONU list" back button, tab/sub-tab nav resets to the picker. (Earlier commit already de-compacted the list: extra filters + two-line rows.) Docs: CLAUDE.md §15 (edge glow technique) + §17 (master-detail). Verified offscreen at 414px: beam element present; panel-left/​panel-main display toggles correctly on select/back; back button visible on mobile; rendered both states. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 22 +++++++++------ renderer/app.css | 69 ++++++++++++++++++++++++++++++++------------- renderer/app.js | 19 +++++++++++-- renderer/index.html | 2 +- 4 files changed, 81 insertions(+), 31 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3b7d493..3d62982 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -779,13 +779,15 @@ port of the iOS `Format.bps`. ### Animated edge glow -`#edge-glow` is a fixed, pointer-events-none full-viewport element. A -`conic-gradient(from var(--beam-angle), …)` with a white-hot core is masked -to just the 3px border ring (the standard `mask` + `mask-composite: -exclude` border trick) and animated by spinning the `@property ---beam-angle` 0→360°, so a neon beam travels around the screen edge. -Respects `prefers-reduced-motion`. z-index sits below the scanline overlay -(9999) and modals (10000). +`#edge-glow` (fixed, pointer-events-none, full-viewport) holds a `.beam` +child masked to a 3px border ring (`mask` + `mask-composite: exclude`); the +ring contains a `::before` that is an oversized `conic-gradient` with a +white-hot core, spun with **`transform: rotate` 0→360°**, so the beam travels +around the screen edge. (It deliberately does *not* use an `@property`- +animated gradient angle — iOS Safari won't animate that, which left the beam +static on mobile.) The glow halo is a `drop-shadow` on the parent so it +survives the child's mask. Respects `prefers-reduced-motion`; z-index sits +below the scanline overlay (9999) and modals (10000). --- @@ -874,7 +876,11 @@ deep-link here via `openOnuInfo`. The left list/search supports name/address/serial + equipment/version + PON-mode + status filters; rows are two-line (serial+status, then equipment · version · last-seen). Parent OLT and upstream switch are one -combined card. +combined card. On narrow screens it's **master-detail**: the picker and the +detail don't stack (which buried the detail under a long list) — selecting an +ONU swaps to the detail (`.show-detail` on `#view-onu-info`, via a +`@media (max-width:860px)` rule in the shared CSS) with a "← ONU list" back +button; navigating in via the tab/sub-tab resets to the picker. ### Single-ONU upgrade diff --git a/renderer/app.css b/renderer/app.css index b42f6b8..269d273 100644 --- a/renderer/app.css +++ b/renderer/app.css @@ -416,39 +416,55 @@ button.danger:hover:not(:disabled) { .modal-type-label { display: block; } .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 2px; } -/* -------- Animated neon edge glow (beam travels around the border) -------- */ -@property --beam-angle { - syntax: ""; - initial-value: 0deg; - inherits: false; -} +/* -------- Animated neon edge glow (beam travels around the border) -------- + A rotating conic-gradient (transform: rotate — works on iOS Safari, unlike + an @property-animated gradient angle) clipped to a thin border ring. The + glow halo is a drop-shadow on the parent so it survives the child's mask. */ #edge-glow { position: fixed; inset: 0; z-index: 9998; pointer-events: none; + filter: drop-shadow(0 0 6px var(--neon)) drop-shadow(0 0 16px var(--cyan)); +} +#edge-glow .beam { + position: absolute; + inset: 0; padding: 3px; /* thickness of the glowing border band */ - background: conic-gradient( - from var(--beam-angle), - transparent 0deg, - transparent 38deg, - var(--cyan) 64deg, - #ffffff 90deg, - var(--neon) 116deg, - transparent 142deg, - transparent 360deg - ); + overflow: hidden; /* Keep only the padding ring (border band); punch out the interior. */ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; - filter: drop-shadow(0 0 8px var(--neon)) drop-shadow(0 0 22px var(--cyan)); - animation: beamspin 7s linear infinite; } -@keyframes beamspin { to { --beam-angle: 360deg; } } +#edge-glow .beam::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 200vmax; /* > viewport diagonal at any rotation */ + height: 200vmax; + transform: translate(-50%, -50%); + background: conic-gradient( + from 0deg, + transparent 0deg, + transparent 40deg, + var(--cyan) 66deg, + #ffffff 90deg, + var(--neon) 114deg, + transparent 140deg, + transparent 360deg + ); + animation: beamspin 7s linear infinite; + will-change: transform; +} +@keyframes beamspin { + from { transform: translate(-50%, -50%) rotate(0deg); } + to { transform: translate(-50%, -50%) rotate(360deg); } +} @media (prefers-reduced-motion: reduce) { - #edge-glow { animation: none; } + #edge-glow .beam::before { animation: none; } } /* -------- Top tabs -------- */ @@ -562,6 +578,19 @@ svg.spark { vertical-align: middle; opacity: 0.9; } .olt-modal { width: min(960px, 95vw); } .olt-modal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; } .olt-onu-list { max-height: 42vh; margin-top: 6px; } + +/* "Back to list" button only matters in the narrow master-detail layout. */ +.onu-detail-back { display: none; align-self: flex-start; } + +/* Narrow screens: the ONU info page is master-detail — show the picker OR + the selected ONU's detail, never both stacked (which buried the detail + under a long list). `.show-detail` is toggled in app.js on select / back. */ +@media (max-width: 860px) { + #view-onu-info .panel-main { display: none; } + #view-onu-info.show-detail .panel-left { display: none; } + #view-onu-info.show-detail .panel-main { display: flex; flex-direction: column; } + #view-onu-info .onu-detail-back { display: inline-block; } +} .detail-card { background: var(--bg-elev); border: 1px solid var(--border); diff --git a/renderer/app.js b/renderer/app.js index 11e0155..9d0eb24 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -261,6 +261,7 @@ for (const tab of $$('.tab')) { showView(view); // Dashboard loads itself on first visit; other tabs load on demand. if (view === '#view-dashboard' && !state.dashboard) loadDashboard(); + if (view === '#view-onu-info') $('#view-onu-info').classList.remove('show-detail'); }); } @@ -1857,10 +1858,22 @@ for (const st of $$('.subtab')) { st.addEventListener('click', () => { const view = st.dataset.subview; showView(view); - if (view === '#view-onu-info' && state.fleet.length) renderOnuInfoList(); + if (view === '#view-onu-info') { + $('#view-onu-info').classList.remove('show-detail'); // land on the picker + if (state.fleet.length) renderOnuInfoList(); + } }); } +// "Back to list" in the narrow master-detail layout (delegated — the detail +// re-renders several times). +$('#onu-info-detail').addEventListener('click', (e) => { + if (e.target.closest('.onu-detail-back')) { + e.preventDefault(); + $('#view-onu-info').classList.remove('show-detail'); + } +}); + // -------- ONU info / stats page -------- $('#onu-info-load').addEventListener('click', async () => { @@ -1937,6 +1950,8 @@ function renderOnuInfoList() { function selectOnuInfo(onuId) { state.onuInfoSelected = onuId; + // Narrow layout: switch from the picker to the detail (master-detail). + $('#view-onu-info').classList.add('show-detail'); for (const row of $$('#onu-info-list .onu-info-row')) { row.classList.toggle('selected', row.dataset.id === onuId); } @@ -1994,7 +2009,7 @@ async function renderOnuDetail(onuId) { const cfg = state.fleet.find((c) => c._id === onuId); if (!cfg) { el.innerHTML = '

ONU not found in the loaded fleet — reload the fleet.

'; return; } - const head = `

${escapeHtml(cfg._id)}

`; + const head = `

${escapeHtml(cfg._id)}

`; // Identity + link render instantly from the loaded fleet; the rest streams // in from one getOnuDetail call. el.innerHTML = `${head}
diff --git a/renderer/index.html b/renderer/index.html index fd55a93..6e9fa67 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -7,7 +7,7 @@ - +
PON Fleet Upgrader