diff --git a/PLAN.md b/PLAN.md index 83b0c5e..62cd4e2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1354,6 +1354,12 @@ provided cert) at the contour caddy; prod VPN; rollback. `..._CHANNEL_ID_..` to post). Switchers became icons — a 🌐 language dropdown (saved, synced to the app) and a ☼/☾ theme toggle that is **ephemeral** (follows the system scheme, never persisted, no "auto"). The "Play in browser" CTA was dropped (no standalone-web onboarding yet). + - **Game/Telegram review-pass polish:** the USSR flag emblem redrawn (canonical hammer & sickle, + scaled down ×1.5 below the star); touch drag enlarges the drag ghost ×1.5 (touch only — the + finger hides the tile) and suppresses the iOS tap-highlight that lingered on a rack tile sliding + into a dragged tile's slot; and **Telegram fullscreen** no longer hides our header under its + native nav — the header drops below the content-safe-area top inset and the menu (hamburger) + lifts into the nav band, centred (`--tg-content-top` from the SDK + a `tg-fullscreen` class). ## Deferred TODOs (cross-stage) diff --git a/ui/public/flag-ussr.svg b/ui/public/flag-ussr.svg index 0bdc2ea..58e4d8b 100644 --- a/ui/public/flag-ussr.svg +++ b/ui/public/flag-ussr.svg @@ -1,14 +1,16 @@ - - - - - - - - - - + + + + + + + + + + + diff --git a/ui/src/app.css b/ui/src/app.css index ff1ed3e..5f33900 100644 --- a/ui/src/app.css +++ b/ui/src/app.css @@ -41,6 +41,9 @@ --radius-sm: 6px; --gap: 8px; --pad: 12px; + /* 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; --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 e39b0a6..d1cb200 100644 --- a/ui/src/components/Header.svelte +++ b/ui/src/components/Header.svelte @@ -89,4 +89,20 @@ transform: rotate(45deg); margin-left: 3px; } + /* Telegram fullscreen: its native nav overlays the top of the viewport (height + --tg-content-top, set from the content-safe-area inset). Drop the header content below the + nav and lift the menu up into the nav band, centred — Telegram's own controls sit in the + corners, leaving the centre clear (Stage 17). */ + :global(html.tg-fullscreen) .bar { + padding-top: var(--tg-content-top); + } + :global(html.tg-fullscreen) .end { + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + height: var(--tg-content-top); + justify-content: center; + z-index: 30; + } diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index dd29acc..4af1517 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -60,7 +60,7 @@ let checkResult = $state<{ word: string; legal: boolean } | null>(null); let resignOpen = $state(false); let messages = $state([]); - let drag = $state<{ letter: string; blank: boolean; x: number; y: number } | null>(null); + let drag = $state<{ letter: string; blank: boolean; x: number; y: number; touch: boolean } | null>(null); const checkedWords = new Map(); let cooling = $state(false); @@ -315,7 +315,7 @@ const src = downInfo.src; const letter = src.from === 'rack' ? placement.rack[src.index] : pendingMap.get(`${src.row},${src.col}`)?.letter ?? ''; - drag = { letter, blank: letter === BLANK, x: e.clientX, y: e.clientY }; + drag = { letter, blank: letter === BLANK, x: e.clientX, y: e.clientY, touch: e.pointerType === 'touch' }; // A rack tile is lifted out of the rack while dragged (the ghost stands in for it). reorderDragId = src.from === 'rack' ? rackIds[src.index] ?? null : null; // No zoom on drag start: the player may still change their mind. Holding the tile @@ -814,7 +814,7 @@ {#if drag} -
+
{drag.blank ? '' : drag.letter}
{/if} @@ -1092,6 +1092,10 @@ pointer-events: none; z-index: 60; } + /* On touch the finger covers the tile, so enlarge the drag ghost ~1.5x (Stage 17). */ + .ghost.touch { + transform: translate(-50%, -50%) scale(1.5); + } .alpha { display: grid; grid-template-columns: repeat(6, 1fr); diff --git a/ui/src/game/Rack.svelte b/ui/src/game/Rack.svelte index 6a11f3b..e85295b 100644 --- a/ui/src/game/Rack.svelte +++ b/ui/src/game/Rack.svelte @@ -91,6 +91,10 @@ font-size: 1.4rem; touch-action: none; user-select: none; + -webkit-user-select: none; + /* iOS shows a tap/active highlight that can linger on the neighbour sliding into a + dragged tile's slot (Stage 17); suppress it so only our own styles mark a tile. */ + -webkit-tap-highlight-color: transparent; } .tile.selected { outline: 3px solid var(--accent); diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 6957f46..c47efb1 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -13,6 +13,7 @@ import { insideTelegram, onTelegramPath, telegramColorScheme, + telegramContentSafeAreaTop, telegramDisableVerticalSwipes, telegramHaptic, telegramLaunch, @@ -227,6 +228,19 @@ 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 lift the menu into its + * band (Stage 17). Called on launch and on Telegram's safe-area / fullscreen change events. + */ +function syncTelegramSafeArea(): void { + if (typeof document === 'undefined') return; + const top = telegramContentSafeAreaTop(); + document.documentElement.style.setProperty('--tg-content-top', `${top}px`); + document.documentElement.classList.toggle('tg-fullscreen', top > 0); +} + export async function bootstrap(): Promise { const prefs = await loadPrefs(); app.theme = prefs.theme ?? 'auto'; @@ -263,6 +277,9 @@ export async function bootstrap(): Promise { // Match Telegram's chrome to the app and stop its swipe-down-to-minimise from // fighting tile drag / board scroll. syncTelegramChrome(); + syncTelegramSafeArea(); + telegramOnEvent('contentSafeAreaChanged', syncTelegramSafeArea); + telegramOnEvent('fullscreenChanged', syncTelegramSafeArea); telegramDisableVerticalSwipes(); try { await adoptSession(await gateway.authTelegram(launch.initData)); diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index 1ed3a63..b7460d7 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -10,6 +10,8 @@ interface TelegramWebApp { initDataUnsafe?: { start_param?: string }; themeParams?: TelegramThemeParams; colorScheme?: 'light' | 'dark'; + isFullscreen?: boolean; + contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number }; ready?: () => void; expand?: () => void; onEvent?: (event: string, handler: () => void) => void; @@ -99,6 +101,15 @@ export function telegramSetChrome(header: string, background: string, bottom: st if (bottom) w?.setBottomBarColor?.(bottom); } +/** + * telegramContentSafeAreaTop returns the height (px) Telegram's own UI overlays at the top of + * the viewport in fullscreen (its nav band; the content-safe area, Bot API 8.0). It is 0 + * outside Telegram or on clients predating it, so callers can pad/position defensively. + */ +export function telegramContentSafeAreaTop(): number { + return webApp()?.contentSafeAreaInset?.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.