Stage 8 polish: keyboard-aware modals, consistent select pickers, required game type
Tests · UI / test (push) Successful in 17s
Tests · UI / test (push) Successful in 17s
Third owner-review pass (iPhone): - Modals (and the chat) size their backdrop to window.visualViewport, so they stay fully above the software keyboard (dvh alone left the sheet partly behind it). - On the owner's call, every profile / new-game picker is a native <select> for consistent cross-platform behaviour: the away window returns to hour + 10-minute selects (which also avoids the iOS time-wheel "clear" button), alongside the offset timezone and the game-type / move-time / hints selects. Native time/wheel inputs render differently per OS and cannot be forced to match. - New-game "play with friends" has no preselected game type — an explicit, required pick (empty placeholder); Send invitation stays disabled until both a type and a friend are chosen. A smart default (from play history / language) is TODO-6.
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
import { t } from '../lib/i18n/index.svelte';
|
||||
import {
|
||||
awayDurationOk,
|
||||
awayHours,
|
||||
awayMinutes,
|
||||
browserOffset,
|
||||
isOffsetZone,
|
||||
timezoneOffsets,
|
||||
@@ -15,8 +17,12 @@
|
||||
let editing = $state(false);
|
||||
let dn = $state('');
|
||||
let tz = $state('+00:00');
|
||||
let awayStart = $state('00:00');
|
||||
let awayEnd = $state('07:00');
|
||||
// Away start/end as hour + 10-minute parts, so the picker is a <select> like every
|
||||
// other profile control (consistent native control across iOS / desktop).
|
||||
let startH = $state('00');
|
||||
let startM = $state('00');
|
||||
let endH = $state('07');
|
||||
let endM = $state('00');
|
||||
let blockChat = $state(false);
|
||||
let blockFriendRequests = $state(false);
|
||||
let emailInput = $state('');
|
||||
@@ -27,21 +33,25 @@
|
||||
const b = browserOffset();
|
||||
return timezoneOffsets.includes(b) ? b : '+00:00';
|
||||
}
|
||||
function clockOr(value: string, fallback: string): string {
|
||||
return /^\d{2}:\d{2}$/.test(value) ? value : fallback;
|
||||
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 startEdit() {
|
||||
const p = app.profile!;
|
||||
dn = p.displayName;
|
||||
tz = isOffsetZone(p.timeZone) && timezoneOffsets.includes(p.timeZone) ? p.timeZone : defaultTz();
|
||||
awayStart = clockOr(p.awayStart, '00:00');
|
||||
awayEnd = clockOr(p.awayEnd, '07:00');
|
||||
[startH, startM] = splitTime(p.awayStart);
|
||||
[endH, endM] = splitTime(p.awayEnd);
|
||||
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);
|
||||
@@ -112,8 +122,16 @@
|
||||
<fieldset class="away" class:invalid={!awayOk}>
|
||||
<legend>{t('profile.awayWindow')}</legend>
|
||||
<div class="times">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<p class="muted">{t('profile.awayHint')}</p>
|
||||
</fieldset>
|
||||
@@ -255,22 +273,17 @@
|
||||
}
|
||||
.times {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.times label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.times input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user