Files
scrabble-game/ui/src/components/Screen.svelte
T
Ilia Denisov fbd67c085c
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 40s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 58s
UI: widen the edge-swipe-back activation band
Replace the very-narrow 24px edge-swipe trigger with a left band of ~20% of the
viewport width (EDGE_FRACTION) so the back-swipe is easy to reach, keeping the
simple instant-navigate behaviour (the route slide plays the animation). A
hit-test keeps the wider band clear of the rack, a draggable pending tile, a
zoomed-in board and text inputs, so it never hijacks those drags.

Test: e2e (Chromium+WebKit) — the band swipe returns to the lobby; a swipe
starting on the rack does not navigate.
2026-06-11 17:13:24 +02:00

100 lines
3.6 KiB
Svelte

<script lang="ts">
import type { Snippet } from 'svelte';
import Header from './Header.svelte';
import AdBanner from './AdBanner.svelte';
import { navigate } from '../lib/router.svelte';
// The app-shell layout (all screens): the nav bar grows; the ad strip, content and
// optional tab bar pin to the bottom (ad directly above the content). Pass `scroll`
// false for screens that own their vertical fit (the game board).
let {
title,
back,
tabbar,
children,
scroll = true,
growNav = false,
column = false,
}: {
title: string;
back?: string;
tabbar?: Snippet;
children?: Snippet;
scroll?: boolean;
growNav?: boolean;
// column lays the content out as a flex column so a child can own the vertical fit
// (the game makes only its board scroll while the score/rack/tab bar stay put).
column?: boolean;
} = $props();
// The promotional banner is feature-gated OFF until it is polished after release. The flag is
// a compile-time `false`, so the {#if} branch — and with it the AdBanner import and its
// banner.ts logic — is dead-code-eliminated from the production bundle. Flip to
// true to bring it back.
const SHOW_AD_BANNER = false;
// 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;
const SWIPE_SKIP = '[data-rack], .cell.pending, .viewport.zoomed, input, textarea, select';
$effect(() => {
function onDown(e: PointerEvent) {
if (!back || e.pointerType === 'mouse' || e.clientX > window.innerWidth * EDGE_FRACTION) return;
if (document.elementFromPoint(e.clientX, e.clientY)?.closest(SWIPE_SKIP)) return;
const x0 = e.clientX;
const y0 = e.clientY;
const onUp = (ev: PointerEvent) => {
window.removeEventListener('pointerup', onUp, true);
const dx = ev.clientX - x0;
const dy = ev.clientY - y0;
if (back && dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
};
window.addEventListener('pointerup', onUp, true);
}
window.addEventListener('pointerdown', onDown, true);
return () => window.removeEventListener('pointerdown', onDown, true);
});
</script>
<div class="screen">
<Header {title} {back} grow={growNav} />
{#if SHOW_AD_BANNER}<AdBanner />{/if}
<main class="content" class:scroll class:fill={!growNav} class:column>{@render children?.()}</main>
{#if tabbar}
<nav class="tabbar">{@render tabbar()}</nav>
{/if}
</div>
<style>
.screen {
display: flex;
flex-direction: column;
/* Fit the visible viewport (set from visualViewport, app.svelte.ts) so a screen with a
bottom input — chat, word-check — stays above an open soft keyboard without the page
scrolling; falls back to the full height where the var is unset. */
height: var(--vvh, 100%);
}
.content {
flex: 0 1 auto;
min-height: 0;
}
.content.fill {
flex: 1 1 auto;
}
.content.scroll {
overflow-y: auto;
}
.content.column {
display: flex;
flex-direction: column;
}
.tabbar {
flex: 0 0 auto;
}
</style>