Improve lobby screen header design

This commit is contained in:
Yuri Restia committed 2026-10-10 04:18:40 +07:00
1 parent 1db8e51023
commit 4bff948290
8 files changed
+49 -21

No files matched your search

+15
View File
@@ -1030,6 +1030,12 @@
.topbar__center { display: flex; flex-direction: column; align-items: center; gap: .03rem; text-align: center; }
.topbar__title { display: flex; align-items: center; gap: .18rem; font-size: calc(.3 * var(--t)); font-weight: 700; letter-spacing: .1em; line-height: 1.2; }
.topbar__sep { width: 1px; height: .28rem; background: var(--line-2); }
/* the lobby's ⚙ / 📖 / install squares: a size up from `sm` and spaced apart; on touch screens at least 40 px each with
a 10 px gap, so their 44 px hit areas (css/devices.css) never overlap and a tap lands on the intended one */
.topbar__tools { display: flex; align-items: center; gap: .14rem; }
.topbar__tools .btn--square.btn--sm { --sq: .46rem; height: var(--sq); min-width: var(--sq); }
.sp-coarse .topbar__tools { gap: max(.16rem, 10px); }
.sp-coarse .topbar__tools .btn--square.btn--sm { --sq: max(.56rem, 40px); }
.topbar__right { justify-self: end; display: flex; align-items: center; gap: .16rem; min-width: 0; }
.section-label { display: flex; align-items: baseline; gap: .12rem; margin: 0 0 .14rem; font-size: calc(.2 * var(--t)); font-weight: 700; letter-spacing: .08em; }
@@ -1038,6 +1044,15 @@
.me-chip { display: flex; align-items: center; gap: .12rem; padding: .06rem .18rem .06rem .06rem; border: 1px solid var(--line); background: rgba(0, 0, 0, .35); }
.me-chip__text { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
.me-chip__name { font-weight: 700; font-size: calc(.18 * var(--t)); max-width: 2.4rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the lobby's chip doubles as the 统计数据 entry */
.me-chip--btn {
font: inherit; color: inherit; text-align: left; cursor: pointer;
transition: border-color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.me-chip--btn:hover { border-color: var(--mint-700); background: rgba(34, 42, 38, .6); }
.me-chip--btn:active { transform: translateY(1px); }
.me-chip__stats { flex: none; width: .2rem; height: .2rem; margin-left: .04rem; color: var(--mint-500); opacity: .7; }
.me-chip--btn:hover .me-chip__stats { opacity: 1; }
.crash { align-items: center; justify-content: center; }
.crash__box {
+3
View File
@@ -520,6 +520,9 @@
inside the label, so the label lays glyph and text out like the button itself */
.settings-btn .btn__label { display: inline-flex; align-items: center; gap: .1rem; }
.settings-btn .btn__icon { color: var(--mint-500); }
/* square (the lobby's gear alone): the label box drops out so the gear sizes and centres against the button, like an `icon` */
.settings-btn.btn--square .btn__label { display: contents; }
.settings-btn.btn--square .btn__icon { width: 62%; height: 62%; } /* the gear glyph is drawn smaller in its box than the book */
.set-list { display: flex; flex-direction: column; gap: .16rem; padding-top: .06rem; }
.set-row { display: grid; grid-template-columns: minmax(0, 3.3rem) minmax(0, 1fr) auto; align-items: center; gap: .18rem; min-height: .44rem; }
/* The label column may shrink (the label wraps) instead of pushing its control out of the modal: at 设置 →「文字大小」
+11 -8
View File
@@ -309,25 +309,28 @@ export function LobbyScreen() {
<div class="topbar__left">
<${Button} variant="ghost" size="sm" icon="chevronLeft" onClick=${backToTitle} title=${t('返回标题')}>${t('返回')}<//>
<${PingPill} ms=${conn.ping} online=${online} />
<span class="topbar__sep" aria-hidden="true"></span>
<div class="topbar__tools">
<${SettingsButton} class="lobby-settings" variant="secondary" square=${true} />
<${GuideButton} class="lobby-guide" variant="secondary" square=${true} />
<${PwaInstallButton} class="lobby-pwa" square=${true} />
</div>
</div>
<div class="topbar__center">
<${MicroLabel} tone="mint">SIMULATION PROTOCOL SELECT<//>
<h1 class="topbar__title">${t('选择模拟协议')}</h1>
</div>
<div class="topbar__right">
<${Button} variant="secondary" size="sm" icon="chart" class="stats-entry" onClick=${openStats} title=${t('统计数据')} aria-label=${t('统计数据')}>${t('统计')}<//>
<${ResumeMatchButton} />
<${PwaInstallButton} class="lobby-pwa" />
<${SettingsButton} class="lobby-settings" variant="secondary" label=${t('设置')} />
<${GuideButton} class="lobby-guide" variant="secondary" label=${t('玩法说明')} />
<${LoadoutButton} from="lobby" size="sm" class="lobby-loadout" label=${t('干员调配')} />
<div class="me-chip">
<button type="button" class="me-chip me-chip--btn stats-entry" onClick=${openStats} title=${t('统计数据')} aria-label=${t('统计数据')}>
<${AvatarFrame} size="sm" name=${me.name} seat=${0} self=${true} />
<div class="me-chip__text">
<span class="me-chip__text">
<span class="me-chip__name">${me.name || t('博士')}</span>
<${MicroLabel}>${me.playerId != null ? `DOCTOR #${doctorNo(me.playerId)}` : 'DOCTOR'}<//>
</div>
</div>
</span>
<${Icon} name="chart" class="me-chip__stats" />
</button>
</div>
</header>
+2
View File
@@ -76,6 +76,8 @@ export const ICONS = {
wifiOff: { d: 'M2 17h3v4H2zm6-4h3v8H8zm6-4h3v12h-3zm6-4h3v16h-3zM2.4 1 23 21.6 21.6 23 1 2.4z' },
expand: { d: 'M3 3h7v2H5v5H3zm11 0h7v7h-2V5h-5zM3 14h2v5h5v2H3zm16 0h2v7h-7v-2h5z' },
collapse: { d: 'M8 3h2v7H3V8h5zm6 0h2v5h5v2h-7zM3 14h7v7H8v-5H3zm11 0h7v2h-5v5h-2z' },
// 添加到桌面 (add to home screen): a house outline with a + inside
homePlus: { d: 'M12 2 22 10.5V21H2V10.5zm0 2.6-8 6.8V19h16v-7.6zM11 10h2v2.5h2.5v2H13V17h-2v-2.5H8.5v-2H11z', eo: true },
rotate: { d: 'M7 2h8a2 2 0 0 1 2 2v7h-2V4H7v16h4v2H7a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm8 12h5a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2zm0 2v4h5v-4z', eo: true },
book: { d: 'M2 4h7.5A3.5 3.5 0 0 1 12 5.1 3.5 3.5 0 0 1 14.5 4H22v16h-7.5a1.5 1.5 0 0 0-1.5 1.5h-2A1.5 1.5 0 0 0 9.5 20H2zm2 2v12h5.5c.5 0 1 .1 1.5.3V7.5A1.5 1.5 0 0 0 9.5 6zm10.5 0A1.5 1.5 0 0 0 13 7.5v10.8c.5-.2 1-.3 1.5-.3H20V6z', eo: true },
// 观战 (spectator seats; the same eye as gameComponents.js GIcon 'eye')
+8 -4
View File
@@ -163,13 +163,17 @@ export function FullscreenButton({ class: cls = '' }) {
</button>`;
}
/** Only a browser-issued install offer shows a button; installed and unsupported environments stay quiet. */
export function PwaInstallButton({ class: cls = '', size = 'sm' }) {
/**
* Only a browser-issued install offer shows a button; installed and unsupported environments stay quiet.
* `square`: the glyph alone (the lobby's top bar, beside ⚙ / 📖), the name stays title / aria-label.
*/
export function PwaInstallButton({ class: cls = '', size = 'sm', square = false }) {
const [available, setAvailable] = useState(pwaInstall.available);
useEffect(() => pwaInstall.subscribe(setAvailable), []);
if (!available) return null;
return html`<${Button} variant="secondary" size=${size} icon="download" class=${cls}
data-testid="pwa-install" onClick=${() => pwaInstall.request()}>${t('添加到桌面')}<//>`;
return html`<${Button} variant="secondary" size=${size} icon="homePlus" square=${square} class=${cls}
title=${t('添加到桌面')} aria-label=${t('添加到桌面')}
data-testid="pwa-install" onClick=${() => pwaInstall.request()}>${square ? null : t('添加到桌面')}<//>`;
}
export function updateRotateHintPwaI18n(doc = globalThis.document) {
+5 -4
View File
@@ -289,11 +289,12 @@ export function SettingsModal({ open, onClose }) {
* The 设置 button of the lobby and the room (GitHub #238 — before it the settings were reachable only from the title
* screen and a running match): the twin of the 玩法说明 button (ui/guide.js GuideButton), with the settings modal behind it
* (mounted only while open). The same modal as the title screen's and the match's: nothing in it is match-only.
* @param {{ class?: string, size?: 'sm'|'md'|'lg'|'xl', variant?: string, label?: string }} props
* `square`: the gear alone (the lobby's top bar), the name stays title / aria-label — like GuideButton's.
* @param {{ class?: string, size?: 'sm'|'md'|'lg'|'xl', variant?: string, label?: string, square?: boolean }} props
*/
export function SettingsButton({ class: cls, size = 'sm', variant = 'ghost', label = t('设置') }) {
export function SettingsButton({ class: cls, size = 'sm', variant = 'ghost', label = t('设置'), square = false }) {
const [open, setOpen] = useState(false);
return html`<${Button} variant=${variant} size=${size} class=${cx('settings-btn', cls)} onClick=${() => setOpen(true)}
title=${t('设置')} aria-label=${t('设置')} data-testid="settings-btn"><${GIcon} name="gear" class="btn__icon" />${label}<//>
return html`<${Button} variant=${variant} size=${size} square=${square} class=${cx('settings-btn', cls)} onClick=${() => setOpen(true)}
title=${t('设置')} aria-label=${t('设置')} data-testid="settings-btn"><${GIcon} name="gear" class="btn__icon" />${square ? null : label}<//>
${open ? html`<${SettingsModal} open=${true} onClose=${() => setOpen(false)} />` : null}`;
}
+2 -2
View File
@@ -36,7 +36,7 @@ describe('the settings entry of the lobby and the room', { skip: !ENABLED && 'se
const settingsBeforeGuide = (page, screen, guideClass) => page.evaluate((s, g) => {
const btn = document.querySelector(`${s} [data-testid="settings-btn"]`);
const guide = document.querySelector(`${s} .${g}`);
return !!btn && !!guide && btn.nextElementSibling === guide && /设置/.test(btn.textContent) && btn.querySelector('svg.btn__icon') != null;
return !!btn && !!guide && btn.nextElementSibling === guide && /设置/.test(btn.textContent || btn.getAttribute('aria-label')) && btn.querySelector('svg.btn__icon') != null;
}, screen, guideClass);
const modalText = (page) => page.evaluate(() => document.querySelector('.modal')?.textContent.replace(/\s+/g, ' ').trim() ?? null);
@@ -53,7 +53,7 @@ describe('the settings entry of the lobby and the room', { skip: !ENABLED && 'se
await page.waitForSelector('.lobby-screen', { timeout: 15000 });
await sleep(500);
assert.ok(await settingsBeforeGuide(page, '.lobby-screen .topbar__right', 'lobby-guide'), '设置 sits right before 玩法说明 in the lobby\'s top bar');
assert.ok(await settingsBeforeGuide(page, '.lobby-screen .topbar__left', 'lobby-guide'), '设置 (gear) sits right before 玩法说明 (book) on the left of the lobby\'s top bar');
assert.equal(await modalText(page), null, 'closed until asked');
await page.click('.lobby-screen [data-testid="settings-btn"]');
await page.waitForSelector('.modal', { timeout: 5000 });
+3 -3
View File
@@ -88,8 +88,8 @@ describe('统计数据 page (real server, headless Chrome)', { skip: !ENABLED &&
}
const texts = (c, sel) => c.page.$$eval(sel, (els) => els.map((e) => e.innerText.replace(/\s+/g, ' ').trim()));
const stored = (c) => c.page.evaluate(() => { const r = localStorage.getItem('sp.pref.stats'); return r ? JSON.parse(r) : null; });
const openStats = async (c, from = '.lobby-screen .topbar__right .btn') => {
await c.click(from, '统计');
const openStats = async (c, from = '.lobby-screen .me-chip--btn') => {
await c.click(from, from.includes('me-chip') ? null : '统计');
await c.page.waitForSelector('.st .st-tab', { visible: true, timeout: 15000 });
};
const toast = (c) => c.page.waitForFunction(() => document.querySelector('.toast')?.textContent || false, { timeout: 8000 }).then((h) => h.jsonValue());
@@ -305,7 +305,7 @@ describe('统计数据: the real flows (a fast real server, headless Chrome)', {
assert.equal(env.records[0].players[0].roundsPassed, 1, 'one round cleared — the server\'s own count for a quit in round 2');
assert.ok(env.records[0].players[0].bandId, 'its strategy');
// the page: one game counted, one left out, both in the history
await c.click('.lobby-screen .topbar__right .btn', '统计');
await c.click('.lobby-screen .me-chip--btn');
await c.page.waitForSelector('.st .st-card', { visible: true });
assert.equal((await c.page.$$eval('.st-card__num', (els) => els.map((e) => e.textContent)))[0], '1');
assert.match(await c.page.$eval('.st-rule', (e) => e.textContent), /另有 1 局未计入统计/);