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
-43
@@ -4,15 +4,16 @@
|
||||
import { valueForLetter } from '../lib/alphabet';
|
||||
import type { Variant } from '../lib/model';
|
||||
import { usesStarBlank, BLANK_STAR } from '../lib/variants';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
slots,
|
||||
variant,
|
||||
selected,
|
||||
landscape = false,
|
||||
shuffling = false,
|
||||
draggingId = null,
|
||||
dropIndex = null,
|
||||
confirm,
|
||||
ondown,
|
||||
}: {
|
||||
// Each slot carries a stable id that travels with its tile through a shuffle, so the
|
||||
@@ -20,14 +21,14 @@
|
||||
slots: (RackSlot & { id: number })[];
|
||||
variant: Variant;
|
||||
selected: number | null;
|
||||
/** Landscape layout: size the tiles to share the (narrow) left-panel width instead of the
|
||||
* viewport-width measure, so seven tiles never overflow the panel. */
|
||||
landscape?: boolean;
|
||||
shuffling?: boolean;
|
||||
// While a rack tile is being dragged to reorder it, draggingId is its id (hidden here —
|
||||
// the drag ghost stands in) and dropIndex is the slot where a gap opens.
|
||||
draggingId?: number | null;
|
||||
dropIndex?: number | null;
|
||||
/** The confirm-move control, rendered as the fixed 7th slot of the rack while a play is staged
|
||||
* (the parent owns the button and its enablement; the rack only positions it). */
|
||||
confirm?: Snippet;
|
||||
ondown: (e: PointerEvent, index: number) => void;
|
||||
} = $props();
|
||||
|
||||
@@ -57,7 +58,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rack" class:reordering={draggingId != null || dropIndex != null} class:landscape data-rack>
|
||||
<div class="rack" class:reordering={draggingId != null || dropIndex != null} data-rack>
|
||||
{#each shown as slot, i (slot.id)}
|
||||
<button
|
||||
class="tile"
|
||||
@@ -75,21 +76,26 @@
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{@render confirm?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Seven equal columns filling the whole tray width, so the tiles are square and as large as the
|
||||
row allows — the same in portrait and landscape. The tile glyphs use cqw against the rack width
|
||||
(the query container) so they track the tile size, the way the board sizes its labels on its
|
||||
.scaler. */
|
||||
.rack {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
/* Reserve one tile's height so an empty rack (e.g. a finished game) keeps the
|
||||
footer the same size as during play — no layout jump between states. */
|
||||
align-items: start;
|
||||
container-type: inline-size;
|
||||
/* Reserve about one tile's height so an empty rack (e.g. a finished game) keeps the footer the
|
||||
same size as during play — no layout jump between states. */
|
||||
min-height: min(12.5vw, 46px);
|
||||
}
|
||||
.tile {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: min(12.5vw, 46px);
|
||||
aspect-ratio: 1;
|
||||
background: var(--tile-bg);
|
||||
color: var(--tile-text);
|
||||
@@ -97,7 +103,6 @@
|
||||
border-radius: 5px;
|
||||
box-shadow: inset 0 -3px 0 var(--tile-edge);
|
||||
font-weight: 700;
|
||||
font-size: 1.4rem;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
@@ -109,35 +114,6 @@
|
||||
outline: 3px solid var(--accent);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
/* Landscape: the rack sits in a fixed-width left panel, so the tiles share the panel width
|
||||
(capped at the portrait size) and shrink below it when the panel is narrow, instead of the
|
||||
viewport-width measure that would overflow seven tiles in a column. */
|
||||
.rack.landscape {
|
||||
/* Size the tile glyphs relative to the rack (the board sizes its labels the same way, on its
|
||||
.scaler container). The tile is a flex + aspect-ratio item whose OWN container-query size
|
||||
resolves unreliably, so the rack — which has a definite width — is the query container. A
|
||||
fixed-rem letter overflowed the tile once it shrank below 46px in a narrow left panel and
|
||||
dropped into the bottom-left corner. The cqw values track the rack≈7×tile relation. */
|
||||
container-type: inline-size;
|
||||
}
|
||||
.rack.landscape .tile {
|
||||
/* Fixed tile size (1/7 of the fixed-width rack, accounting for the six 5px gaps), NOT flex-grow:
|
||||
so placing a tile leaves the remaining ones put instead of growing them to refill the row —
|
||||
matching the portrait rack. The constant rack width also keeps the cqw glyphs above steady as
|
||||
tiles leave. */
|
||||
flex: 0 0 auto;
|
||||
width: calc((100% - 6 * 5px) / 7);
|
||||
max-width: 46px;
|
||||
}
|
||||
.rack.landscape .letter {
|
||||
font-size: 6cqw;
|
||||
}
|
||||
.rack.landscape .val {
|
||||
font-size: 3.1cqw;
|
||||
}
|
||||
.rack.landscape .star {
|
||||
font-size: 7.6cqw;
|
||||
}
|
||||
/* While reordering, tiles at/after the drop slot slide right to open a gap there (one
|
||||
tile width plus the rack gap), so the drop position is visible. */
|
||||
.rack.reordering .tile {
|
||||
@@ -150,12 +126,15 @@
|
||||
position: absolute;
|
||||
top: 8%;
|
||||
left: 14%;
|
||||
font-size: 6vmin; /* cqw fallback for Chrome < 105 — approximates the portrait rack ≈ viewport width */
|
||||
font-size: 6cqw;
|
||||
}
|
||||
.val {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 1px;
|
||||
font-size: 0.7rem;
|
||||
font-size: 3.1vmin; /* cqw fallback for Chrome < 105 */
|
||||
font-size: 3.1cqw;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* Erudit's blank ("звёздочка") shows its star horizontally centred on the otherwise empty
|
||||
@@ -167,6 +146,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 1.7rem;
|
||||
font-size: 7.6vmin; /* cqw fallback for Chrome < 105 */
|
||||
font-size: 7.6cqw;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user