docs: amend plan to reflect code-review fixes

Tasks 8/10/11 received review fixes during execution; the plan's code
blocks are updated to match what shipped:
- Task 8: drag controller handles pointercancel + idempotent start.
- Task 10: palette pieces are plain spans + svelte-ignore (no
  focusable-but-not-operable role/tabindex).
- Task 11: phantom-load effect keyed on gameId; drag ghost gated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude (blind_chess)
2026-05-18 20:51:37 -04:00
parent 82a69d8812
commit 59717b3b5b
@@ -873,6 +873,12 @@ function makeDrag() {
let suppressClickOn: Square | null = null; let suppressClickOn: Square | null = null;
const THRESHOLD = 6; const THRESHOLD = 6;
function detach() {
window.removeEventListener('pointermove', onMove);
window.removeEventListener('pointerup', onUp);
window.removeEventListener('pointercancel', onCancel);
}
function onMove(e: PointerEvent) { function onMove(e: PointerEvent) {
if (!state.active) return; if (!state.active) return;
state.x = e.clientX; state.x = e.clientX;
@@ -882,15 +888,23 @@ function makeDrag() {
} }
} }
// pointercancel fires instead of pointerup when the browser/OS takes over
// the gesture (common on touch). Abort the drag: clean up, drop nothing.
function onCancel() {
detach();
state.active = null;
state.moved = false;
}
function onUp(e: PointerEvent) { function onUp(e: PointerEvent) {
window.removeEventListener('pointermove', onMove); detach();
window.removeEventListener('pointerup', onUp);
const src = state.active; const src = state.active;
const wasDrag = state.moved; const wasDrag = state.moved;
state.active = null; state.active = null;
state.moved = false; state.moved = false;
if (!src || !wasDrag) return; // a tap — the board click handler deals with it if (!src || !wasDrag) return; // a tap — the board click handler deals with it
// elementFromPoint returns null off-viewport — treated as an off-board drop.
const el = document.elementFromPoint(e.clientX, e.clientY); const el = document.elementFromPoint(e.clientX, e.clientY);
const sqEl = el?.closest('[data-square]') as HTMLElement | null; const sqEl = el?.closest('[data-square]') as HTMLElement | null;
const target = sqEl?.dataset.square as Square | undefined; const target = sqEl?.dataset.square as Square | undefined;
@@ -907,6 +921,7 @@ function makeDrag() {
} }
function start(src: DragSource, e: PointerEvent) { function start(src: DragSource, e: PointerEvent) {
detach(); // idempotency — drop any listeners from an unfinished prior drag
suppressClickOn = null; suppressClickOn = null;
state.active = src; state.active = src;
state.x = startX = e.clientX; state.x = startX = e.clientX;
@@ -914,6 +929,7 @@ function makeDrag() {
state.moved = false; state.moved = false;
window.addEventListener('pointermove', onMove); window.addEventListener('pointermove', onMove);
window.addEventListener('pointerup', onUp); window.addEventListener('pointerup', onUp);
window.addEventListener('pointercancel', onCancel);
} }
/** The board calls this first in its square-click handler. */ /** The board calls this first in its square-click handler. */
@@ -1095,11 +1111,11 @@ Create `packages/client/src/lib/PhantomPalette.svelte`:
<span class="hint muted">Drag onto your board — your guess of where the opponent is.</span> <span class="hint muted">Drag onto your board — your guess of where the opponent is.</span>
<div class="pieces"> <div class="pieces">
{#each TYPES as t (t)} {#each TYPES as t (t)}
<!-- Pointer-only drag source — same deliberate a11y trade-off as the
phantom spans in Board.svelte (no keyboard drag interaction). -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span <span
class="pp pp-{oppColor}" class="pp pp-{oppColor}"
role="button"
tabindex="0"
aria-label={`place ${t}`}
onpointerdown={(e) => { e.preventDefault(); phantomDrag.start({ kind: 'palette', type: t }, e); }} onpointerdown={(e) => { e.preventDefault(); phantomDrag.start({ kind: 'palette', type: t }, e); }}
>{pieceGlyph({ color: oppColor, type: t })}</span> >{pieceGlyph({ color: oppColor, type: t })}</span>
{/each} {/each}
@@ -1183,14 +1199,17 @@ After the existing `const turnLabel = ...` derived block, add:
return a && phantomDrag.state.moved ? a.type : null; return a && phantomDrag.state.moved ? a.type : null;
}); });
// Load the phantom layer once `you` is known (blind games only). // Load the phantom layer when `you` is known (blind games only). Keyed on
let phantomsLoaded = $state(false); // gameId — like the connection effect — so it reloads if this <Game>
// instance is reused for a different game without a remount.
let loadedFor: string | null = $state(null);
$effect(() => { $effect(() => {
if (phantomsLoaded) return; const id = gameId;
const you = game.state.you; const you = game.state.you;
if (loadedFor === id) return;
if (you && game.state.mode === 'blind') { if (you && game.state.mode === 'blind') {
untrack(() => phantoms.loadForGame(gameId, you)); untrack(() => phantoms.loadForGame(id, you));
phantomsLoaded = true; loadedFor = id;
} }
}); });
@@ -1234,7 +1253,7 @@ Replace the `<div class="board-area">...</div>` block with:
Immediately after the closing `</div>` of `<div class="game-layout" ...>` (and before the `{#if pendingPromotion ...}` block), add: Immediately after the closing `</div>` of `<div class="game-layout" ...>` (and before the `{#if pendingPromotion ...}` block), add:
```svelte ```svelte
{#if dragGhost} {#if phantomLayerEnabled && dragGhost}
<div <div
class="drag-ghost piece-{oppColor}" class="drag-ghost piece-{oppColor}"
style="left: {phantomDrag.state.x}px; top: {phantomDrag.state.y}px;" style="left: {phantomDrag.state.x}px; top: {phantomDrag.state.y}px;"