feat(client): render and drag phantom pieces on the board
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ALL_SQUARES, FILES, RANKS, geometricMoves, type Color, type Piece, type Square } from '@blind-chess/shared';
|
||||
import { pieceGlyph } from './pieces.js';
|
||||
import { phantomDrag } from './stores/phantom-drag.svelte.js';
|
||||
|
||||
interface Props {
|
||||
pieces: Partial<Record<Square, Piece>>;
|
||||
@@ -8,6 +9,8 @@
|
||||
toMove: Color;
|
||||
mode: 'blind' | 'vanilla';
|
||||
highlightingEnabled: boolean;
|
||||
phantoms?: Partial<Record<Square, Piece>>;
|
||||
phantomsEnabled?: boolean;
|
||||
armedSquare: Square | null; // local visual arm (pre-commit)
|
||||
touchedSquare: Square | null; // server-authoritative touch
|
||||
onArm: (sq: Square | null) => void;
|
||||
@@ -16,6 +19,7 @@
|
||||
|
||||
let {
|
||||
pieces, you, toMove, mode, highlightingEnabled,
|
||||
phantoms = {}, phantomsEnabled = false,
|
||||
armedSquare, touchedSquare, onArm, onCommit,
|
||||
}: Props = $props();
|
||||
|
||||
@@ -43,6 +47,14 @@
|
||||
return new Set(moves);
|
||||
});
|
||||
|
||||
// The board square a phantom is currently being dragged out of (so it can
|
||||
// be dimmed while the drag ghost is shown). Bind `active` to a local first
|
||||
// so TypeScript narrows the discriminated union reliably.
|
||||
const dragOrigin = $derived.by(() => {
|
||||
const a = phantomDrag.state.active;
|
||||
return a?.kind === 'board' && phantomDrag.state.moved ? a.from : null;
|
||||
});
|
||||
|
||||
function squareColor(sq: Square): 'light' | 'dark' {
|
||||
const f = sq.charCodeAt(0) - 'a'.charCodeAt(0);
|
||||
const r = parseInt(sq[1]!, 10) - 1;
|
||||
@@ -50,6 +62,7 @@
|
||||
}
|
||||
|
||||
function onSquareClick(sq: Square) {
|
||||
if (phantomDrag.shouldSuppressClick(sq)) return;
|
||||
const piece = pieces[sq];
|
||||
|
||||
// If a piece is touched (server-authoritative), only commits to that square's destinations are valid.
|
||||
@@ -82,6 +95,7 @@
|
||||
{#each filesDisplay as f (f)}
|
||||
{@const sq = `${f}${r}` as Square}
|
||||
{@const piece = pieces[sq]}
|
||||
{@const ph = phantomsEnabled ? phantoms[sq] : undefined}
|
||||
{@const sc = squareColor(sq)}
|
||||
{@const isArmed = sq === armedSquare}
|
||||
{@const isTouched = sq === touchedSquare}
|
||||
@@ -96,6 +110,7 @@
|
||||
class:hl-cap={isHighlightCap}
|
||||
onclick={() => onSquareClick(sq)}
|
||||
aria-label={sq}
|
||||
data-square={sq}
|
||||
>
|
||||
{#if r === (you === 'w' ? '1' : '8') && f === filesDisplay[0]}
|
||||
<span class="coord coord-rank">{r}</span>
|
||||
@@ -106,6 +121,13 @@
|
||||
{#if piece}
|
||||
<span class="piece piece-{piece.color}">{pieceGlyph(piece)}</span>
|
||||
{/if}
|
||||
{#if ph && !piece}
|
||||
<span
|
||||
class="phantom phantom-{ph.color}"
|
||||
class:dragging={sq === dragOrigin}
|
||||
onpointerdown={(e) => { e.preventDefault(); phantomDrag.start({ kind: 'board', from: sq, type: ph.type }, e); }}
|
||||
>{pieceGlyph(ph)}</span>
|
||||
{/if}
|
||||
{#if isHighlight && !piece}
|
||||
<span class="dot"></span>
|
||||
{/if}
|
||||
@@ -154,6 +176,23 @@
|
||||
.piece-w { color: #fafafa; }
|
||||
.piece-b { color: #1a1a1a; }
|
||||
|
||||
.phantom {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
opacity: 0.4;
|
||||
outline: 2px dashed var(--text-dim);
|
||||
outline-offset: -5px;
|
||||
}
|
||||
.phantom-w { color: #fafafa; }
|
||||
.phantom-b { color: #1a1a1a; }
|
||||
.phantom.dragging { opacity: 0.12; }
|
||||
|
||||
.armed { box-shadow: inset 0 0 0 4px var(--armed); }
|
||||
.touched { box-shadow: inset 0 0 0 4px var(--touched); }
|
||||
.hl::before {
|
||||
|
||||
Reference in New Issue
Block a user