Stage 8 polish: iPhone refinements (keyboard, native pickers, compact invite)
Tests · UI / test (push) Successful in 17s
Tests · UI / test (push) Successful in 17s
Second owner-review pass (iPhone simulator): - Chat (and the modal) are sized in dvh so they shrink above the software keyboard, keeping the start of the conversation on screen instead of pushed off the top. - The profile away window returns to a native <input type="time" step="600"> (the iOS wheel with 10-minute steps) instead of separate dropdowns; the timezone stays a native offset <select>. - A finished game reserves the rack's height (min-height) so the footer no longer collapses when the final rack is empty — no layout jump versus an active game. - New-game "play with friends" is made compact: a searchable, bounded-scroll friend list, the game-type / move-time / hints controls as native selects in one row (labels above), and Send invitation pinned at the bottom — it scales to many friends.
This commit is contained in:
@@ -61,10 +61,17 @@
|
||||
// --- friend game ---
|
||||
let friends = $state<AccountRef[]>([]);
|
||||
let selected = $state<string[]>([]);
|
||||
let friendFilter = $state('');
|
||||
let inviteVariant = $state<Variant>('english');
|
||||
let timeoutSecs = $state(86400);
|
||||
let hints = $state(1);
|
||||
|
||||
const filteredFriends = $derived(
|
||||
friendFilter.trim()
|
||||
? friends.filter((f) => f.displayName.toLowerCase().includes(friendFilter.trim().toLowerCase()))
|
||||
: friends,
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
if (guest) return;
|
||||
try {
|
||||
@@ -121,45 +128,45 @@
|
||||
<button class="variant" onclick={() => find(v.id)}>{t(v.label)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if friends.length === 0}
|
||||
<p class="subtitle">{t('new.noFriends')}</p>
|
||||
{:else}
|
||||
{#if friends.length === 0}
|
||||
<p class="subtitle">{t('new.noFriends')}</p>
|
||||
{:else}
|
||||
<h3>{t('new.pickFriends')}</h3>
|
||||
<div class="friends">
|
||||
{#each friends as f (f.accountId)}
|
||||
<div class="fg">
|
||||
<div class="picked">
|
||||
<span class="ftitle">{t('new.pickFriends')} ({selected.length})</span>
|
||||
<input class="search" bind:value={friendFilter} placeholder={t('new.searchFriends')} />
|
||||
</div>
|
||||
<div class="friends-scroll">
|
||||
{#each filteredFriends as f (f.accountId)}
|
||||
<label class="friend">
|
||||
<input type="checkbox" checked={selected.includes(f.accountId)} onchange={() => toggle(f.accountId)} />
|
||||
<span>{f.displayName}</span>
|
||||
</label>
|
||||
{/each}
|
||||
{#if filteredFriends.length === 0}<p class="muted">—</p>{/if}
|
||||
</div>
|
||||
|
||||
<h3>{t('new.title')}</h3>
|
||||
<div class="seg">
|
||||
{#each variants as v (v.id)}
|
||||
<button class="opt" class:active={inviteVariant === v.id} onclick={() => (inviteVariant = v.id)}>{t(v.label)}</button>
|
||||
{/each}
|
||||
<div class="settings-row">
|
||||
<label class="field">
|
||||
<span>{t('new.gameType')}</span>
|
||||
<select bind:value={inviteVariant}>
|
||||
{#each variants as v (v.id)}<option value={v.id}>{t(v.label)}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>{t('new.moveTime')}</span>
|
||||
<select bind:value={timeoutSecs}>
|
||||
{#each timeouts as to (to.secs)}<option value={to.secs}>{t(to.key, { n: to.n })}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>{t('new.hintsPerPlayer')}</span>
|
||||
<select bind:value={hints}>
|
||||
{#each [0, 1, 2] as h (h)}<option value={h}>{h}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>{t('new.moveTime')}</h3>
|
||||
<div class="seg">
|
||||
{#each timeouts as to (to.secs)}
|
||||
<button class="opt" class:active={timeoutSecs === to.secs} onclick={() => (timeoutSecs = to.secs)}>
|
||||
{t(to.key, { n: to.n })}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<h3>{t('new.hintsPerPlayer')}</h3>
|
||||
<div class="seg">
|
||||
{#each [0, 1, 2] as h (h)}
|
||||
<button class="opt" class:active={hints === h} onclick={() => (hints = h)}>{h}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="invite" disabled={selected.length === 0} onclick={sendInvite}>{t('new.invite')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -171,16 +178,13 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 6px 0 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.variants {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -219,7 +223,34 @@
|
||||
color: var(--accent-text);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.friends {
|
||||
.fg {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.picked {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.ftitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.search {
|
||||
min-width: 0;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.friends-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -233,8 +264,37 @@
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.settings-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.field span {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.field select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
padding: 9px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.muted {
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
.invite {
|
||||
margin-top: 8px;
|
||||
flex: 0 0 auto;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent);
|
||||
|
||||
Reference in New Issue
Block a user