Stage 7 polish (round 2): layout/zoom/tab-bar/hint/check fixes
- nav bar grows ONLY in game (other screens: minimal nav, content fills); tab bar always bottom - tab bar: tighter icon/label spacing, bigger icons, hint badge on the icon corner - board zoom reworked to width-based (real native scroll, fixes Safari/Chrome) + constant cqw labels; pinch & swipe-to-history dropped (conflict), double-tap kept, history via menu - beginner bonus labels shrunk to fit cells - Draw opens exchange directly (no confirm); confirm popovers restyled like the hamburger dropdown (vertical); removed the floating direction toggle - pending tiles darker bg (no outline); last-word dark-tile highlight (static / 1s flash) - check button disabled for <2/>15 chars, already-checked, or 5s cooldown - global user-select:none (inputs exempt); docs updated; TODO-4 alphabet-on-wire
This commit is contained in:
+55
-88
@@ -10,7 +10,8 @@
|
||||
board,
|
||||
premium,
|
||||
pending,
|
||||
recent,
|
||||
highlight,
|
||||
flash,
|
||||
centre,
|
||||
zoomed,
|
||||
variant,
|
||||
@@ -23,7 +24,8 @@
|
||||
board: (BoardCell | null)[][];
|
||||
premium: Premium[][];
|
||||
pending: Map<string, { letter: string; blank: boolean }>;
|
||||
recent: Set<string>;
|
||||
highlight: Set<string>;
|
||||
flash: boolean;
|
||||
centre: { row: number; col: number };
|
||||
zoomed: boolean;
|
||||
variant: Variant;
|
||||
@@ -40,19 +42,20 @@
|
||||
|
||||
let viewport = $state<HTMLElement>();
|
||||
|
||||
// When zoomed in (typically on a placement), centre the focus cell.
|
||||
// Genuine layout zoom (the board grows; cqw labels stay constant), so native scroll
|
||||
// works in every browser. Centre the focus cell when zoomed in.
|
||||
$effect(() => {
|
||||
const vp = viewport;
|
||||
if (!vp || !zoomed || !focus) return;
|
||||
const cell = vp.clientWidth / 15;
|
||||
const cell = (vp.clientWidth * Z) / 15;
|
||||
vp.scrollTo({
|
||||
left: (focus.col + 0.5) * cell * Z - vp.clientWidth / 2,
|
||||
top: (focus.row + 0.5) * cell * Z - vp.clientHeight / 2,
|
||||
left: (focus.col + 0.5) * cell - vp.clientWidth / 2,
|
||||
top: (focus.row + 0.5) * cell - vp.clientHeight / 2,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
|
||||
// Double-tap toggles zoom.
|
||||
// Double-tap toggles zoom (pinch was dropped — it conflicts with native scroll).
|
||||
let lastTap = 0;
|
||||
function onTap(row: number, col: number) {
|
||||
const now = Date.now();
|
||||
@@ -65,50 +68,11 @@
|
||||
oncell(row, col);
|
||||
}
|
||||
|
||||
// Minimal pinch: spread zooms in, pinch zooms out (two-state).
|
||||
const pts = new Map<number, { x: number; y: number }>();
|
||||
let startDist = 0;
|
||||
function dist(): number {
|
||||
const p = [...pts.values()];
|
||||
return p.length < 2 ? 0 : Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y);
|
||||
}
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
if (pts.size === 2) startDist = dist();
|
||||
}
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!pts.has(e.pointerId)) return;
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
if (pts.size === 2 && startDist > 0) {
|
||||
const d = dist();
|
||||
if (!zoomed && d > startDist * 1.25) {
|
||||
ontogglezoom();
|
||||
startDist = 0;
|
||||
} else if (zoomed && d < startDist * 0.8) {
|
||||
ontogglezoom();
|
||||
startDist = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
pts.delete(e.pointerId);
|
||||
if (pts.size < 2) startDist = 0;
|
||||
}
|
||||
|
||||
const key = (r: number, c: number) => `${r},${c}`;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="viewport"
|
||||
class:zoomed
|
||||
bind:this={viewport}
|
||||
onpointerdown={onPointerDown}
|
||||
onpointermove={onPointerMove}
|
||||
onpointerup={onPointerUp}
|
||||
onpointercancel={onPointerUp}
|
||||
>
|
||||
<div class="scaler" style="transform: scale({z}); --inv: {1 / z};">
|
||||
<div class="viewport" class:zoomed bind:this={viewport}>
|
||||
<div class="scaler" style="--z: {z};">
|
||||
<div class="grid">
|
||||
{#each board as rowCells, r (r)}
|
||||
{#each rowCells as cell, c (c)}
|
||||
@@ -120,24 +84,23 @@
|
||||
class="cell {premClass[premium[r][c]]}"
|
||||
class:filled={!!cell}
|
||||
class:pending={!!p && !cell}
|
||||
class:recent={recent.has(key(r, c))}
|
||||
class:hl={!!cell && highlight.has(key(r, c))}
|
||||
class:flash={!!cell && flash && highlight.has(key(r, c))}
|
||||
data-cell
|
||||
data-row={r}
|
||||
data-col={c}
|
||||
onclick={() => onTap(r, c)}
|
||||
>
|
||||
<span class="ct">
|
||||
{#if letter}
|
||||
<span class="letter">{letter}</span>
|
||||
{#if !blank}<span class="val">{tileValue(variant, letter)}</span>{/if}
|
||||
{:else if r === centre.row && c === centre.col}
|
||||
<span class="star">★</span>
|
||||
{:else if bl?.kind === 'single'}
|
||||
<span class="b1">{bl.text}</span>
|
||||
{:else if bl?.kind === 'split'}
|
||||
<span class="bsplit"><span class="bt">{bl.top}</span><span class="bb">{bl.bottom}</span></span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if letter}
|
||||
<span class="letter">{letter}</span>
|
||||
{#if !blank}<span class="val">{tileValue(variant, letter)}</span>{/if}
|
||||
{:else if r === centre.row && c === centre.col}
|
||||
<span class="star">★</span>
|
||||
{:else if bl?.kind === 'single'}
|
||||
<span class="b1">{bl.text}</span>
|
||||
{:else if bl?.kind === 'split'}
|
||||
<span class="bsplit"><span class="bt">{bl.top}</span><span class="bb">{bl.bottom}</span></span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
@@ -153,15 +116,13 @@
|
||||
background: var(--board-bg);
|
||||
border-radius: var(--radius-sm);
|
||||
container-type: inline-size;
|
||||
touch-action: none;
|
||||
}
|
||||
.viewport.zoomed {
|
||||
overflow: auto;
|
||||
}
|
||||
.scaler {
|
||||
width: 100%;
|
||||
transform-origin: 0 0;
|
||||
transition: transform 0.25s ease;
|
||||
width: calc(100% * var(--z));
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
@@ -179,6 +140,7 @@
|
||||
color: var(--prem-text);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-size: 0;
|
||||
}
|
||||
.cell.tw {
|
||||
background: var(--prem-tw);
|
||||
@@ -200,35 +162,37 @@
|
||||
}
|
||||
.cell.pending {
|
||||
background: var(--tile-pending);
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.cell.recent {
|
||||
box-shadow:
|
||||
inset 0 -2px 0 var(--tile-edge),
|
||||
0 0 0 2px var(--warn);
|
||||
.cell.hl {
|
||||
background: var(--tile-recent);
|
||||
}
|
||||
/* Counter-scaled text layer: stays constant size as the board scales. */
|
||||
.ct {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: scale(var(--inv));
|
||||
transform-origin: center;
|
||||
pointer-events: none;
|
||||
.cell.flash {
|
||||
animation: tileflash 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes tileflash {
|
||||
0%,
|
||||
100% {
|
||||
background: var(--tile-bg);
|
||||
}
|
||||
50% {
|
||||
background: var(--tile-recent);
|
||||
}
|
||||
}
|
||||
/* cqw fonts are sized against the fixed viewport, so labels stay a constant size as
|
||||
the board grows on zoom (relatively smaller, never overflowing). */
|
||||
.letter {
|
||||
position: absolute;
|
||||
top: 6%;
|
||||
left: 9%;
|
||||
font-size: 4.3cqw;
|
||||
top: 5%;
|
||||
left: 8%;
|
||||
font-size: 4.2cqw;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.val {
|
||||
position: absolute;
|
||||
right: 5%;
|
||||
bottom: 2%;
|
||||
font-size: 2.5cqw;
|
||||
bottom: 3%;
|
||||
font-size: 2.4cqw;
|
||||
font-weight: 600;
|
||||
}
|
||||
.star {
|
||||
@@ -236,7 +200,7 @@
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 4cqw;
|
||||
font-size: 3.6cqw;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.b1 {
|
||||
@@ -244,7 +208,7 @@
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 3cqw;
|
||||
font-size: 2.7cqw;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -255,15 +219,18 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
line-height: 1.05;
|
||||
opacity: 0.92;
|
||||
overflow: hidden;
|
||||
padding: 0 1px;
|
||||
}
|
||||
.bt {
|
||||
font-size: 2.4cqw;
|
||||
font-size: 1.7cqw;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bb {
|
||||
font-size: 3.1cqw;
|
||||
font-size: 1.9cqw;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user