zebra-spaces: hide-panel toggle + camera PiP over screen — full-bleed live view

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 <html>
   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.
This commit is contained in:
Russell Ballestrini 2026-06-02 08:57:14 -04:00
parent 8c90379006
commit 6dfa148612
No known key found for this signature in database

View file

@ -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 <html> 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(_){}
</script>
</head>
<body>
<button id="btn-theme" class="theme-toggle" type="button" aria-label="toggle dark mode">dark</button>
<button id="btn-controls-toggle" class="controls-toggle" type="button" aria-label="hide side panel" title="hide right side panel">hide panel</button>
<div class="page">
@ -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 <html> pref onto the
* .page element, then keeps both in sync on click. Pre-paint class on
* <html> 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');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">df76b79d099ed55cab1336afdf62e8a8</span><br>
sha256 <span class="stamp-sha">f2a8f4316571f5accc3ed4b6be564ab31328c732323f2f64ce62675d3de1701c</span><br>
md5 <span class="stamp-md5">016e64ea183b4be1c9803669e0558045</span><br>
sha256 <span class="stamp-sha">9ad330fb516bb1e632b1f66511fb840cdcbd2cfc14390101fcb1bb5c362c978b</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>