Game/Telegram review polish: USSR flag, touch drag ghost, TG fullscreen header
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 31s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 55s

Backlog item 2 of ~4 (owner review pass):
- USSR flag emblem redrawn (canonical hammer & sickle, scaled down 1.5x
  below the star).
- Touch drag-and-drop: enlarge the drag ghost 1.5x on touch only (the finger
  hides the tile); suppress the iOS tap-highlight that lingered on a rack tile
  sliding into a dragged tile's slot.
- Telegram fullscreen: its native nav no longer hides our header -- 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 inset + a
  tg-fullscreen class; new telegram.ts helper + app wiring).

Tests: UI check/test:unit/build + full e2e (60) green. The iOS tap-highlight
fix and the TG-fullscreen layout want on-device verification on the deploy.
This commit is contained in:
Ilia Denisov
2026-06-08 17:11:10 +02:00
parent 3fd279cf8c
commit 34385240b9
8 changed files with 76 additions and 13 deletions
+6
View File
@@ -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)
+12 -10
View File
@@ -1,14 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 16" role="img" aria-label="СССР">
<rect width="24" height="16" fill="#cc0000"/>
<!-- five-pointed star (filled, slightly smaller) -->
<path fill="#ffd700" d="M6 2.4l.78 1.6 1.76.26-1.27 1.24.3 1.75L6 6.63l-1.57.82.3-1.75L3.46 4.5l1.76-.26z"/>
<!-- schematic hammer & sickle (a sketch, thin strokes) -->
<g fill="none" stroke="#ffd700" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round">
<!-- sickle: an elongated semicircle blade with a short handle -->
<path d="M8.2 7.4a3 3 0 1 1-3.3 3.9"/>
<path d="M4.9 11.3l-.8.7"/>
<!-- hammer: a T-shape (handle + head) crossing the sickle -->
<path d="M5.1 11 8.1 8"/>
<path d="M7.2 7.1 9 8.9"/>
<!-- small five-pointed star -->
<path fill="#ffd700" d="M6 1.9 L6.32 2.86 7.33 2.87 6.51 3.47 6.82 4.43 6 3.84 5.18 4.43 5.49 3.47 4.67 2.87 5.68 2.86 Z"/>
<g fill="none" stroke="#ffd700" stroke-linecap="round" stroke-linejoin="round" transform="translate(6.8 6) scale(0.667) translate(-6.8 -6)">
<!-- sickle: a crescent blade + short handle, mirrored across a diagonal through its centre
so it reads as the canonical sickle (blade sweeping down-right); the hammer is untouched -->
<g transform="matrix(0 1 1 0 -2.8 2.8)">
<path stroke-width="0.6" d="M8.1 6.0 C 10.7 6.9 10.9 11.3 7.2 13.3 C 5.1 14.5 2.9 13.2 2.7 10.9"/>
<path stroke-width="0.6" d="M8.1 6.0 l 0.85 -0.95"/>
</g>
<!-- hammer: handle (down-right) + head (a short bar) at ~90°, crossing the sickle -->
<path stroke-width="0.78" d="M4.6 8.4 L 8.4 12.9"/>
<path stroke-width="0.78" d="M3.25 9.05 L 5.95 7.05"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 1.1 KiB

+3
View File
@@ -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);
+16
View File
@@ -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;
}
</style>
+7 -3
View File
@@ -60,7 +60,7 @@
let checkResult = $state<{ word: string; legal: boolean } | null>(null);
let resignOpen = $state(false);
let messages = $state<ChatMessage[]>([]);
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<string, boolean>();
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 @@
</Screen>
{#if drag}
<div class="ghost" style="left:{drag.x}px; top:{drag.y}px">
<div class="ghost" class:touch={drag.touch} style="left:{drag.x}px; top:{drag.y}px">
<span>{drag.blank ? '' : drag.letter}</span>
</div>
{/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);
+4
View File
@@ -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);
+17
View File
@@ -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<void> {
const prefs = await loadPrefs();
app.theme = prefs.theme ?? 'auto';
@@ -263,6 +277,9 @@ export async function bootstrap(): Promise<void> {
// 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));
+11
View File
@@ -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.