Review fixes: swipe (capture-phase, enabled in TG), TG header aligns to the nav band, DnD zoom delay 1s->0.7s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 32s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s

- Edge-swipe back now listens at the window in the CAPTURE phase (the board's
  pointer handlers can't swallow it) and is no longer skipped inside Telegram
  (where the owner tests it).
- TG-fullscreen header: expose the device safe-area top (--tg-safe-top) and
  centre the title + menu pair within Telegram's nav band ([safe-top,
  content-top]) below the notch, keeping the band's height — lining up with
  Telegram's own controls.
- DnD auto-zoom-on-hover delay reduced 1000ms -> 700ms.

(Client-IP: diagnosed as the owner's home-router SNAT — the host caddy already
receives 192.168.0.1 with no XFF, so the real IP is lost upstream of our stack;
correct in prod. No code change.)
This commit is contained in:
Ilia Denisov
2026-06-08 22:01:43 +02:00
parent 645df52c0b
commit 7e34897d6d
6 changed files with 46 additions and 26 deletions
+3
View File
@@ -44,6 +44,9 @@
/* Height Telegram's native nav overlays at the top in fullscreen; set from the SDK's /* Height Telegram's native nav overlays at the top in fullscreen; set from the SDK's
content-safe-area inset (Stage 17), 0 elsewhere. */ content-safe-area inset (Stage 17), 0 elsewhere. */
--tg-content-top: 0px; --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, --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
"Noto Sans", "Liberation Sans", sans-serif; "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); --shadow: 0 1px 2px rgba(0, 0, 0, 0.08), 0 6px 16px rgba(0, 0, 0, 0.06);
+8 -7
View File
@@ -89,17 +89,18 @@
transform: rotate(45deg); transform: rotate(45deg);
margin-left: 3px; margin-left: 3px;
} }
/* Telegram fullscreen: TG's native nav overlays a band of height --tg-content-top at the top /* Telegram fullscreen: TG's native nav occupies the band between the device notch
of the viewport. Pull our title + menu up into the BOTTOM of that band and centre them as a (--tg-safe-top) and --tg-content-top. Our header spans that full band (so the layout below
pair (hamburger right of the title) so they line up with Telegram's own nav controls rather is unchanged) and centres the title + menu as a pair (hamburger right of the title) within
than floating above them (Stage 17). */ 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 { :global(html.tg-fullscreen) .bar {
min-height: var(--tg-content-top); min-height: var(--tg-content-top);
box-sizing: border-box; box-sizing: border-box;
align-items: flex-end; align-items: center;
justify-content: center; justify-content: center;
padding-top: 0; padding-top: var(--tg-safe-top);
padding-bottom: 6px; padding-bottom: 0;
} }
:global(html.tg-fullscreen) .spacer { :global(html.tg-fullscreen) .spacer {
display: none; display: none;
+12 -10
View File
@@ -3,7 +3,6 @@
import Header from './Header.svelte'; import Header from './Header.svelte';
import AdBanner from './AdBanner.svelte'; import AdBanner from './AdBanner.svelte';
import { navigate } from '../lib/router.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 // 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` // optional tab bar pin to the bottom (ad directly above the content). Pass `scroll`
@@ -37,25 +36,28 @@
const SHOW_AD_BANNER = false; const SHOW_AD_BANNER = false;
// Edge-swipe back (Stage 17): a left-edge rightward drag returns to `back`, the standard // 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 // mobile gesture. Listened at the window in the CAPTURE phase so the board's own pointer
// board's own horizontal gestures; touch/pen only. Skipped inside Telegram, whose native // handlers (which capture/stop the event) can never swallow it; armed only from the very
// back button + swipe already cover this (and would otherwise double up). // left edge (<=24px), touch/pen only, so it never competes with the board's gestures.
function onEdgeDown(e: PointerEvent): void { $effect(() => {
if (!back || e.pointerType === 'mouse' || insideTelegram() || e.clientX > 24) return; function onDown(e: PointerEvent) {
if (!back || e.pointerType === 'mouse' || e.clientX > 24) return;
const x0 = e.clientX; const x0 = e.clientX;
const y0 = e.clientY; const y0 = e.clientY;
const onUp = (ev: PointerEvent) => { const onUp = (ev: PointerEvent) => {
window.removeEventListener('pointerup', onUp); window.removeEventListener('pointerup', onUp, true);
const dx = ev.clientX - x0; const dx = ev.clientX - x0;
const dy = ev.clientY - y0; const dy = ev.clientY - y0;
if (back && dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back); if (back && dx > 64 && Math.abs(dx) > Math.abs(dy) * 1.4) navigate(back);
}; };
window.addEventListener('pointerup', onUp); window.addEventListener('pointerup', onUp, true);
} }
window.addEventListener('pointerdown', onDown, true);
return () => window.removeEventListener('pointerdown', onDown, true);
});
</script> </script>
<!-- svelte-ignore a11y_no_static_element_interactions --> <div class="screen">
<div class="screen" onpointerdown={onEdgeDown}>
<Header {title} {back} {menu} grow={growNav} /> <Header {title} {back} {menu} grow={growNav} />
{#if SHOW_AD_BANNER}<AdBanner />{/if} {#if SHOW_AD_BANNER}<AdBanner />{/if}
<main class="content" class:scroll class:fill={!growNav} class:column>{@render children?.()}</main> <main class="content" class:scroll class:fill={!growNav} class:column>{@render children?.()}</main>
+1 -1
View File
@@ -364,7 +364,7 @@
zoomed = true; zoomed = true;
telegramHaptic('light'); telegramHaptic('light');
} }
}, 1000) }, 700)
: null; : null;
} }
} }
+3
View File
@@ -14,6 +14,7 @@ import {
onTelegramPath, onTelegramPath,
telegramColorScheme, telegramColorScheme,
telegramContentSafeAreaTop, telegramContentSafeAreaTop,
telegramSafeAreaTop,
telegramDisableVerticalSwipes, telegramDisableVerticalSwipes,
telegramHaptic, telegramHaptic,
telegramLaunch, telegramLaunch,
@@ -238,6 +239,7 @@ function syncTelegramSafeArea(): void {
if (typeof document === 'undefined') return; if (typeof document === 'undefined') return;
const top = telegramContentSafeAreaTop(); const top = telegramContentSafeAreaTop();
document.documentElement.style.setProperty('--tg-content-top', `${top}px`); 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); document.documentElement.classList.toggle('tg-fullscreen', top > 0);
} }
@@ -279,6 +281,7 @@ export async function bootstrap(): Promise<void> {
syncTelegramChrome(); syncTelegramChrome();
syncTelegramSafeArea(); syncTelegramSafeArea();
telegramOnEvent('contentSafeAreaChanged', syncTelegramSafeArea); telegramOnEvent('contentSafeAreaChanged', syncTelegramSafeArea);
telegramOnEvent('safeAreaChanged', syncTelegramSafeArea);
telegramOnEvent('fullscreenChanged', syncTelegramSafeArea); telegramOnEvent('fullscreenChanged', syncTelegramSafeArea);
telegramDisableVerticalSwipes(); telegramDisableVerticalSwipes();
try { try {
+11
View File
@@ -11,6 +11,7 @@ interface TelegramWebApp {
themeParams?: TelegramThemeParams; themeParams?: TelegramThemeParams;
colorScheme?: 'light' | 'dark'; colorScheme?: 'light' | 'dark';
isFullscreen?: boolean; isFullscreen?: boolean;
safeAreaInset?: { top: number; bottom: number; left: number; right: number };
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number }; contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
ready?: () => void; ready?: () => void;
expand?: () => void; expand?: () => void;
@@ -110,6 +111,16 @@ export function telegramContentSafeAreaTop(): number {
return webApp()?.contentSafeAreaInset?.top ?? 0; 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 * 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. * it does not fight tile drag-and-drop or the board's vertical scroll.