Stage 17 (contour round 3): Telegram Mini Apps polish, board scroll, keyboard overlay
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 10s
CI / ui (pull_request) Successful in 27s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 54s

- Telegram (lib/telegram.ts): chrome colours (setHeaderColor/setBackgroundColor/setBottomBarColor) match Telegram's header/bg/bottom bar to the app; native BackButton on sub-screens (app chevron hidden in TG); HapticFeedback on tile place/commit/error; enableClosingConfirmation while a game is open; disableVerticalSwipes so swipe-to-minimise doesn't fight tile drag / board scroll
- #9 board-only vertical scroll: Screen 'column' mode lets the board area scroll while score/status/rack/tab bar stay fixed (zoom keeps its own scroll)
- #10 check-word dialog opens in Modal keyboard-overlay mode (top-anchored, keyboard overlays the empty area) — no resize/relayout jank; other modals stay keyboard-aware
- docs: UI_DESIGN Telegram integration + vertical fit/keyboard; PLAN round 2-3 follow-ups
This commit is contained in:
Ilia Denisov
2026-06-06 12:55:46 +02:00
parent 645a503532
commit f6bffd1f57
9 changed files with 204 additions and 18 deletions
+18 -3
View File
@@ -19,6 +19,7 @@
import { canCheckWord, sanitizeCheckWord } from '../lib/checkword';
import { shareOrDownloadGcg } from '../lib/share';
import { getCachedGame, setCachedGame } from '../lib/gamecache';
import { telegramClosingConfirmation, telegramHaptic } from '../lib/telegram';
import {
BLANK,
newPlacement,
@@ -112,6 +113,8 @@
}
}
onMount(() => {
// Guard against an accidental swipe-close losing the open game (Telegram).
telegramClosingConfirmation(true);
// Render instantly from the cache (a game opened before), then refresh in the
// background. A cold open shows the loading state until load() resolves.
const cached = getCachedGame(id);
@@ -185,6 +188,7 @@
onDestroy(() => {
window.removeEventListener('pointermove', onWinMove);
window.removeEventListener('pointerup', onWinUp);
telegramClosingConfirmation(false);
});
function onCell(row: number, col: number) {
@@ -212,12 +216,14 @@
return;
}
placement = place(placement, index, row, col);
telegramHaptic('select');
recompute();
}
function chooseBlank(letter: string) {
if (!blankPrompt) return;
placement = place(placement, blankPrompt.rackIndex, blankPrompt.row, blankPrompt.col, letter);
blankPrompt = null;
telegramHaptic('select');
recompute();
}
@@ -242,6 +248,7 @@
busy = true;
try {
await gateway.submitPlay(id, sub.dir, sub.tiles, variant);
telegramHaptic('success');
zoomed = false;
await load();
} catch (e) {
@@ -449,7 +456,7 @@
]);
</script>
<Screen title={t('app.title')} back="/" growNav scroll={!historyOpen}>
<Screen title={t('app.title')} back="/" growNav column scroll={false}>
{#snippet menu()}
<Menu items={menuItems} />
{/snippet}
@@ -596,7 +603,7 @@
{/if}
{#if checkOpen}
<Modal title={t('game.checkWord')} onclose={() => (checkOpen = false)}>
<Modal title={t('game.checkWord')} overlayKeyboard onclose={() => (checkOpen = false)}>
<div class="check">
<input
value={checkWord}
@@ -635,6 +642,7 @@
<style>
.scoreboard {
display: flex;
flex: none;
gap: 6px;
padding: 8px var(--pad);
background: var(--bg-elev);
@@ -681,7 +689,12 @@
}
.stage {
position: relative;
overflow: hidden;
/* The board is the only part that scrolls vertically when the game does not fit;
the score bar, status, rack and tab bar stay put (#9). */
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
}
.history {
position: absolute;
@@ -741,6 +754,7 @@
}
.status {
display: flex;
flex: none;
align-items: center;
justify-content: space-between;
padding: 2px var(--pad) 6px;
@@ -762,6 +776,7 @@
}
.rack-row {
display: flex;
flex: none;
gap: 8px;
align-items: stretch;
padding: 0 var(--pad) 6px;