Merge pull request 'feat(ui): open the move history at the newest move; scale the landscape rack glyphs' (#289) from feature/history-autoscroll-landscape-rack into development
CI / changes (push) Successful in 3s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m16s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m5s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
CI / changes (push) Successful in 3s
CI / unit (push) Has been skipped
CI / integration (push) Has been skipped
CI / ui (push) Successful in 1m16s
CI / conformance (push) Successful in 10s
CI / gate (push) Successful in 0s
CI / deploy (push) Successful in 2m5s
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 12s
CI / integration (pull_request) Successful in 24s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Has been skipped
This commit was merged in pull request #289.
This commit is contained in:
+10
-1
@@ -202,6 +202,11 @@ e2e and the screenshots.
|
|||||||
the **move history** — a fixed-height slide-down drawer whose bottom border (and its
|
the **move history** — a fixed-height slide-down drawer whose bottom border (and its
|
||||||
shadow) pins to the board as the board slides down, instead of tracking the table as
|
shadow) pins to the board as the board slides down, instead of tracking the table as
|
||||||
moves accumulate; its scrollbar gutter is reserved so the centred cells do not jitter. The
|
moves accumulate; its scrollbar gutter is reserved so the centred cells do not jitter. The
|
||||||
|
table is shown **scrolled to its newest row**: the grid fills top to bottom, oldest first, so an
|
||||||
|
untouched scroller would open on the opening moves — the least interesting end. It is pinned once
|
||||||
|
per showing (in landscape, once the moves reach the already-docked table), never on each arriving
|
||||||
|
move, so a player reading back through a long game is not yanked to the bottom by an opponent's
|
||||||
|
move. The
|
||||||
history is a **ruled matrix** — one column per seat, aligned under the score plaques, each
|
history is a **ruled matrix** — one column per seat, aligned under the score plaques, each
|
||||||
seat's moves filling its column top to bottom: no player names (the plaque heads the column)
|
seat's moves filling its column top to bottom: no player names (the plaque heads the column)
|
||||||
and no running total, just the word(s) and the move score, `WORD (12)`, centred. A non-play
|
and no running total, just the word(s) and the move score, `WORD (12)`, centred. A non-play
|
||||||
@@ -247,7 +252,11 @@ e2e and the screenshots.
|
|||||||
portrait layout is byte-for-byte unchanged; both layouts render from the same snippets, so the
|
portrait layout is byte-for-byte unchanged; both layouts render from the same snippets, so the
|
||||||
behaviour and markup stay single-sourced. The rack is a **seven-column grid** filling the tray
|
behaviour and markup stay single-sourced. The rack is a **seven-column grid** filling the tray
|
||||||
width in both layouts, so the tiles are square and as large as the row allows (the confirm ✅ takes
|
width in both layouts, so the tiles are square and as large as the row allows (the confirm ✅ takes
|
||||||
the fixed 7th slot while a play is staged).
|
the fixed 7th slot while a play is staged). The landscape tray shares the narrow left panel, so its
|
||||||
|
tiles — and with them the `cqw`-sized glyphs — come out smaller than the full-width portrait tray:
|
||||||
|
the **letter** and Erudit's **blank star** are scaled up a quarter there (a `(orientation:
|
||||||
|
landscape)` query in `Rack.svelte`) to stay comfortably legible, while the point value keeps its
|
||||||
|
size — it reads fine small, and growing it would crowd the letter on a tile that size.
|
||||||
- **Highlights**: while composing a play the staged tiles are **tinted by legality** — a light
|
- **Highlights**: while composing a play the staged tiles are **tinted by legality** — a light
|
||||||
green once they form a word (a shade darker on the committed board tiles the word runs through —
|
green once they form a word (a shade darker on the committed board tiles the word runs through —
|
||||||
in a **single-word game** only the main word lights up, since the engine ignores cross words),
|
in a **single-word game** only the main word lights up, since the engine ignores cross words),
|
||||||
|
|||||||
@@ -121,6 +121,32 @@ test('a two-finger pinch does not open the history (pinch-zoom vs swipe-down)',
|
|||||||
await expect(page.locator('.history')).toHaveCount(0);
|
await expect(page.locator('.history')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** tallHistoryRows stretches the move-table cells so the seeded four-move game overflows the
|
||||||
|
* table's scroller — the condition under which "opens at the newest move" is observable at all
|
||||||
|
* (with no overflow the assertion would hold vacuously). Injected at document start, since the
|
||||||
|
* table is pinned as soon as it is shown. */
|
||||||
|
async function tallHistoryRows(page: Page): Promise<void> {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = '.hcell { min-height: 200px !important; }';
|
||||||
|
document.head.append(style);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the history opens scrolled to the newest move', async ({ page }) => {
|
||||||
|
await tallHistoryRows(page);
|
||||||
|
await openGame(page);
|
||||||
|
await page.locator('.scoreboard').click(); // open the history
|
||||||
|
|
||||||
|
const m = await page
|
||||||
|
.locator('.hgridwrap')
|
||||||
|
.evaluate((el) => ({ top: el.scrollTop, scroll: el.scrollHeight, client: el.clientHeight }));
|
||||||
|
expect(m.scroll).toBeGreaterThan(m.client); // the table really overflows — no vacuous pass
|
||||||
|
expect(m.top).toBeGreaterThanOrEqual(m.scroll - m.client - 1); // pinned to the last row
|
||||||
|
});
|
||||||
|
|
||||||
test('a swipe-down on the zoomed-in board does not open the history (native scroll wins)', async ({ page }) => {
|
test('a swipe-down on the zoomed-in board does not open the history (native scroll wins)', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
// Double-tap an empty cell to zoom in (two synchronous clicks = a double-tap).
|
// Double-tap an empty cell to zoom in (two synchronous clicks = a double-tap).
|
||||||
|
|||||||
@@ -26,6 +26,27 @@ test('landscape lays the game in two columns with the history docked open', asyn
|
|||||||
await expect(page.locator('.leftpane .tabbar')).toBeVisible();
|
await expect(page.locator('.leftpane .tabbar')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the docked history shows the newest move', async ({ page }) => {
|
||||||
|
// Stretch the cells so the seeded four-move game overflows the docked table's scroller — the
|
||||||
|
// condition under which "shows the newest move" is observable at all. Injected at document
|
||||||
|
// start: in landscape the table is docked open from the mount, and it is pinned to its last row
|
||||||
|
// as soon as the moves arrive. The portrait (drawer) counterpart lives in history.spec.ts.
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = '.hcell { min-height: 200px !important; }';
|
||||||
|
document.head.append(style);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await openGame(page);
|
||||||
|
|
||||||
|
const m = await page
|
||||||
|
.locator('.hgridwrap')
|
||||||
|
.evaluate((el) => ({ top: el.scrollTop, scroll: el.scrollHeight, client: el.clientHeight }));
|
||||||
|
expect(m.scroll).toBeGreaterThan(m.client); // the table really overflows — no vacuous pass
|
||||||
|
expect(m.top).toBeGreaterThanOrEqual(m.scroll - m.client - 1); // pinned to the last row
|
||||||
|
});
|
||||||
|
|
||||||
test('landscape zooms the board on a double-tap and the zoomed board overflows the pane', async ({ page }) => {
|
test('landscape zooms the board on a double-tap and the zoomed board overflows the pane', async ({ page }) => {
|
||||||
await openGame(page);
|
await openGame(page);
|
||||||
// Double-tap an empty cell zooms in, same as portrait. The board is height-driven here, so the
|
// Double-tap an empty cell zooms in, same as portrait. The board is height-driven here, so the
|
||||||
|
|||||||
+24
-1
@@ -1358,6 +1358,29 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The move table's scroller (the grid between the sticky header and the pinned bag footer).
|
||||||
|
let hgridEl = $state<HTMLDivElement>();
|
||||||
|
// Whether the table has already been pinned to its newest row for the current showing.
|
||||||
|
// A plain flag (not $state): the effect below writes it, and tracking it would re-run the
|
||||||
|
// effect from its own write.
|
||||||
|
let historyPinned = false;
|
||||||
|
// The history opens showing the LATEST moves: the grid fills top to bottom (oldest first), so
|
||||||
|
// an untouched scroller would open on the opening moves — the least interesting end. It is
|
||||||
|
// pinned once per showing, not on every arriving move, so a player who has scrolled back
|
||||||
|
// through a long game is not yanked to the bottom by an opponent's move. In landscape the
|
||||||
|
// table is docked open from the start, so the moves usually arrive after it mounts — hence
|
||||||
|
// the wait for a non-empty journal rather than a plain "on open".
|
||||||
|
$effect(() => {
|
||||||
|
const el = hgridEl;
|
||||||
|
if (!historyShown || !el) {
|
||||||
|
historyPinned = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (historyPinned || moves.length === 0) return;
|
||||||
|
historyPinned = true;
|
||||||
|
el.scrollTop = el.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
// Friend state for the in-game "add friend" affordance (the 🤝 in each opponent's score
|
// Friend state for the in-game "add friend" affordance (the 🤝 in each opponent's score
|
||||||
// card while the history is open), derived from the server so it is correct across reloads
|
// card while the history is open), derived from the server so it is correct across reloads
|
||||||
// and live-updates when a request is answered: `friends` are the caller's accepted friends;
|
// and live-updates when a request is answered: `friends` are the caller's accepted friends;
|
||||||
@@ -1657,7 +1680,7 @@
|
|||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="hgridwrap">
|
<div class="hgridwrap" bind:this={hgridEl}>
|
||||||
<div class="hgrid" style="grid-template-columns: repeat({view.game.seats.length}, 1fr)">
|
<div class="hgrid" style="grid-template-columns: repeat({view.game.seats.length}, 1fr)">
|
||||||
{#each historyGrid(moves, view.game.seats.length, thinkingSeat) as row}
|
{#each historyGrid(moves, view.game.seats.length, thinkingSeat) as row}
|
||||||
{#each row as cell (cell.player)}
|
{#each row as cell (cell.player)}
|
||||||
|
|||||||
@@ -159,4 +159,19 @@
|
|||||||
font-size: 7.6vmin; /* cqw fallback for Chrome < 105 */
|
font-size: 7.6vmin; /* cqw fallback for Chrome < 105 */
|
||||||
font-size: 7.6cqw;
|
font-size: 7.6cqw;
|
||||||
}
|
}
|
||||||
|
/* Landscape docks the rack in the narrow left panel (Game.svelte's .leftpane), so its tiles —
|
||||||
|
and with them the cqw-sized glyphs — come out markedly smaller than the full-width portrait
|
||||||
|
tray. Scale the tile face up by a quarter there so the letters stay comfortably legible; the
|
||||||
|
point value keeps its size (it reads fine small, and growing it would crowd the letter on a
|
||||||
|
tile this size). The media query matches the one that selects the landscape layout. */
|
||||||
|
@media (orientation: landscape) {
|
||||||
|
.letter {
|
||||||
|
font-size: 7.5vmin; /* cqw fallback for Chrome < 105 */
|
||||||
|
font-size: 7.5cqw;
|
||||||
|
}
|
||||||
|
.star {
|
||||||
|
font-size: 9.5vmin; /* cqw fallback for Chrome < 105 */
|
||||||
|
font-size: 9.5cqw;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user