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 {

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');