Round-6 follow-up: UX polish + client-IP fix
CI / changes (pull_request) Successful in 2s
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

- Client IP: the compose caddy trusts X-Forwarded-For from private-range
  upstreams (trusted_proxies private_ranges), so the real client IP survives
  the host-caddy hop (it was logging the docker caddy hop 172.18.0.x for chat
  moderation and bucketing the gateway per-IP rate limiter on it). Correct and
  spoof-safe in both contours (prod has no host caddy); peerIP unit-tested.
- Ad banner gated off behind a compile-time SHOW_AD_BANNER=false (the if-branch,
  the AdBanner import and banner.ts are tree-shaken out of the prod bundle).
- Landing: the Telegram entry is just the 64px logo (clickable, no button/text).
- TG-fullscreen header: title + menu centred as a pair (hamburger right of the
  title), pinned to the bottom of the TG nav band.
- Edge-swipe back (Screen): a left-edge rightward drag navigates to back
  (touch/pen only, armed from <=24px; skipped inside Telegram).
- Chat soft-keyboard: a bottom-sheet Modal lifted above the keyboard by a
  visualViewport-driven transform (compositor-only, no page/sheet relayout).
  iOS-specific, needs on-device tuning; native resize=none awaits Capacitor.
- Tests: e2e for the in-game '✓ in friends' item and a board→board tile
  relocation; codec units for last_activity_unix + OutgoingRequestList.

Deferred to the next PR (agreed): #4 enrich the your-turn/game-end push; #5 hide
finished games from the lobby.
This commit is contained in:
Ilia Denisov
2026-06-08 21:31:44 +02:00
parent f95a6cb9c8
commit 645df52c0b
12 changed files with 229 additions and 29 deletions
+40 -4
View File
@@ -5,8 +5,15 @@
title = '',
onclose,
overlayKeyboard = false,
bottomSheet = false,
children,
}: { title?: string; onclose?: () => void; overlayKeyboard?: boolean; children?: Snippet } = $props();
}: {
title?: string;
onclose?: () => void;
overlayKeyboard?: boolean;
bottomSheet?: boolean;
children?: Snippet;
} = $props();
// Track the visual viewport so the backdrop covers only the area above an open
// mobile keyboard: dvh alone shrinks the sheet but the fixed, layout-viewport
@@ -14,14 +21,21 @@
// visualViewport keeps the sheet (and the start of a chat) fully on screen.
// overlayKeyboard opts out: the sheet is small and top-anchored, so the keyboard
// simply overlays the empty lower area — no resize, no relayout jank (e.g. check word).
// bottomSheet anchors a tall sheet (the chat) to the bottom and lifts it above the
// keyboard with a transform (kb), driven by the visual viewport — a compositor-only
// move, so neither the page behind nor the sheet relayouts as the keyboard animates
// (Stage 17). The backdrop is not resized in this mode (no per-event reflow).
let vh = $state(0);
let top = $state(0);
let kb = $state(0);
$effect(() => {
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
if (!vv || overlayKeyboard) return;
const update = () => {
vh = vv.height;
top = vv.offsetTop;
// Soft-keyboard height: the layout viewport minus the visible viewport.
kb = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
};
update();
vv.addEventListener('resize', update);
@@ -38,11 +52,19 @@
<div
class="backdrop"
class:overlay={overlayKeyboard}
style:height={vh ? `${vh}px` : null}
style:top={vh ? `${top}px` : null}
class:bottom={bottomSheet}
style:height={!bottomSheet && vh ? `${vh}px` : null}
style:top={!bottomSheet && vh ? `${top}px` : null}
onclick={() => onclose?.()}
>
<div class="sheet" role="dialog" aria-modal="true" tabindex="-1" onclick={(e) => e.stopPropagation()}>
<div
class="sheet"
role="dialog"
aria-modal="true"
tabindex="-1"
style:--kb={bottomSheet ? `${kb}px` : null}
onclick={(e) => e.stopPropagation()}
>
{#if title}<h2>{title}</h2>{/if}
{@render children?.()}
</div>
@@ -71,6 +93,20 @@
align-items: flex-start;
padding-top: 12vh;
}
/* Bottom-sheet mode (the chat): a wide sheet pinned to the bottom that lifts above the
soft keyboard via a transform (--kb) — compositor-only, so the page behind and the
sheet itself do not relayout as the keyboard animates (Stage 17). */
.backdrop.bottom {
align-items: flex-end;
padding: 0;
}
.backdrop.bottom .sheet {
width: 100%;
max-width: 640px;
border-radius: var(--radius) var(--radius) 0 0;
transform: translateY(calc(-1 * var(--kb, 0px)));
transition: transform 0.15s ease;
}
.sheet {
background: var(--surface);
color: var(--text);