diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 28430e6..67b32ac 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -176,26 +176,44 @@
position: relative;
}
.controls { min-width: 0; }
- /* games live in the LEFT column now (under cameras + screens-thumbs)
- * so people can play casually during a meeting without competing
- * with screen-shares for the middle slot. The tabs wrap if the
- * column is narrow; the iframe is sized for the column. */
+ /* games live in the LEFT column under cameras + screens-thumbs as
+ * proper tile-thumbs. Click one to spotlight it in the middle (an
+ * iframe loads at full size); the click also broadcasts the spotlight
+ * choice through the same channel cameras/screens use, so the log
+ * shows 'alice now viewing unmario' and the thumb-order popularity
+ * sort knows people are watching. */
.games-h2 { margin-top: 0.5rem; }
- .game-tabs {
- display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: row;
- gap: 0.25rem; margin-bottom: 0.35rem;
+ #games-thumbs { display: grid; grid-template-columns: 1fr; gap: 0.45rem; }
+ .game-thumb {
+ border: 1px solid #ddd; background: #fff; padding: 0;
+ display: grid; grid-template-columns: 1fr; gap: 0;
+ cursor: pointer; position: relative;
+ aspect-ratio: 16 / 9;
}
- .game-tabs button {
- background: #fff; color: #000; border: 1px solid #000;
- padding: 0.25rem 0.5rem; font-family: monospace; font-size: 0.72rem;
- cursor: pointer; text-align: left;
+ .game-thumb:hover { outline: 1px solid #888; }
+ .game-thumb .game-label {
+ display: grid; place-items: center; text-align: center;
+ font-family: monospace; font-size: 0.85rem; padding: 0.5rem;
+ background: #f0f0f0; color: #333;
}
- .game-tabs button.active { background: #000; color: #fff; }
- .game-tabs button:hover:not(.active) { background: #f0f0f0; }
- .game-tile-frame {
- width: 100%; aspect-ratio: 4 / 3;
- border: 1px solid #ddd; background: #fff; display: block;
+ /* spotlit game shows the iframe at full size in the middle column */
+ #spotlight > .game-tile {
+ border: 1px solid #ddd; background: #fff;
+ display: grid; grid-template-rows: 1fr auto;
}
+ #spotlight > .game-tile iframe {
+ width: 100%; height: 80vh;
+ border: none; background: #fff; display: block;
+ }
+ #spotlight > .game-tile .screen-meta {
+ background: #f0f0f0; color: #333; font-size: 0.7rem; padding: 0.3rem 0.5rem;
+ display: grid; grid-template-columns: 1fr auto; align-items: center;
+ }
+ html.theme-dark .game-thumb { background: #000; border-color: #000; }
+ html.theme-dark .game-thumb .game-label { background: #111; color: #ddd; }
+ html.theme-dark #spotlight > .game-tile { background: #000; border-color: #000; }
+ html.theme-dark #spotlight > .game-tile iframe { background: #000; }
+ html.theme-dark #spotlight > .game-tile .screen-meta { background: #111; color: #ddd; }
@media (max-width: 800px) {
body { padding: 1.25rem; }
/* mobile: single column, all three sections stack to grid-column:1 */
@@ -513,17 +531,7 @@ try {
screens
games
-
-
-
-
-
+
@@ -1505,11 +1513,50 @@ function memberOf(pubHex){
}
return null;
}
+/* games are tiles too. The 'pubHex' for a game is its short id below;
+ * spotlight broadcasts use 'game:' as the key, so the room sees
+ * 'alice now viewing unmario' in the log just like 'alice now viewing
+ * bob's screen'. */
+const GAMES = {
+ unmario: { label: 'unmario', src: 'https://unmario.com/' },
+ cake: { label: 'cake murder adventure', src: 'https://cuppcb.com/games/cake-murder-adventure/' },
+};
function ownerLabel(kind, pubHex){
+ if (kind === 'game'){
+ const g = GAMES[pubHex];
+ return g ? g.label : pubHex;
+ }
const m = memberOf(pubHex);
const handle = m ? (m.mm.handle || shortHex(pubHex)) : shortHex(pubHex);
return handle + "'s " + (TILE_KINDS[kind] ? TILE_KINDS[kind].labelPrefix : kind);
}
+/* game thumbs live in #games-thumbs and click → setSpotlight('game', id) */
+function buildGameThumb(id){
+ const g = GAMES[id]; if (!g) return null;
+ const tile = document.createElement('div');
+ tile.className = 'game-thumb';
+ tile.dataset.pubHex = id;
+ tile.dataset.kind = 'game';
+ const label = document.createElement('div');
+ label.className = 'game-label';
+ label.textContent = g.label;
+ tile.appendChild(label);
+ const view = document.createElement('div'); view.className = 'viewing-badge'; view.textContent = 'viewing';
+ tile.appendChild(view);
+ tile.addEventListener('click', () => {
+ if (spotlight && spotlight.kind === 'game' && spotlight.pubHex === id) return;
+ setSpotlight('game', id);
+ });
+ return tile;
+}
+function renderGamesColumn(){
+ const c = $('games-thumbs'); if (!c) return;
+ c.innerHTML = '';
+ for (const id of Object.keys(GAMES)){
+ const t = buildGameThumb(id);
+ if (t) c.appendChild(t);
+ }
+}
/* tile score = role-based boost on the OWNER + count of current viewers.
* Host always at top, co-hosts second, then speakers sorted by viewers.
* Listeners can't publish so they never appear. */
@@ -1605,24 +1652,52 @@ function buildTile(kind, pubHex, label, opts){
return { tile, video, fsBtn };
}
-function setSpotlight(kind, pubHex){
- const entry = getEntry(kind, pubHex);
- if (!entry){ if (spotlight) clearSpotlightDOM(); spotlight = null; updateContainerVisibility(); return; }
- /* unmark the previous spotlight's thumb, tear its big tile DOM */
- if (spotlight){
- const prev = getEntry(spotlight.kind, spotlight.pubHex);
- if (prev) prev.tile.classList.remove('viewing');
- clearSpotlightDOM();
+/* find a thumb element by kind+pubHex regardless of whether it's a
+ * camera/screen tile or a game card. Returned element gets the
+ * 'viewing' class toggle so the thumb→spotlight mapping is obvious. */
+function thumbElementFor(kind, pubHex){
+ if (kind === 'game'){
+ return document.querySelector('.game-thumb[data-pub-hex="'+pubHex+'"]');
+ }
+ const e = getEntry(kind, pubHex);
+ return e ? e.tile : null;
+}
+
+function setSpotlight(kind, pubHex){
+ /* unmark previous spotlight's thumb + tear its big tile */
+ if (spotlight){
+ const prevThumb = thumbElementFor(spotlight.kind, spotlight.pubHex);
+ if (prevThumb) prevThumb.classList.remove('viewing');
+ clearSpotlightDOM();
+ spotlight = null;
+ }
+ if (kind === 'game'){
+ const g = GAMES[pubHex]; if (!g){ updateContainerVisibility(); return; }
+ const big = document.createElement('div'); big.className = 'game-tile';
+ const iframe = document.createElement('iframe');
+ iframe.src = g.src;
+ iframe.title = g.label;
+ iframe.setAttribute('referrerpolicy', 'no-referrer');
+ iframe.loading = 'eager';
+ const meta = document.createElement('div'); meta.className = 'screen-meta';
+ const who = document.createElement('span'); who.textContent = 'game: ' + g.label;
+ meta.appendChild(who);
+ big.appendChild(iframe); big.appendChild(meta);
+ $('spotlight').appendChild(big);
+ const thumb = thumbElementFor('game', pubHex);
+ if (thumb) thumb.classList.add('viewing');
+ spotlight = { kind, pubHex, tile: big, video: null };
+ } else {
+ const entry = getEntry(kind, pubHex);
+ if (!entry){ updateContainerVisibility(); return; }
+ const big = buildTile(kind, pubHex, entry.label, { thumb: false });
+ big.video.srcObject = entry.video.srcObject;
+ try { const p = big.video.play(); if (p && p.catch) p.catch(()=>{ big.tile.classList.add('needs-tap'); }); } catch(_){}
+ $('spotlight').appendChild(big.tile);
+ entry.tile.classList.add('viewing');
+ spotlight = { kind, pubHex, tile: big.tile, video: big.video };
}
- /* build a fresh big tile for the new selection, hand it the same stream */
- const big = buildTile(kind, pubHex, entry.label, { thumb: false });
- big.video.srcObject = entry.video.srcObject;
- try { const p = big.video.play(); if (p && p.catch) p.catch(()=>{ big.tile.classList.add('needs-tap'); }); } catch(_){}
- $('spotlight').appendChild(big.tile);
- entry.tile.classList.add('viewing');
- spotlight = { kind, pubHex, tile: big.tile, video: big.video };
updateContainerVisibility();
- /* track our own choice, broadcast it, log it, re-sort */
if (myUUID){
spotlights.set(myUUID, spotlightKey());
logSpotlightChange(myUUID, spotlightKey());
@@ -3214,15 +3289,9 @@ $('btn-copy-share').addEventListener('click', async () => {
setTimeout(() => { joinSpace().catch(e => logLine('err', 'auto-rejoin failed: ' + e.message)); }, 100);
})();
-/* game-tabs: click a button → swap iframe src + mark active. data-src is
- * the only source of truth so new games just need a button. */
-$('game-tabs').addEventListener('click', (ev) => {
- const b = ev.target.closest('button[data-src]');
- if (!b) return;
- const frame = $('game-frame');
- if (frame.src !== b.dataset.src) frame.src = b.dataset.src;
- for (const el of $('game-tabs').querySelectorAll('button')) el.classList.toggle('active', el === b);
-});
+/* games as left-column tiles — click to spotlight, broadcast through
+ * the same channel cameras/screens use so the room sees the choice */
+renderGamesColumn();
$('btn-leave').addEventListener('click', async () => {
wantConnected = false;
roomMachines.call.send('LEAVE');
@@ -3262,8 +3331,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');