From b84bd1297e23244abf38e6b3e7cae710ddc37554 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 24 Jun 2026 10:24:56 +0200 Subject: [PATCH] feat(telegram): clear bottom and side safe-area insets Only the device safe-area TOP inset was mirrored, so on phones with a home indicator the rack / bottom bar sat under it, and in landscape the notch clipped the screen edges. Mirror the full device safe-area inset (bottom / left / right) into new --tg-safe-bottom / --tg-safe-left / --tg-safe-right CSS vars (0 outside Telegram) and pad the shared Screen wrapper by them, so every screen clears the home indicator and the landscape notch; the top inset stays owned by the header. Replace telegramSafeAreaTop with telegramSafeAreaInset (the full inset object), with a unit test. --- ui/src/app.css | 5 +++++ ui/src/components/Screen.svelte | 5 +++++ ui/src/lib/app.svelte.ts | 24 ++++++++++++++++-------- ui/src/lib/telegram.test.ts | 9 +++++++++ ui/src/lib/telegram.ts | 14 ++++++++------ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/ui/src/app.css b/ui/src/app.css index c2fb4ba..46a1d44 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -49,6 +49,11 @@ /* 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, 0 elsewhere. */ --tg-safe-top: 0px; + /* Telegram device safe-area bottom / sides (home indicator; landscape notch), 0 elsewhere — + the screen pads its bottom and left/right edges by these so content clears the cut-outs. */ + --tg-safe-bottom: 0px; + --tg-safe-left: 0px; + --tg-safe-right: 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/Screen.svelte b/ui/src/components/Screen.svelte index f428b59..b7baf57 100644 --- a/ui/src/components/Screen.svelte +++ b/ui/src/components/Screen.svelte @@ -101,6 +101,11 @@ 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%); + /* Clear the device safe-area cut-outs inside Telegram — the home indicator at the bottom and + the notch sides in landscape; all 0 elsewhere. The top inset is owned by the header. */ + padding-bottom: var(--tg-safe-bottom, 0px); + padding-left: var(--tg-safe-left, 0px); + padding-right: var(--tg-safe-right, 0px); } .content { flex: 0 1 auto; diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 5233ae1..64bebe3 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -20,7 +20,7 @@ import { telegramColorScheme, telegramThemeParams, telegramContentSafeAreaTop, - telegramSafeAreaTop, + telegramSafeAreaInset, telegramDisableVerticalSwipes, telegramHaptic, telegramLaunch, @@ -528,17 +528,25 @@ function syncTelegramChrome(): void { } /** - * syncTelegramSafeArea mirrors Telegram's content-safe-area top inset (the height its native - * nav overlays the viewport in fullscreen) into the --tg-content-top CSS var and toggles a - * `tg-fullscreen` class, so the header can drop below the nav and centre the title in its - * band. Called on launch and on Telegram's safe-area / fullscreen change events. + * syncTelegramSafeArea mirrors Telegram's safe-area insets into CSS vars: the content-safe-area top + * (the height Telegram's native nav overlays the viewport in fullscreen) into --tg-content-top + * (which also toggles the `tg-fullscreen` class so the header drops below the nav and centres the + * title in its band), and the device safe-area insets — notch / status bar (top), home indicator + * (bottom) and the landscape notch sides (left / right) — into --tg-safe-top / --tg-safe-bottom / + * --tg-safe-left / --tg-safe-right, so the header, rack and screen edges clear the device cut-outs. + * Called on launch and on Telegram's safe-area / fullscreen change events. */ function syncTelegramSafeArea(): void { if (typeof document === 'undefined') return; + const root = document.documentElement; 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); + const safe = telegramSafeAreaInset(); + root.style.setProperty('--tg-content-top', `${top}px`); + root.style.setProperty('--tg-safe-top', `${safe.top}px`); + root.style.setProperty('--tg-safe-bottom', `${safe.bottom}px`); + root.style.setProperty('--tg-safe-left', `${safe.left}px`); + root.style.setProperty('--tg-safe-right', `${safe.right}px`); + root.classList.toggle('tg-fullscreen', top > 0); } /** diff --git a/ui/src/lib/telegram.test.ts b/ui/src/lib/telegram.test.ts index b6f4816..fec4340 100644 --- a/ui/src/lib/telegram.test.ts +++ b/ui/src/lib/telegram.test.ts @@ -8,6 +8,7 @@ import { telegramLaunch, telegramOpenExternalLink, telegramThemeParams, + telegramSafeAreaInset, } from './telegram'; function stubWebApp(initData: string, startParam?: string) { @@ -51,6 +52,14 @@ describe('telegram launch detection', () => { stubWebApp('query_id=abc'); expect(telegramThemeParams()?.bg_color).toBe('#101418'); }); + + it('telegramSafeAreaInset returns zeros outside Telegram and the SDK insets inside', () => { + expect(telegramSafeAreaInset()).toEqual({ top: 0, bottom: 0, left: 0, right: 0 }); + vi.stubGlobal('window', { + Telegram: { WebApp: { initData: 'x', safeAreaInset: { top: 59, bottom: 34, left: 0, right: 0 } } }, + }); + expect(telegramSafeAreaInset()).toEqual({ top: 59, bottom: 34, left: 0, right: 0 }); + }); }); describe('telegramOpenExternalLink', () => { diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index f0d85cc..40b3efe 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -274,13 +274,15 @@ export function telegramContentSafeAreaTop(): number { } /** - * 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. + * telegramSafeAreaInset returns the device safe-area insets (px) — the notch / status bar (top), + * the home indicator (bottom) and, in landscape, the notch sides (left / right) — from the SDK's + * safeAreaInset (Bot API 8.0). All 0 outside Telegram or on a client predating it, so callers can + * pad defensively. Telegram's own nav controls sit in the band between the top inset and + * telegramContentSafeAreaTop, so aligning our header to that band lines it up with them. */ -export function telegramSafeAreaTop(): number { - return webApp()?.safeAreaInset?.top ?? 0; +export function telegramSafeAreaInset(): { top: number; bottom: number; left: number; right: number } { + const i = webApp()?.safeAreaInset; + return { top: i?.top ?? 0, bottom: i?.bottom ?? 0, left: i?.left ?? 0, right: i?.right ?? 0 }; } /**