diff --git a/ui/src/screens/Lobby.svelte b/ui/src/screens/Lobby.svelte index 6ba4fbe..6189512 100644 --- a/ui/src/screens/Lobby.svelte +++ b/ui/src/screens/Lobby.svelte @@ -4,6 +4,7 @@ import Screen from '../components/Screen.svelte'; import TabBar from '../components/TabBar.svelte'; import Modal from '../components/Modal.svelte'; + import PinPad from '../components/PinPad.svelte'; import { app, handleError, refreshFeedbackBadge, seedChatUnread } from '../lib/app.svelte'; import { connection } from '../lib/connection.svelte'; import { gateway } from '../lib/gateway'; @@ -186,6 +187,9 @@ // revealed by swiping the row left (touch) or tapping its kebab (any pointer); the action is // per-account and irreversible. Only one row is revealed at a time. let revealedId = $state(null); + // A hotseat game's deletion is gated by the host (master) PIN — both active (terminate the game) + // and finished; pinTarget holds the game id while its PIN pad is open. + let pinTarget = $state(null); let drag: { id: string; x0: number; y0: number } | null = null; // A horizontal swipe must not also count as a tap that opens the game; armed on swipe, // consumed by the next tap, and reset on the next pointerdown so a later tap is never eaten. @@ -196,13 +200,14 @@ if (e.pointerType === 'mouse') return; // desktop reveals via the kebab, not a swipe drag = { id, x0: e.clientX, y0: e.clientY }; } - function onRowUp(e: PointerEvent, finished: boolean): void { + function onRowUp(e: PointerEvent, revealable: boolean): void { if (!drag) return; const dx = e.clientX - drag.x0; const dy = e.clientY - drag.y0; - // Only a finished row reveals on a horizontal swipe; that swipe then suppresses the tap so it - // does not also open the game. Active rows ignore swipes and stay plain tap-to-open. - if (finished && Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.4) { + // Only a revealable row (a finished game, or any hotseat game) reveals its delete on a horizontal + // swipe; that swipe then suppresses the tap so it does not also open the game. Other active rows + // ignore swipes and stay plain tap-to-open. + if (revealable && Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.4) { swiped = true; revealedId = dx < 0 ? drag.id : null; } @@ -240,6 +245,14 @@ } } + // startDelete routes the row's delete: a hotseat game (active or finished) is gated behind the host + // master PIN (so the last mover cannot instantly wipe the game); any other finished game deletes at + // once. + function startDelete(g: GameView): void { + if (g.hotseat) pinTarget = g.id; + else void hide(g.id); + } + async function acceptInvite(inv: Invitation) { try { const r = await gateway.invitationAccept(inv.id); @@ -315,15 +328,15 @@
{#each group.list as g (g.id)} {@const badge = badgeKind(app.chatUnread[g.id] ?? false, app.messageUnread[g.id] ?? false)} -
- {#if group.finished} - +
+ {#if group.finished || g.hotseat} + {/if}
- {#if group.finished} + {#if group.finished || g.hotseat} {:else} + {#if pinTarget} + localSource.verifyHostPin(pinTarget ?? '', p)} + onclose={() => (pinTarget = null)} + onresult={() => { + const id = pinTarget; + pinTarget = null; + if (id) void hide(id); + }} + /> + {/if} + {#snippet tabbar()}