diff --git a/ui/e2e/edgeswipe.spec.ts b/ui/e2e/edgeswipe.spec.ts index fb61ac6..d2e5a8f 100644 --- a/ui/e2e/edgeswipe.spec.ts +++ b/ui/e2e/edgeswipe.spec.ts @@ -40,6 +40,17 @@ test('a rightward swipe in the left band returns to the lobby', async ({ page }) await expect(page.getByRole('button', { name: /Ann/ })).toBeVisible(); }); +test('a rightward swipe from the left half (past the old edge band) returns to the lobby', async ({ page }) => { + await openAnnGame(page); + const board = (await page.locator('.viewport').boundingBox())!; + + // ~38% of the 390px viewport: inside the new half-width band, outside the old 20% edge band. + await touchDrag(page, 150, board.y + board.height * 0.5, 320, board.y + board.height * 0.5); + + await expect(page).toHaveURL(/#\/$/); + await expect(page.getByRole('button', { name: /Ann/ })).toBeVisible(); +}); + test('a swipe starting on the rack does not navigate (hit-test guard)', async ({ page }) => { await openAnnGame(page); const rack = (await page.locator('[data-rack]').boundingBox())!; diff --git a/ui/e2e/history.spec.ts b/ui/e2e/history.spec.ts index e4b1dcd..53b19dc 100644 --- a/ui/e2e/history.spec.ts +++ b/ui/e2e/history.spec.ts @@ -58,6 +58,46 @@ test('the history is a per-seat grid with move scores but no names or running to await expect(grid).not.toContainText('Ann'); }); +test('a two-finger pinch does not open the history (pinch-zoom vs swipe-down)', async ({ page }) => { + await openGame(page); + await page.locator('.stage').evaluate((el) => (el.scrollTop = 0)); + const box = (await page.locator('.boardwrap').boundingBox())!; + const cx = box.x + box.width / 2; + const y0 = box.y + 40; + + // Two fingers land, then spread apart while their centroid drifts down — a pinch-out whose + // downward component used to also slide the history open. With the single-pointer guard the + // second finger disarms the pull, so the history stays closed. + await page.locator('.boardwrap').evaluate( + (el, { cx, y0 }) => { + const fire = (type: string, id: number, x: number, y: number) => + el.dispatchEvent( + new PointerEvent(type, { + pointerType: 'touch', + pointerId: id, + isPrimary: id === 1, + clientX: x, + clientY: y, + bubbles: true, + cancelable: true, + }), + ); + fire('pointerdown', 1, cx - 20, y0); + fire('pointerdown', 2, cx + 20, y0); + for (let i = 1; i <= 8; i++) { + const d = i * 18; + fire('pointermove', 1, cx - 20 - d, y0 + d); + fire('pointermove', 2, cx + 20 + d, y0 + d); + } + fire('pointerup', 1, cx - 164, y0 + 144); + fire('pointerup', 2, cx + 164, y0 + 144); + }, + { cx, y0 }, + ); + + await expect(page.locator('.history')).toHaveCount(0); +}); + test('a swipe-down on the zoomed-in board does not open the history (native scroll wins)', async ({ page }) => { await openGame(page); // Double-tap an empty cell to zoom in (two synchronous clicks = a double-tap). diff --git a/ui/src/components/Screen.svelte b/ui/src/components/Screen.svelte index ea3b74e..444c6bd 100644 --- a/ui/src/components/Screen.svelte +++ b/ui/src/components/Screen.svelte @@ -36,11 +36,11 @@ // Edge-swipe back: a rightward drag begun in the left band returns to `back`, the standard // mobile gesture (instant on release — the route slide plays the animation). Listened at the // window in the CAPTURE phase so the board's own pointer handlers (which capture/stop the - // event) can never swallow it; touch/pen only. The band is a fraction of the viewport width - // (EDGE_FRACTION — widen/narrow there). A hit-test keeps the wider band clear of the gestures - // it would otherwise hijack: the rack (tile lift/reorder), a draggable pending tile, a - // zoomed-in board (it pans), and text inputs. - const EDGE_FRACTION = 0.2; + // event) can never swallow it; touch/pen only. The band is the left half of the viewport + // width (EDGE_FRACTION — widen/narrow there). A hit-test keeps the wide band clear of the + // gestures it would otherwise hijack: the rack (tile lift/reorder), a draggable pending tile, + // a zoomed-in board (it pans), and text inputs. + const EDGE_FRACTION = 0.5; const SWIPE_SKIP = '[data-rack], .cell.pending, .viewport.zoomed, input, textarea, select'; $effect(() => { function onDown(e: PointerEvent) { diff --git a/ui/src/game/Chat.svelte b/ui/src/game/Chat.svelte index 985288b..f148140 100644 --- a/ui/src/game/Chat.svelte +++ b/ui/src/game/Chat.svelte @@ -42,7 +42,7 @@ {/if} {#each messages as m (m.id)} {#if m.kind === 'nudge'} -