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:
parent
8c90379006
commit
6dfa148612
1 changed files with 75 additions and 8 deletions
|
|
@ -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> · 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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue