feat(ui): landscape iteration — reorder left panel, enable board zoom
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m13s
Left panel order is now score / history / status / rack / controls (the history fills the middle and scrolls). Board zoom is re-enabled in landscape with the same gestures as portrait, but height-driven: the viewport is the full right pane, the board fits by height as a centred square when zoomed out, and on zoom-in it magnifies past the pane and pans within it (focus-centred scroll that rides the magnify transition), occupying the full width up to the left panel. Board.svelte gains a landscape prop; portrait stays width-driven and unchanged. landscape.spec.ts now asserts zoom works (the zoomed board overflows the pane).
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
flash,
|
||||
centre,
|
||||
zoomed,
|
||||
landscape,
|
||||
variant,
|
||||
labelMode,
|
||||
lines,
|
||||
@@ -34,6 +35,10 @@
|
||||
flash: boolean;
|
||||
centre: { row: number; col: number };
|
||||
zoomed: boolean;
|
||||
/** Landscape layout: the viewport is the full (wide) right pane and the board fits by HEIGHT —
|
||||
* a square sized from the smaller pane dimension, centred and panned on zoom. Portrait fills
|
||||
* the (square) viewport by width. */
|
||||
landscape: boolean;
|
||||
variant: Variant;
|
||||
labelMode: BoardLabelMode;
|
||||
/** Draw 1px grid lines between cells; when false the board is a gapless checkerboard. */
|
||||
@@ -85,19 +90,40 @@
|
||||
const clientW = vp.clientWidth;
|
||||
const clientH = vp.clientHeight;
|
||||
if (clientW === 0) return;
|
||||
const fitW = clientW; // board width at scale 1 (fills the viewport)
|
||||
const fullW = clientW * Z; // board width at full zoom
|
||||
// Landscape fits the board by HEIGHT into a wide viewport (the full right pane), so the board
|
||||
// side is the smaller viewport dimension; portrait fills the (square) viewport by width.
|
||||
// fullSide is the board's side at full zoom — the board is square, so it bounds both axes.
|
||||
const fitSide = landscape ? Math.min(clientW, clientH) : clientW;
|
||||
const fullSide = fitSide * Z;
|
||||
let finalSL = 0;
|
||||
let finalST = 0;
|
||||
if (on && f) {
|
||||
const cell = fullW / 15;
|
||||
finalSL = Math.max(0, Math.min((f.col + 0.5) * cell - clientW / 2, fullW - clientW));
|
||||
finalST = Math.max(0, Math.min((f.row + 0.5) * cell - clientH / 2, fullW - clientH));
|
||||
const cell = fullSide / 15;
|
||||
finalSL = Math.max(0, Math.min((f.col + 0.5) * cell - clientW / 2, fullSide - clientW));
|
||||
finalST = Math.max(0, Math.min((f.row + 0.5) * cell - clientH / 2, fullSide - clientH));
|
||||
}
|
||||
const toggled = on !== prevZoom;
|
||||
const recentered = rc !== prevRecenter;
|
||||
prevZoom = on;
|
||||
prevRecenter = rc;
|
||||
if (landscape) {
|
||||
// Height-driven board in a wide viewport: pan so the focused cell is centred. The board
|
||||
// magnifies over the .scaler.land width/height transition, so the focus-centred scroll target
|
||||
// is only reachable once the board has grown — clamp it to the CURRENT scrollable size each
|
||||
// frame and ride the transition over a fixed time budget. (A scrollWidth-progress tween like
|
||||
// portrait's never settles here: zoom-out shrinks the board below the viewport, where it
|
||||
// stops overflowing.)
|
||||
if (toggled || (recentered && on && f)) {
|
||||
const t0 = performance.now();
|
||||
let raf = requestAnimationFrame(function tick(now: number) {
|
||||
vp.scrollLeft = Math.min(finalSL, Math.max(0, vp.scrollWidth - vp.clientWidth));
|
||||
vp.scrollTop = Math.min(finalST, Math.max(0, vp.scrollHeight - vp.clientHeight));
|
||||
if (now - t0 < 320) raf = requestAnimationFrame(tick);
|
||||
});
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!toggled) {
|
||||
// No zoom change: pan straight to the focused word only on an explicit recenter request (the
|
||||
// hint), never on an incidental re-run such as a viewport resize.
|
||||
@@ -109,8 +135,8 @@
|
||||
}
|
||||
const startSL = vp.scrollLeft;
|
||||
const startST = vp.scrollTop;
|
||||
const fromW = on ? fitW : fullW; // board width when this transition begins
|
||||
const toW = on ? fullW : fitW;
|
||||
const fromW = on ? fitSide : fullSide; // board side when this transition begins
|
||||
const toW = on ? fullSide : fitSide;
|
||||
let raf = requestAnimationFrame(function tick() {
|
||||
const curW = vp.scrollWidth || fromW;
|
||||
let prog = (curW - fromW) / (toW - fromW);
|
||||
@@ -206,8 +232,8 @@
|
||||
const key = (r: number, c: number) => `${r},${c}`;
|
||||
</script>
|
||||
|
||||
<div class="viewport" class:zoomed bind:this={viewport}>
|
||||
<div class="scaler" style="--z: {z};">
|
||||
<div class="viewport" class:zoomed class:land={landscape} bind:this={viewport}>
|
||||
<div class="scaler" class:land={landscape} style="--z: {z};">
|
||||
<div class="grid" class:gridless={!lines}>
|
||||
{#each board as rowCells, r (r)}
|
||||
{#each rowCells as cell, c (c)}
|
||||
@@ -267,6 +293,24 @@
|
||||
transition: width 0.25s ease;
|
||||
container-type: inline-size;
|
||||
}
|
||||
/* Landscape: the viewport is the full (wide) right pane, not a square. The board fits by HEIGHT
|
||||
— the scaler is a square of side min(pane) × zoom, centred (margin auto) when smaller than the
|
||||
pane and panned (native scroll) once the zoom grows it past the pane. The min(100cqw,100cqh)
|
||||
resolves against the .rightpane size container (the scaler's nearest ANCESTOR query container),
|
||||
so it is not circular with the scaler's own inline-size container, which only sizes the
|
||||
descendant cell labels. */
|
||||
.viewport.land {
|
||||
height: 100%;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
.scaler.land {
|
||||
width: calc(min(100cqw, 100cqh) * var(--z));
|
||||
height: calc(min(100cqw, 100cqh) * var(--z));
|
||||
margin: auto;
|
||||
transition:
|
||||
width 0.25s ease,
|
||||
height 0.25s ease;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(15, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user