From 6dfa148612380f15af2538659dd8c382a47de3bd Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 2 Jun 2026 08:57:14 -0400 Subject: [PATCH] =?UTF-8?q?zebra-spaces:=20hide-panel=20toggle=20+=20camer?= =?UTF-8?q?a=20PiP=20over=20screen=20=E2=80=94=20full-bleed=20live=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX problem in the screenshot fox sent: at full window with 1080p screen share, the right controls column ate 360px every viewer would rather spend on pixels, and the camera tiles rendered BELOW the screen so they fell off-screen mid-stream. Two fixes: 1. 'hide panel' button next to the theme toggle. Click collapses the right column to zero, screen-share fills the whole viewport width. Persists to zebra-spaces-controls-v1; applied pre-paint on so a 'hidden' reload doesn't flash the panel before hiding it. 2. Cameras get out of the way when a screen share is active. CSS :has() detects sec-screens visible and promotes #sec-cameras to absolute positioning, bottom-right of the timeline column, ~22% width, max 70vh, dark backdrop. Standard Zoom/Meet PiP placement. Multiple cameras stack vertically inside the strip (single-column grid in PiP mode). When nobody is sharing screen, cameras render normally in their multi-column grid below the timeline iframe. --- web/zebra-spaces.html | 83 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 6a5da35..64d7ae3 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -29,12 +29,14 @@ /* buttons + inputs */ html.theme-dark button, html.theme-dark .game-tabs button, - html.theme-dark .theme-toggle { + html.theme-dark .theme-toggle, + html.theme-dark .controls-toggle { background: #000; color: #ccc; border-color: #444; } html.theme-dark button:hover:not(:disabled), html.theme-dark .game-tabs button:hover:not(.active), - html.theme-dark .theme-toggle:hover { background: #1a1a1a; } + html.theme-dark .theme-toggle:hover, + html.theme-dark .controls-toggle:hover { background: #1a1a1a; } html.theme-dark button.invert { background: #ccc; color: #000; border-color: #ccc; } html.theme-dark button.invert:hover:not(:disabled) { background: #999; } html.theme-dark .game-tabs button.active { background: #ccc; color: #000; border-color: #ccc; } @@ -83,13 +85,15 @@ html.theme-dark footer span { color: #555 !important; } html.theme-dark footer a { color: #777 !important; } - .theme-toggle { - position: fixed; top: 0.6rem; right: 0.8rem; z-index: 9999; + .theme-toggle, .controls-toggle { + position: fixed; top: 0.6rem; z-index: 9999; background: #fff; color: #000; border: 1px solid #000; padding: 0.2rem 0.55rem; font-family: monospace; font-size: 0.7rem; cursor: pointer; line-height: 1; } - .theme-toggle:hover { background: #f0f0f0; } + .theme-toggle { right: 0.8rem; } + .controls-toggle { right: 4.3rem; } + .theme-toggle:hover, .controls-toggle:hover { background: #f0f0f0; } /* two-column page on desktop: left = timeline (1fr, takes remaining * space — hosts screen shares + the running event feed), right = * controls (kept narrow so the screen share gets maximum pixels). @@ -101,9 +105,22 @@ gap: 1.25rem; align-items: start; } + /* collapsed: right column folds away so the screen share fills the + * full viewport width. The timeline column's right border + padding + * collapse with it. */ + .page.controls-collapsed { + grid-template-columns: minmax(0, 1fr); + gap: 0; + } + .page.controls-collapsed > .controls { display: none; } + .page.controls-collapsed > .timeline { border-right: none; padding-right: 0; } + /* honour the pre-paint pref so first-paint matches saved state */ + html.controls-collapsed-pref .page { grid-template-columns: minmax(0, 1fr); gap: 0; } + html.controls-collapsed-pref .page > .controls { display: none; } + html.controls-collapsed-pref .page > .timeline { border-right: none; padding-right: 0; } .timeline { border-right: 1px solid #ddd; padding-right: 1.25rem; min-height: 60vh; - min-width: 0; display: flex; flex-direction: column; + min-width: 0; display: flex; flex-direction: column; position: relative; } .controls { min-width: 0; } .game-tabs { display: flex; gap: 0.4rem; margin-bottom: 0.5rem; flex-wrap: wrap; } @@ -185,6 +202,26 @@ } .camera-tile.needs-tap .tap-play { display: flex; } + /* PiP mode: when a screen is being shared, cameras get out of the way — + * they float as a small vertical strip in the bottom-right of the + * timeline column, layered over the screen tile. Standard Zoom/Meet + * pattern. Scrolls vertically if there are more than fit. The screen + * keeps the full timeline width so 1080p screen-share stays huge. */ + .timeline:has(> #sec-screens:not(.hidden)) > #sec-cameras { + position: absolute; right: 0.6rem; bottom: 0.6rem; + width: clamp(180px, 22%, 320px); + max-height: 70vh; overflow-y: auto; + z-index: 50; padding: 0.35rem; + background: rgba(0,0,0,0.55); + border-radius: 4px; + } + .timeline:has(> #sec-screens:not(.hidden)) > #sec-cameras > h2 { + color: #ddd; font-size: 0.75rem; margin: 0 0 0.3rem; border-bottom: none; + } + .timeline:has(> #sec-screens:not(.hidden)) #cameras { grid-template-columns: 1fr; gap: 0.3rem; } + .timeline:has(> #sec-screens:not(.hidden)) .camera-tile video { max-height: 16vh; } + .timeline:has(> #sec-screens:not(.hidden)) .camera-tile .screen-meta { font-size: 0.6rem; padding: 0.15rem 0.3rem; } + /* latency panel: one row per live PC, fixed-width columns so the numbers * line up vertically as values bounce. lat-good < 50ms, mid < 150ms, * bad ≥ 150ms — colour-tagged so the user can scan at a glance. */ @@ -319,10 +356,18 @@ try { document.documentElement.classList.add('theme-dark'); } } catch(_){ document.documentElement.classList.add('theme-dark'); } +/* sidebar-collapsed pre-paint: apply to so the .page child can + * read it via :has() or via a body-level class added on DOMContentLoaded */ +try { + if (localStorage.getItem('zebra-spaces-controls-v1') === 'hidden'){ + document.documentElement.classList.add('controls-collapsed-pref'); + } +} catch(_){} +
@@ -538,6 +583,28 @@ $('btn-theme').addEventListener('click', () => { try { localStorage.setItem(THEME_KEY, goDark ? 'dark' : 'light'); } catch(_){} applyThemeLabel(); }); + +/* controls-panel collapse — promotes the pre-paint pref onto the + * .page element, then keeps both in sync on click. Pre-paint class on + * stays so a fresh load with a saved 'hidden' pref still shows + * the layout collapsed instantly. */ +const CONTROLS_KEY = 'zebra-spaces-controls-v1'; +function applyControlsLabel(){ + const hidden = document.querySelector('.page').classList.contains('controls-collapsed'); + const btn = $('btn-controls-toggle'); if (btn) btn.textContent = hidden ? 'show panel' : 'hide panel'; +} +if (document.documentElement.classList.contains('controls-collapsed-pref')){ + document.querySelector('.page').classList.add('controls-collapsed'); +} +applyControlsLabel(); +$('btn-controls-toggle').addEventListener('click', () => { + const page = document.querySelector('.page'); + const goHidden = !page.classList.contains('controls-collapsed'); + page.classList.toggle('controls-collapsed', goHidden); + document.documentElement.classList.toggle('controls-collapsed-pref', goHidden); + try { localStorage.setItem(CONTROLS_KEY, goHidden ? 'hidden' : 'shown'); } catch(_){} + applyControlsLabel(); +}); const VAULT_PREFIX = 'zspc-id-v1|'; const PBKDF2_ITER = 600000; @@ -2183,8 +2250,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');