From c1ac77bc6ad8895dbb8f42b9026007c3220c875a Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 7 Jul 2026 13:19:22 +0200 Subject: [PATCH] fix(offline): unify NewGame layout to natural-flow + PinPad verdict pause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NewGame: all three forms (quick / friends / hotseat) now use the Profile sub-view's natural-flow .page (no forced height, no pinned-CTA .grow/.fg scroll), so the screen's single .content scroll + --vvh handle overflow and a focused name input scrolls into view cleanly — fixes the keyboard blank-space / deep-scroll regression. One layout rule, one place. - PinPad: pause 250ms after the 4th digit before the verdict, so the fill (and the shake on a wrong PIN) reads as a deliberate response. - e2e: typePin waits for empty dots before typing (robust to the pause). --- ui/e2e/hotseat.spec.ts | 3 +++ ui/src/components/PinPad.svelte | 3 +++ ui/src/screens/NewGame.svelte | 35 ++++----------------------------- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/ui/e2e/hotseat.spec.ts b/ui/e2e/hotseat.spec.ts index 1ac8c6a..4f92a54 100644 --- a/ui/e2e/hotseat.spec.ts +++ b/ui/e2e/hotseat.spec.ts @@ -27,7 +27,10 @@ async function goOffline(page: Page): Promise { } // typePin clicks the on-screen keypad digits (the pad has no OK button — the 4th digit is the action). +// It first waits for the dots to be empty, so a call made right after a previous entry does not lose +// digits during the 250 ms verdict pause (the 4th digit → pause → clear/advance). async function typePin(page: Page, digits: string): Promise { + await expect(page.locator('.pad .dot.on')).toHaveCount(0); for (const d of digits) await page.locator('.pad .key', { hasText: d }).click(); } diff --git a/ui/src/components/PinPad.svelte b/ui/src/components/PinPad.svelte index f903392..9514e61 100644 --- a/ui/src/components/PinPad.svelte +++ b/ui/src/components/PinPad.svelte @@ -72,6 +72,9 @@ async function commit(): Promise { busy = true; try { + // Let the 4th dot register before the verdict: a beat so the fill (and, on failure, the shake) + // reads as a deliberate response rather than an instant flicker. + await new Promise((r) => setTimeout(r, 250)); if (phase === 'old') { if (verify && (await verify(buffer))) { buffer = ''; diff --git a/ui/src/screens/NewGame.svelte b/ui/src/screens/NewGame.svelte index 8ada7c3..f3c607b 100644 --- a/ui/src/screens/NewGame.svelte +++ b/ui/src/screens/NewGame.svelte @@ -232,14 +232,6 @@ deleteRow = null; } - // Centre a focused name input above the soft keyboard: the keyboard shrinks the screen (--vvh), - // so wait a beat for it to open + the layout to settle, then scroll the field into view (the - // default focus-scroll leaves a bottom row hidden behind the keyboard). - function bringIntoView(e: FocusEvent): void { - const el = e.currentTarget as HTMLElement; - setTimeout(() => el.scrollIntoView({ block: 'center' }), 300); - } - function setHostPlays(yes: boolean): void { askHostPlays = false; if (!yes) return; @@ -286,10 +278,7 @@ - -
+
@@ -333,7 +322,6 @@ {/if} -

{opponent === 'ai' ? t('new.aiInactiveLimit') : t('new.moveLimit', { n: AUTO_MATCH_HOURS })}

{#if opponent === 'random'}

{t('new.searchHint')}

{/if}