feat(ui): move in-game status to the board — highlight, score badge, full-width rack
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Failing after 13s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Failing after 0s
CI / deploy (pull_request) Has been skipped
Remove the under-board status strip and relocate its signals: - bag count -> a badge on the exchange/pass control + the foot of the move table - whose-turn / win-lose -> a thin strip above the score plaques - the tentative-move caption -> the board itself: staged tiles tint green (legal) or pink (illegal), the board tiles a formed word runs through go a shade darker, and an orange score badge sits on the main word (digit sized like a tile value, clamped on-board) The word geometry (covered cells + badge anchor) is a new pure client-side helper (ui/src/lib/formed.ts), independent of the move evaluator, so it works on the local or network preview path alike; the badge's number still comes from the preview score. Rack: a seven-column grid filling the tray width in both layouts — square, full-width tiles — with the confirm control in the fixed 7th slot. Settings: a touch-only "Zoom the board" toggle (default on, device-local) gates the tile-placement auto-zoom; taking a hint while zoomed in now zooms out so the highlighted hint word is never left off-screen. Docs (FUNCTIONAL +_ru, UI_DESIGN, ARCHITECTURE) and e2e/unit tests updated.
This commit is contained in:
@@ -23,6 +23,9 @@
|
||||
focus,
|
||||
recenter,
|
||||
dropTarget,
|
||||
previewLegal,
|
||||
formed,
|
||||
scoreBadge,
|
||||
oncell,
|
||||
ontogglezoom,
|
||||
onrecall,
|
||||
@@ -48,6 +51,15 @@
|
||||
recenter: number;
|
||||
/** The cell a dragged tile is currently aimed at, highlighted as a drop target. */
|
||||
dropTarget: { row: number; col: number } | null;
|
||||
/** While composing: true when the staged tiles form a legal play, false when not, null when
|
||||
* there is no preview (off-turn, nothing staged, or a recall drag) — tints the pending tiles
|
||||
* green vs a calm pink. */
|
||||
previewLegal: boolean | null;
|
||||
/** "row,col" keys of every committed board cell a formed word runs through, greened a shade
|
||||
* darker than the player's own staged tiles. */
|
||||
formed: Set<string>;
|
||||
/** The orange move-score badge for the legal play being composed, or null. */
|
||||
scoreBadge: { row: number; col: number; corner: 'tr' | 'bl'; score: number } | null;
|
||||
oncell: (row: number, col: number) => void;
|
||||
ontogglezoom: (row: number, col: number) => void;
|
||||
/** Recall the pending tile at (row, col) — fired on a double-tap of a pending cell. */
|
||||
@@ -60,6 +72,20 @@
|
||||
const z = $derived(zoomed ? Z : 1);
|
||||
const premClass: Record<Premium, string> = { '': '', TW: 'tw', DW: 'dw', TL: 'tl', DL: 'dl' };
|
||||
|
||||
// The score badge sits on a corner of its anchor cell, clamped so the whole pill stays on the
|
||||
// board. Its position is a percentage of the (15-cell) board; CSS centres the pill on the point.
|
||||
const BADGE_MARGIN = 2.4;
|
||||
const badgePos = $derived.by(() => {
|
||||
const b = scoreBadge;
|
||||
if (!b) return null;
|
||||
const cell = 100 / 15;
|
||||
let x = b.corner === 'tr' ? (b.col + 1) * cell : b.col * cell;
|
||||
let y = b.corner === 'tr' ? b.row * cell : (b.row + 1) * cell;
|
||||
x = Math.max(BADGE_MARGIN, Math.min(100 - BADGE_MARGIN, x));
|
||||
y = Math.max(BADGE_MARGIN, Math.min(100 - BADGE_MARGIN, y));
|
||||
return { x, y };
|
||||
});
|
||||
|
||||
let viewport = $state<HTMLElement>();
|
||||
|
||||
// Genuine layout zoom (the board grows; cqw labels stay constant), so native scroll
|
||||
@@ -315,6 +341,9 @@
|
||||
class="cell {premClass[premium[r][c]]}"
|
||||
class:filled={!!cell}
|
||||
class:pending={!!p && !cell}
|
||||
class:legal={!!p && !cell && previewLegal === true}
|
||||
class:illegal={!!p && !cell && previewLegal === false}
|
||||
class:formed={!!cell && formed.has(key(r, c))}
|
||||
class:hl={!!cell && highlight.has(key(r, c)) && !flash}
|
||||
class:flash={!!cell && flash && highlight.has(key(r, c))}
|
||||
class:dark={premium[r][c] === '' && !cell && !p && (r + c) % 2 === 1}
|
||||
@@ -342,6 +371,9 @@
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
{#if scoreBadge && badgePos}
|
||||
<span class="scorebadge" style="left:{badgePos.x}%; top:{badgePos.y}%">{scoreBadge.score}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,6 +429,8 @@
|
||||
gap: 0;
|
||||
background: var(--board-bg);
|
||||
padding: 0;
|
||||
/* Anchor for the absolutely-positioned score badge (its percentages resolve against the board). */
|
||||
position: relative;
|
||||
}
|
||||
.cell {
|
||||
position: relative;
|
||||
@@ -445,6 +479,19 @@
|
||||
board) instead of the touch starting a board pan. */
|
||||
touch-action: none;
|
||||
}
|
||||
/* While composing, the staged play recolours its tiles: a calm reddish-pink when they form no
|
||||
word, a light green for the player's own tiles once they do; committed board tiles a formed
|
||||
word runs through go a shade darker (see lib/formed + Game.svelte). .formed sits after .filled
|
||||
so it wins on committed cells (equal specificity). */
|
||||
.cell.pending.illegal {
|
||||
background: var(--tile-pending-illegal);
|
||||
}
|
||||
.cell.pending.legal {
|
||||
background: var(--tile-pending-legal);
|
||||
}
|
||||
.cell.formed {
|
||||
background: var(--tile-formed);
|
||||
}
|
||||
.cell.droptarget {
|
||||
/* The cell a carried tile is aimed at: an accent ring plus a light accent wash, so the
|
||||
target reads clearly while dragging without obscuring the bonus label underneath. */
|
||||
@@ -545,4 +592,25 @@
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* The move-score badge for the legal play being composed: an orange pill centred on a corner of
|
||||
the main word's tile (see lib/formed.badgePlacement + Game.svelte), its digit sized like a
|
||||
tile's point value so it scales with the zoom. Decorative — no pointer events — and drawn above
|
||||
the tiles. */
|
||||
.scorebadge {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
border-radius: 999px;
|
||||
background: var(--score-badge);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
padding: 2px 5px; /* px fallback for Chrome < 105 (no cqw) */
|
||||
padding: 0.4cqw 0.9cqw;
|
||||
font-size: calc(2.4vmin * var(--z, 1)); /* cqw fallback for Chrome < 105 — see .letter */
|
||||
font-size: 2.4cqw;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user