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
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.
100 lines
3.6 KiB
Svelte
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>
|