Stage 8 polish: iPhone refinements (keyboard, native pickers, compact invite)
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:
Ilia Denisov
2026-06-03 22:47:22 +02:00
parent acbb2d8254
commit 1d795e0acf
7 changed files with 129 additions and 67 deletions
+19 -30
View File
@@ -5,8 +5,6 @@
import { t } from '../lib/i18n/index.svelte';
import {
awayDurationOk,
awayHours,
awayMinutes,
browserOffset,
isOffsetZone,
timezoneOffsets,
@@ -17,10 +15,8 @@
let editing = $state(false);
let dn = $state('');
let tz = $state('+00:00');
let startH = $state('00');
let startM = $state('00');
let endH = $state('07');
let endM = $state('00');
let awayStart = $state('00:00');
let awayEnd = $state('07:00');
let blockChat = $state(false);
let blockFriendRequests = $state(false);
let emailInput = $state('');
@@ -31,25 +27,21 @@
const b = browserOffset();
return timezoneOffsets.includes(b) ? b : '+00:00';
}
function splitTime(hhmm: string): [string, string] {
const m = /^(\d{2}):(\d{2})$/.exec(hhmm);
if (!m) return ['00', '00'];
return [m[1], awayMinutes.includes(m[2]) ? m[2] : '00'];
function clockOr(value: string, fallback: string): string {
return /^\d{2}:\d{2}$/.test(value) ? value : fallback;
}
function startEdit() {
const p = app.profile!;
dn = p.displayName;
tz = isOffsetZone(p.timeZone) && timezoneOffsets.includes(p.timeZone) ? p.timeZone : defaultTz();
[startH, startM] = splitTime(p.awayStart);
[endH, endM] = splitTime(p.awayEnd);
awayStart = clockOr(p.awayStart, '00:00');
awayEnd = clockOr(p.awayEnd, '07:00');
blockChat = p.blockChat;
blockFriendRequests = p.blockFriendRequests;
editing = true;
}
const awayStart = $derived(`${startH}:${startM}`);
const awayEnd = $derived(`${endH}:${endM}`);
const nameOk = $derived(validDisplayName(dn));
const awayOk = $derived(awayDurationOk(awayStart, awayEnd));
const formValid = $derived(nameOk && awayOk);
@@ -120,16 +112,8 @@
<fieldset class="away" class:invalid={!awayOk}>
<legend>{t('profile.awayWindow')}</legend>
<div class="times">
<span class="tlabel">{t('profile.from')}</span>
<select bind:value={startH}>{#each awayHours as h (h)}<option>{h}</option>{/each}</select>
<span class="colon">:</span>
<select bind:value={startM}>{#each awayMinutes as m (m)}<option>{m}</option>{/each}</select>
</div>
<div class="times">
<span class="tlabel">{t('profile.to')}</span>
<select bind:value={endH}>{#each awayHours as h (h)}<option>{h}</option>{/each}</select>
<span class="colon">:</span>
<select bind:value={endM}>{#each awayMinutes as m (m)}<option>{m}</option>{/each}</select>
<label><span class="tlabel">{t('profile.from')}</span><input type="time" step="600" bind:value={awayStart} /></label>
<label><span class="tlabel">{t('profile.to')}</span><input type="time" step="600" bind:value={awayEnd} /></label>
</div>
<p class="muted">{t('profile.awayHint')}</p>
</fieldset>
@@ -271,17 +255,22 @@
}
.times {
display: flex;
align-items: center;
gap: 8px;
gap: 12px;
}
.times label {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.times input {
width: 100%;
box-sizing: border-box;
}
.tlabel {
min-width: 2.5em;
color: var(--text-muted);
font-size: 0.85rem;
}
.colon {
font-weight: 700;
}
.check {
flex-direction: row !important;
align-items: center;