v0.16.1 web client update: inline favicon, board fills the window, large-font phones
This commit is contained in:
1 parent
664cf9654e
commit
0461020c46
4 files changed
+89
-32
No files matched your search
@@ -2202,6 +2202,14 @@ function defaultFlip(history) {
|
||||
return white?.type === 2 && black?.type !== 2;
|
||||
}
|
||||
|
||||
// The exit button's text, its short form for a phone's row, and its
|
||||
// accessible name for the row that shows the icon alone.
|
||||
function setReplayExitLabel(text, short) {
|
||||
$('replay-exit-label').textContent = text;
|
||||
$('replay-exit-short').textContent = short;
|
||||
$('replay-exit').setAttribute('aria-label', text);
|
||||
}
|
||||
|
||||
function exitReplay({ push = true } = {}) {
|
||||
if (!replay.active) return;
|
||||
stopAutoplay();
|
||||
@@ -2221,7 +2229,8 @@ function exitReplay({ push = true } = {}) {
|
||||
}
|
||||
|
||||
function renderReplayLoading() {
|
||||
$('replay-result').textContent = '';
|
||||
$('replay-score').textContent = '';
|
||||
$('replay-term').textContent = '';
|
||||
$('replay-ply').textContent = '';
|
||||
$('replay-players').textContent = 'Loading game…';
|
||||
$('replay-meta').textContent = replay.gameId || '';
|
||||
@@ -2229,7 +2238,7 @@ function renderReplayLoading() {
|
||||
grid.innerHTML = '';
|
||||
$('replay-moves-empty').textContent = 'Loading…';
|
||||
setReplayNavEnabled(false);
|
||||
$('replay-exit').textContent = live.gameId ? 'Back to live game' : 'Close replay';
|
||||
setReplayExitLabel(live.gameId ? 'Back to live game' : 'Close replay', live.gameId ? 'Live' : 'Close');
|
||||
}
|
||||
|
||||
function renderReplayError(message) {
|
||||
@@ -2250,9 +2259,11 @@ function setReplayNavEnabled(enabled) {
|
||||
function renderReplayHead() {
|
||||
const h = replay.history;
|
||||
const finished = !!h.result;
|
||||
$('replay-result').textContent = finished
|
||||
? `${h.pgnResult} · ${terminationLabel(h.termination) || describeResult(h.result, h.termination)}`
|
||||
: 'Ongoing';
|
||||
// Score and termination apart: a large-font phone shows the score alone.
|
||||
$('replay-score').textContent = finished ? h.pgnResult : 'Ongoing';
|
||||
$('replay-term').textContent = finished
|
||||
? ` · ${terminationLabel(h.termination) || describeResult(h.result, h.termination)}`
|
||||
: '';
|
||||
$('replay-players').textContent = `${sideLabel(h.players?.white)} vs ${sideLabel(h.players?.black)}`;
|
||||
$('replay-players').title = `White: ${sideLabel(h.players?.white)} · Black: ${sideLabel(h.players?.black)}`;
|
||||
const plies = h.moves.length;
|
||||
@@ -2264,7 +2275,7 @@ function renderReplayHead() {
|
||||
: `resignation at ply ${h.concession.ply}`);
|
||||
}
|
||||
$('replay-meta').textContent = meta.join(' · ');
|
||||
$('replay-exit').textContent = live.gameId ? 'Back to live game' : 'Close replay';
|
||||
setReplayExitLabel(live.gameId ? 'Back to live game' : 'Close replay', live.gameId ? 'Live' : 'Close');
|
||||
setReplayNavEnabled(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Chess</title>
|
||||
<!-- The brand mark as the tab icon, inline so no request leaves the page
|
||||
(the host answers /favicon.ico with its 404 page) and CSP img-src data: covers it. -->
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='41 34 430 430'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='20%' y1='0%' x2='80%' y2='100%'%3E%3Cstop offset='0%' stop-color='%23C8CDD6'/%3E%3Cstop offset='45%' stop-color='%238B929E'/%3E%3Cstop offset='100%' stop-color='%234F545D'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cg fill='url(%23g)' stroke='%2314161A' stroke-width='4' stroke-linejoin='round'%3E%3Cpath d='M136 450Q256 465 376 450L346 410Q256 420 166 410Z'/%3E%3Cpath d='M166 410Q256 420 346 410L316 370Q256 380 196 370Z'/%3E%3Cpath d='M196 370Q256 380 316 370C300 290 286 230 282 205H230C226 230 212 290 196 370Z'/%3E%3C/g%3E%3Cpath d='M330 90Q265 96 230 120Q265 144 335 150Z' fill='%23111318'/%3E%3Cg stroke='%23F0F2F5' stroke-width='6' stroke-linecap='round'%3E%3Cpath d='M263.17 85V155M299.33 85V155'/%3E%3C/g%3E%3Cg fill='url(%23g)' stroke='%2314161A' stroke-width='4' stroke-linejoin='round'%3E%3Cpath d='M226 200C150 180 150 70 256 40C290 50 315 65 330 90Q265 96 230 120Q265 144 335 150C335 170 315 190 286 200Z'/%3E%3Crect x='216' y='195' width='80' height='16' rx='8'/%3E%3C/g%3E%3C/svg%3E">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body data-mode="live">
|
||||
@@ -98,7 +101,7 @@
|
||||
<div class="replay-head">
|
||||
<div class="replay-title-row">
|
||||
<span class="badge" id="replay-badge">Replay</span>
|
||||
<span class="replay-result" id="replay-result"></span>
|
||||
<span class="replay-result" id="replay-result"><span id="replay-score"></span><span class="replay-term" id="replay-term"></span></span>
|
||||
<span class="replay-ply" id="replay-ply" aria-hidden="true"></span>
|
||||
</div>
|
||||
<div class="replay-players" id="replay-players"></div>
|
||||
@@ -155,8 +158,12 @@
|
||||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1.5 1.5M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1.5-1.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<span>Link</span>
|
||||
</button>
|
||||
<!-- A row of its own beside the board; on a phone it shares this row. -->
|
||||
<button id="replay-exit" class="btn btn-secondary btn-exit" type="button" aria-label="Close replay">
|
||||
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<span id="replay-exit-label">Close replay</span><span class="label-short" id="replay-exit-short" aria-hidden="true">Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<button id="replay-exit" class="btn btn-secondary btn-block" type="button">Back to live game</button>
|
||||
<div id="ply-announcer" class="visually-hidden" aria-live="polite" aria-atomic="true"></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -45,17 +45,16 @@
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
|
||||
|
||||
/* Side-by-side sizes: the board edge includes its coordinate margin. It
|
||||
grows with the window but never past what is left under the top bar
|
||||
(the host's 30rem frame leaves 416px), so the embed keeps a 360px
|
||||
board while a full window gets up to 560px. */
|
||||
/* Side-by-side sizes: the board edge includes its coordinate margin. The
|
||||
panel takes a quarter of the width between 260px and 420px; the board
|
||||
takes everything else the window leaves beside it and under the top
|
||||
bar, so the margins give way first, the board second, and the panel
|
||||
is never pushed out of view. 3.5rem and 2rem are the app's padding and
|
||||
gaps (8px each side, a gap of up to 2rem beside the board) plus 8px of
|
||||
slack; the host's embed sizes its frame to this formula. */
|
||||
--topbar-height: 2.5rem;
|
||||
--panel-width: clamp(260px, 22vw, 380px);
|
||||
/* The board takes what the window leaves beside the panel and under the
|
||||
top bar, so the panel is never pushed out of view and the row never
|
||||
scrolls: 5rem covers the gaps and the frame's padding. The host's
|
||||
30rem desktop frame leaves 416px. */
|
||||
--board-edge: clamp(280px, min(100vw - var(--panel-width) - 5rem, 100vh - var(--topbar-height) - 2.5rem), 600px);
|
||||
--panel-width: clamp(260px, 24vw, 420px);
|
||||
--board-edge: clamp(280px, min(100vw - var(--panel-width) - 3.5rem, 100vh - var(--topbar-height) - 2rem), 840px);
|
||||
--board-pad: clamp(14px, 1.04vw, 20px);
|
||||
}
|
||||
|
||||
@@ -549,6 +548,11 @@ body[data-mode="replay"] .pane-live { display: none; }
|
||||
|
||||
/* --- Replay pane ----------------------------------------------------- */
|
||||
|
||||
/* The export row wraps the exit button onto a row of its own. */
|
||||
.pane-replay .toolbar { flex-wrap: wrap; row-gap: 0.6rem; }
|
||||
.pane-replay .btn-exit { flex: 1 0 100%; }
|
||||
.pane-replay .btn-exit .label-short { display: none; }
|
||||
|
||||
.replay-head {
|
||||
flex: none;
|
||||
display: flex;
|
||||
@@ -1090,13 +1094,15 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
@media (max-width: 699px) {
|
||||
:root {
|
||||
/* Reserve under the board, in rem so a larger system font grows it:
|
||||
top bar 2.5 + gap 0.4 + turn strip 2 + controls 2.25 + move list 6
|
||||
+ toolbar 2.25 + gaps and padding 2.35 = 17.75rem. Small viewport
|
||||
units, so the browser's bars never cover the panel. The replay
|
||||
top bar 2.5 + gap 0.4 + turn strip 2 + controls 2.25 + move list 4
|
||||
+ toolbar 2.25 + gaps and padding 2.35 = 15.75rem. Small viewport
|
||||
units, so the browser's bars never cover the panel. At a 1.5x
|
||||
system font (rem 24px) on a 412x734 viewport this still leaves a
|
||||
356px board; the move list scrolls inside its 4rem. The replay
|
||||
pane is taller (head, navigation, export, exit), so a replay
|
||||
reserves more and the board gives way on a short screen rather
|
||||
than the controls dropping below the fold. */
|
||||
--board-edge: max(240px, min(100vw, calc(100svh - 17.75rem)));
|
||||
--board-edge: max(240px, min(100vw, calc(100svh - 15.75rem)));
|
||||
--board-pad: 14px;
|
||||
--panel-width: 100%;
|
||||
--panel-height: auto;
|
||||
@@ -1104,7 +1110,9 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
}
|
||||
|
||||
body { overflow-y: auto; overflow-x: hidden; }
|
||||
body[data-mode="replay"] { --board-edge: max(240px, min(100vw, calc(100svh - 23.25rem))); }
|
||||
/* A replay's panel: head 2.3 + list 4 + navigation 2.2 + export row 2.25
|
||||
+ gaps and padding 2.85, under the top bar 2.9 = 16.5rem. */
|
||||
body[data-mode="replay"] { --board-edge: max(240px, min(100vw, calc(100svh - 16.5rem))); }
|
||||
|
||||
/* The app is exactly the viewport, so the panel and its move list shrink
|
||||
to fit; only when their minimums do not fit does the page scroll. */
|
||||
@@ -1167,6 +1175,8 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
padding: 0.5rem 0.6rem 0.6rem;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
container-type: inline-size;
|
||||
container-name: pane;
|
||||
}
|
||||
|
||||
.pane > * { min-width: 0; }
|
||||
@@ -1180,14 +1190,29 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
}
|
||||
.pane-live .controls .btn { padding: 0.5rem 0.2rem; font-size: 0.85rem; min-height: 2.25rem; height: auto; }
|
||||
|
||||
.moves { flex: 1 1 auto; min-height: 6rem; }
|
||||
.moves { flex: 1 1 auto; min-height: 4rem; }
|
||||
.move-grid { grid-template-columns: 2.2rem 1fr 1fr; font-size: 0.9rem; }
|
||||
|
||||
.toolbar .btn-tool { padding: 0.45rem 0.3rem; font-size: 0.82rem; min-width: 0; }
|
||||
|
||||
/* Replay: head, list, navigation, export and exit as rows. */
|
||||
.pane-replay .replay-head { padding: 0.35rem 0.6rem; }
|
||||
/* Replay: a one-line head (result, players, ply; no badge, no meta),
|
||||
the list, the navigation row, then export and exit on one row. */
|
||||
.pane-replay .replay-head {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
min-height: 2.3rem;
|
||||
}
|
||||
.pane-replay .replay-title-row { display: contents; }
|
||||
.pane-replay .badge { display: none; }
|
||||
.pane-replay .replay-result { flex: none; white-space: nowrap; }
|
||||
.pane-replay .replay-players { order: 1; flex: 1 1 auto; }
|
||||
.pane-replay .replay-ply { order: 2; margin-left: auto; }
|
||||
.pane-replay .replay-meta { display: none; }
|
||||
.pane-replay .btn-exit { flex: 1.3 1 0; min-width: 0; padding: 0.45rem 0.3rem; font-size: 0.82rem; }
|
||||
.pane-replay .btn-exit #replay-exit-label { display: none; }
|
||||
.pane-replay .btn-exit .label-short { display: inline; }
|
||||
.pane-replay .replay-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr) 1.4fr 1fr;
|
||||
@@ -1196,7 +1221,6 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
.pane-replay .replay-nav .speed { margin: 0; }
|
||||
.pane-replay .replay-nav .speed select { width: 100%; }
|
||||
.pane-replay .btn-nav { width: 100%; height: 2.2rem; }
|
||||
.pane-replay #replay-exit { padding: 0.5rem; font-size: 0.85rem; }
|
||||
|
||||
.games-modal { padding: 0.75rem; }
|
||||
.game-row { gap: 0.5rem; padding: 0.4rem; }
|
||||
@@ -1205,10 +1229,15 @@ input[type="range"]:focus-visible { outline: 2px solid var(--periwinkle); outlin
|
||||
.modal-overlay { padding: 0.5rem; }
|
||||
}
|
||||
|
||||
/* Very narrow phones: icon-only export buttons. */
|
||||
@media (max-width: 380px) {
|
||||
.pane-replay .toolbar .btn-tool span { display: none; }
|
||||
.pane-replay .toolbar .btn-tool .icon { width: 1.2rem; height: 1.2rem; }
|
||||
/* Narrow phones, and phones whose system font is large: the pane is a
|
||||
container on phones and rem in a container query is the root font size,
|
||||
so 24rem is 384px at the default size and 600px at a 1.5x font. Then the
|
||||
export buttons show icons alone and the replay head keeps the score and
|
||||
the players but not the termination. */
|
||||
@container pane (max-width: 24rem) {
|
||||
.pane-replay .toolbar .btn-tool span, .pane-replay .btn-exit span, .pane-replay .btn-exit .label-short { display: none; }
|
||||
.pane-replay .toolbar .btn-tool .icon, .pane-replay .btn-exit .icon { width: 1.2rem; height: 1.2rem; }
|
||||
.pane-replay .replay-term { display: none; }
|
||||
.pane-live .controls .btn { font-size: 0.8rem; }
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -167,7 +167,7 @@ test.describe('web replay', () => {
|
||||
await expect(page.locator('#replay-ply')).toHaveText('2 / 4');
|
||||
await expect(page.locator('#replay-result')).toContainText('0-1');
|
||||
await expect(page.locator('#replay-result')).toContainText('Checkmate');
|
||||
await expect(page.locator('#replay-exit')).toHaveText('Close replay');
|
||||
await expect(page.locator('#replay-exit-label')).toHaveText('Close replay');
|
||||
await expect(page.locator('#replay-moves .move').nth(3)).toHaveText('Qh4#');
|
||||
await page.keyboard.press('End');
|
||||
await expect(page.locator('[data-square="e1"]')).toHaveClass(/mated-king/);
|
||||
@@ -267,6 +267,16 @@ test.describe('web replay', () => {
|
||||
await expect(page.locator('#replay-exit')).toBeInViewport();
|
||||
const box = await page.locator('#replay-moves-box').evaluate(el => ({ scrolls: el.scrollHeight > el.clientHeight, atEnd: el.scrollTop > 0 }));
|
||||
expect(box).toEqual({ scrolls: true, atEnd: true });
|
||||
// A 1.5x system font (Android font scaling raises the root size): the
|
||||
// board keeps a usable size, the export row shows icons alone and
|
||||
// nothing overflows its button.
|
||||
await page.evaluate(() => { document.documentElement.style.fontSize = '25px'; });
|
||||
expect(await fits()).toEqual({ w: true, h: true });
|
||||
await expect(page.locator('#replay-exit')).toBeInViewport();
|
||||
expect((await page.locator('.board-wrapper').boundingBox()).width).toBeGreaterThanOrEqual(280);
|
||||
await expect(page.locator('#replay-exit-label')).toBeHidden();
|
||||
const clipped = await page.locator('.pane-replay .toolbar > *').evaluateAll(els => els.filter(el => el.scrollWidth > el.clientWidth + 1).length);
|
||||
expect(clipped).toBe(0);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user