feat(offline): local pass-and-play (hotseat) — 2-4 players, seat/host PINs #211
@@ -232,6 +232,14 @@
|
|||||||
deleteRow = null;
|
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 {
|
function setHostPlays(yes: boolean): void {
|
||||||
askHostPlays = false;
|
askHostPlays = false;
|
||||||
if (!yes) return;
|
if (!yes) return;
|
||||||
@@ -278,7 +286,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Screen title={t('new.title')} back="/">
|
<Screen title={t('new.title')} back="/">
|
||||||
<div class="page">
|
<!-- The hotseat form flows naturally and scrolls with the screen's own scroll (scrollform: no
|
||||||
|
forced full height, no pinned button) so a focused name input's soft keyboard just scrolls the
|
||||||
|
page rather than fighting a nested scroll region. -->
|
||||||
|
<div class="page" class:scrollform={mode === 'friends' && offlineMode.active}>
|
||||||
<!-- The auto/friends selector. Online it is hidden for guests (no friends to invite). Offline
|
<!-- The auto/friends selector. Online it is hidden for guests (no friends to invite). Offline
|
||||||
it is always shown: "quick" is the local vs_ai flow, "with friends" is the local
|
it is always shown: "quick" is the local vs_ai flow, "with friends" is the local
|
||||||
pass-and-play (hotseat) flow. -->
|
pass-and-play (hotseat) flow. -->
|
||||||
@@ -332,11 +343,7 @@
|
|||||||
>{t('new.start')}</button>
|
>{t('new.start')}</button>
|
||||||
{:else if offlineMode.active}
|
{:else if offlineMode.active}
|
||||||
<!-- Offline pass-and-play (hotseat): a device-local 2-4 human game. The host sets a master
|
<!-- Offline pass-and-play (hotseat): a device-local 2-4 human game. The host sets a master
|
||||||
PIN first (it gates the roster); each seat may add its own PIN. The scrollable region +
|
PIN first (it gates the roster); each seat may add its own PIN. -->
|
||||||
the pinned Start button mirror the friends form, so a name input's soft keyboard shrinks
|
|
||||||
the region (no full-height spacer that would paint a blank block behind the keyboard). -->
|
|
||||||
<div class="fg">
|
|
||||||
<div class="hs-scroll">
|
|
||||||
<div class="hostpin">
|
<div class="hostpin">
|
||||||
<span class="ftitle">{t('hotseat.hostPin')}</span>
|
<span class="ftitle">{t('hotseat.hostPin')}</span>
|
||||||
<button class="plink" onclick={() => (pad = hostPin ? { kind: 'host-change' } : { kind: 'host-set' })}>
|
<button class="plink" onclick={() => (pad = hostPin ? { kind: 'host-change' } : { kind: 'host-set' })}>
|
||||||
@@ -375,6 +382,7 @@
|
|||||||
disabled={!hostPin}
|
disabled={!hostPin}
|
||||||
placeholder={t('hotseat.playerName')}
|
placeholder={t('hotseat.playerName')}
|
||||||
maxlength="40"
|
maxlength="40"
|
||||||
|
onfocus={bringIntoView}
|
||||||
oninput={() => (row.committed = commitName(row.name, row.committed))}
|
oninput={() => (row.committed = commitName(row.name, row.committed))}
|
||||||
onblur={() => (row.name = row.committed)}
|
onblur={() => (row.name = row.committed)}
|
||||||
/>
|
/>
|
||||||
@@ -401,13 +409,11 @@
|
|||||||
+ {t('hotseat.addPlayer')}
|
+ {t('hotseat.addPlayer')}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
class="invite"
|
class="invite"
|
||||||
disabled={!hostPin || !inviteVariant || !rosterReady(rows) || starting}
|
disabled={!hostPin || !inviteVariant || !rosterReady(rows) || starting}
|
||||||
onclick={startHotseat}
|
onclick={startHotseat}
|
||||||
>{t('new.start')}</button>
|
>{t('new.start')}</button>
|
||||||
</div>
|
|
||||||
{:else if friends.length === 0}
|
{:else if friends.length === 0}
|
||||||
<p class="subtitle">{t('new.noFriends')}</p>
|
<p class="subtitle">{t('new.noFriends')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -486,6 +492,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
/* The hotseat form: natural height (grows past the viewport → the screen's own scroll takes over)
|
||||||
|
with no full-height pinning, so a focused name input just scrolls the page above the keyboard. */
|
||||||
|
.page.scrollform {
|
||||||
|
height: auto;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -680,17 +692,6 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
/* --- offline hotseat roster --- */
|
/* --- offline hotseat roster --- */
|
||||||
/* The scrollable region (host PIN + variants + roster), pinned above the Start button — mirrors
|
|
||||||
the friends form so a focused name input's soft keyboard shrinks this region (via --vvh on the
|
|
||||||
screen) instead of leaving a full-height blank spacer painted behind the keyboard. */
|
|
||||||
.hs-scroll {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.hostpin {
|
.hostpin {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user