zebra-spaces: 3-column layout — cameras left, screen middle, controls right

Replaces the PiP overlay with a real 3-column grid: a narrow left
column carrying cameras stacked vertically, the timeline/screen in
the middle, controls on the right.

Layout rules:
- cameras-col is 220px wide on desktop, hidden via .hidden when no
  cameras are live (zero-cost when nobody has a webcam on — keeps
  the original two-column feel for typical mic-only rooms)
- hide-panel still folds away the right column → 2-col cameras+screen
- on <800px viewports all three stack vertically: controls, then
  timeline (or screen), then cameras as a multi-column grid

Camera tiles are now single-column stacked with a 16:9 aspect-ratio
letterbox so face-cams stay readable regardless of incoming resolution.
This commit is contained in:
Russell Ballestrini 2026-06-02 08:58:55 -04:00
parent 6dfa148612
commit af1e5d4b55
No known key found for this signature in database

View file

@ -24,7 +24,8 @@
html.theme-dark a { color: #9cf; }
html.theme-dark .sub, html.theme-dark .sub a, html.theme-dark .note,
html.theme-dark .status-line { color: #888; }
html.theme-dark .timeline { border-right-color: #222; }
html.theme-dark .timeline,
html.theme-dark .cameras-col { border-right-color: #222; }
html.theme-dark .timeline-frame { background: #000; border-color: #222; }
/* buttons + inputs */
html.theme-dark button,
@ -99,23 +100,34 @@
* controls (kept narrow so the screen share gets maximum pixels).
* Both columns get min-width:0 so they shrink without forcing a
* horizontal scrollbar. */
/* three columns: cameras-vertical | screen/timeline | controls.
* The cameras column auto-collapses to width:0 when nobody has a
* camera on (.hidden), so a typical mic-only room reads as the
* original two-column layout. */
.page {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 360px);
grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, 360px);
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. */
.cameras-col {
width: 220px; min-width: 0;
border-right: 1px solid #ddd; padding-right: 1.25rem;
display: flex; flex-direction: column;
}
.cameras-col.hidden { display: none; }
/* collapsed: right column folds away so the screen+cameras layout
* fills the viewport. */
.page.controls-collapsed {
grid-template-columns: minmax(0, 1fr);
gap: 0;
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
gap: 1.25rem;
}
.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 {
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
}
html.controls-collapsed-pref .page > .controls { display: none; }
html.controls-collapsed-pref .page > .timeline { border-right: none; padding-right: 0; }
.timeline {
@ -136,12 +148,16 @@
}
@media (max-width: 800px) {
body { padding: 1.25rem; }
/* stack on mobile, controls first, timeline below */
/* stack on mobile: controls first, then any live screen, cameras last */
.page { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
.controls { order: 1; }
.timeline { order: 2;
border-right: none; border-top: 1px solid #ddd;
padding-right: 0; padding-top: 1rem; min-height: 0; }
.cameras-col { order: 3;
width: 100%; border-right: none; border-top: 1px solid #ddd;
padding-right: 0; padding-top: 1rem; }
#cameras { grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); }
}
/* never let dynamically-appended <audio> sinks (one per remote
* speaker) render their default ~300px control strip — they're just
@ -178,15 +194,15 @@
.timeline:has(> #sec-screens:not(.hidden)) > .game-tabs,
.timeline:has(> #sec-screens:not(.hidden)) > #game-frame { display: none; }
/* camera tiles: smaller, multi-column grid so several face-cams fit
* side-by-side without dwarfing the screen share above them */
#cameras { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.5rem; }
/* camera tiles live in the LEFT column now — single column, stacked
* vertically. Each tile keeps a 16:9 letterbox so faces don't squash. */
#cameras { display: grid; grid-template-columns: 1fr; gap: 0.45rem; }
.camera-tile {
border: 1px solid #000; background: #000; padding: 0;
display: flex; flex-direction: column; position: relative;
}
.camera-tile video {
width: 100%; height: auto; max-height: 40vh; display: block; background: #000;
width: 100%; height: auto; display: block; background: #000;
aspect-ratio: 16 / 9; object-fit: cover;
}
.camera-tile .screen-meta {
@ -202,26 +218,6 @@
}
.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. */
@ -371,20 +367,23 @@ try {
<div class="page">
<!-- left column: cameras stacked vertically so face-cams stay visible
during a screen share without dwarfing the screen itself. Hidden
when nobody has a camera on. -->
<aside id="sec-cameras" class="cameras-col hidden">
<h2>cameras</h2>
<div id="cameras"></div>
</aside>
<main class="timeline">
<!-- shared-screen tiles take priority over the game iframe. When sec-screens
is visible (someone is sharing) the game tabs + iframe get hidden via
CSS so the screen has the full left column. -->
CSS so the screen has the full middle column. -->
<section id="sec-screens" class="hidden">
<h2>screens</h2>
<div id="screens"></div>
</section>
<section id="sec-cameras" class="hidden">
<h2>cameras</h2>
<div id="cameras"></div>
</section>
<!-- placeholder content until the v0.3 event timeline lands: a game switcher
framing an external playable. Iframes load unsandboxed so the game JS can
run; if a target refuses framing (X-Frame-Options / CSP frame-ancestors)
@ -2250,8 +2249,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">016e64ea183b4be1c9803669e0558045</span><br>
sha256 <span class="stamp-sha">9ad330fb516bb1e632b1f66511fb840cdcbd2cfc14390101fcb1bb5c362c978b</span><br>
md5 <span class="stamp-md5">8adf0c5717ac94980b9f3f986f6c947b</span><br>
sha256 <span class="stamp-sha">60d9c9bb2e8c72334e53847efd8f151b972235a2425b39c2302f4fe5c0897fc7</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>