diff --git a/ui/src/app.css b/ui/src/app.css index 5f33900..bc94eda 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -44,6 +44,9 @@ /* Height Telegram's native nav overlays at the top in fullscreen; set from the SDK's content-safe-area inset (Stage 17), 0 elsewhere. */ --tg-content-top: 0px; + /* Telegram device safe-area top (the notch); TG's own nav controls sit between it and + --tg-content-top, so the in-app header aligns to that band (Stage 17), 0 elsewhere. */ + --tg-safe-top: 0px; --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; --shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 6px 16px rgba(0, 0, 0, 0.06); diff --git a/ui/src/components/Header.svelte b/ui/src/components/Header.svelte index 34b6529..140bbd5 100644 --- a/ui/src/components/Header.svelte +++ b/ui/src/components/Header.svelte @@ -89,17 +89,18 @@ transform: rotate(45deg); margin-left: 3px; } - /* Telegram fullscreen: TG's native nav overlays a band of height --tg-content-top at the top - of the viewport. Pull our title + menu up into the BOTTOM of that band and centre them as a - pair (hamburger right of the title) so they line up with Telegram's own nav controls rather - than floating above them (Stage 17). */ + /* Telegram fullscreen: TG's native nav occupies the band between the device notch + (--tg-safe-top) and --tg-content-top. Our header spans that full band (so the layout below + is unchanged) and centres the title + menu as a pair (hamburger right of the title) within + it, BELOW the notch — lining them up vertically with Telegram's own back/menu controls, + which sit in the band's corners (Stage 17). */ :global(html.tg-fullscreen) .bar { min-height: var(--tg-content-top); box-sizing: border-box; - align-items: flex-end; + align-items: center; justify-content: center; - padding-top: 0; - padding-bottom: 6px; + padding-top: var(--tg-safe-top); + padding-bottom: 0; } :global(html.tg-fullscreen) .spacer { display: none; diff --git a/ui/src/components/Screen.svelte b/ui/src/components/Screen.svelte index 29f7d0c..6ddf877 100644 --- a/ui/src/components/Screen.svelte +++ b/ui/src/components/Screen.svelte @@ -3,7 +3,6 @@ import Header from './Header.svelte'; import AdBanner from './AdBanner.svelte'; import { navigate } from '../lib/router.svelte'; - import { insideTelegram } from '../lib/telegram'; // 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` @@ -37,25 +36,28 @@ const SHOW_AD_BANNER = false; // Edge-swipe back (Stage 17): a left-edge rightward drag returns to `back`, the standard - // mobile gesture. Armed only from the very left edge (<=24px) so it never competes with the - // board's own horizontal gestures; touch/pen only. Skipped inside Telegram, whose native - // back button + swipe already cover this (and would otherwise double up). - function onEdgeDown(e: PointerEvent): void { - if (!back || e.pointerType === 'mouse' || insideTelegram() || e.clientX > 24) return; - const x0 = e.clientX; - const y0 = e.clientY; - const onUp = (ev: PointerEvent) => { - window.removeEventListener('pointerup', onUp); - 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); - } + // mobile gesture. Listened at the window in the CAPTURE phase so the board's own pointer + // handlers (which capture/stop the event) can never swallow it; armed only from the very + // left edge (<=24px), touch/pen only, so it never competes with the board's gestures. + $effect(() => { + function onDown(e: PointerEvent) { + if (!back || e.pointerType === 'mouse' || e.clientX > 24) 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); + }); - -
+
{#if SHOW_AD_BANNER}{/if}
{@render children?.()}
diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index ed23e93..ca9ef6a 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -364,7 +364,7 @@ zoomed = true; telegramHaptic('light'); } - }, 1000) + }, 700) : null; } } diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index c47efb1..e929900 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -14,6 +14,7 @@ import { onTelegramPath, telegramColorScheme, telegramContentSafeAreaTop, + telegramSafeAreaTop, telegramDisableVerticalSwipes, telegramHaptic, telegramLaunch, @@ -238,6 +239,7 @@ function syncTelegramSafeArea(): void { if (typeof document === 'undefined') return; const top = telegramContentSafeAreaTop(); document.documentElement.style.setProperty('--tg-content-top', `${top}px`); + document.documentElement.style.setProperty('--tg-safe-top', `${telegramSafeAreaTop()}px`); document.documentElement.classList.toggle('tg-fullscreen', top > 0); } @@ -279,6 +281,7 @@ export async function bootstrap(): Promise { syncTelegramChrome(); syncTelegramSafeArea(); telegramOnEvent('contentSafeAreaChanged', syncTelegramSafeArea); + telegramOnEvent('safeAreaChanged', syncTelegramSafeArea); telegramOnEvent('fullscreenChanged', syncTelegramSafeArea); telegramDisableVerticalSwipes(); try { diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index b7460d7..0645102 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -11,6 +11,7 @@ interface TelegramWebApp { themeParams?: TelegramThemeParams; colorScheme?: 'light' | 'dark'; isFullscreen?: boolean; + safeAreaInset?: { top: number; bottom: number; left: number; right: number }; contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number }; ready?: () => void; expand?: () => void; @@ -110,6 +111,16 @@ export function telegramContentSafeAreaTop(): number { return webApp()?.contentSafeAreaInset?.top ?? 0; } +/** + * telegramSafeAreaTop returns the device safe-area top inset (px) — the notch / status bar + * (Bot API 8.0). Telegram's own nav controls sit in the band between it and + * telegramContentSafeAreaTop, so aligning our header to that band lines it up with them. 0 + * outside Telegram or on older clients. + */ +export function telegramSafeAreaTop(): number { + return webApp()?.safeAreaInset?.top ?? 0; +} + /** * telegramDisableVerticalSwipes turns off Telegram's swipe-down-to-minimise gesture so * it does not fight tile drag-and-drop or the board's vertical scroll.