From 68835394263fc9118f1ad73d21d10e0b11ea3dc9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 17 Apr 2026 16:22:14 -0600 Subject: [PATCH] fix(chess): render piece overlays inside the drag-transform layer HP pips and any other per-piece overlays lived in the grid cell as siblings of \u2014 outside the transforming motion.div, so they stayed pinned to the origin square while the piece visually slid toward the cursor. Moved them inside the transform layer so they inherit the same x/y/rotate/scale motion values and follow the piece during drag and FLIP animations. Piece accepts overlays + pieceFacts as props; Board just forwards what it already computes. Same render cost, same wiring, visible result: pips dangle with the piece. --- packages/chess/src/ui/Board.tsx | 12 ++---------- packages/chess/src/ui/Piece.tsx | 30 ++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/chess/src/ui/Board.tsx b/packages/chess/src/ui/Board.tsx index c256207..b15f82a 100644 --- a/packages/chess/src/ui/Board.tsx +++ b/packages/chess/src/ui/Board.tsx @@ -322,19 +322,11 @@ export function Board({ facts, legalMoves, onMove, turn, myColor, lastMove, chec piece.color === turn && (myColor === null || myColor === undefined || piece.color === myColor) } + overlays={overlays} + pieceFacts={factsById.get(piece.id) ?? []} onDragStart={handleDragStart} onDragEnd={handleDragEnd} /> - {/* Per-piece preset overlays (HP pips, etc.). Each is a - pure function of the piece's facts; Board stays - agnostic to which presets exist. */} - {overlays.map((Overlay, i) => ( - - ))} )} diff --git a/packages/chess/src/ui/Piece.tsx b/packages/chess/src/ui/Piece.tsx index 272bd70..82cfbd4 100644 --- a/packages/chess/src/ui/Piece.tsx +++ b/packages/chess/src/ui/Piece.tsx @@ -1,4 +1,4 @@ -import type { PieceColor, PieceType } from '../schema'; +import type { ChessAttrMap, ChessFact, PieceColor, PieceType } from '../schema'; import { pieceAssets } from '../assets/pieces'; import { motion, @@ -6,8 +6,9 @@ import { useSpring, useTransform, } from 'motion/react'; -import { useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import type { DragEvent as ReactDragEvent } from 'react'; +import type { PieceOverlayComponent } from './preset-overlays'; export interface PieceProps { color: PieceColor; @@ -18,6 +19,14 @@ export interface PieceProps { * ongoing, etc). When false, drag is disabled and the piece shows a * default cursor. */ isDraggable: boolean; + /** Per-piece preset overlays (HP pips, ammo counter, etc.). Rendered + * INSIDE the drag-transform layer so they follow the piece as it's + * translated / rotated / scaled during a drag. Pass [] when no + * overlays are active. */ + overlays?: ReadonlyArray; + /** Facts for this piece, used by the overlays. Pre-indexed by the + * Board so the overlay component doesn't re-scan all facts. */ + pieceFacts?: ReadonlyArray>; onDragStart: (pieceId: number, square: number) => void; onDragEnd: () => void; } @@ -81,6 +90,8 @@ export function Piece({ pieceId, square, isDraggable, + overlays, + pieceFacts, onDragStart, onDragEnd, }: PieceProps) { @@ -344,6 +355,21 @@ export function Piece({ }`} draggable={false} /> + + {/* + * Per-piece preset overlays (HP pips, etc.) live INSIDE the + * transform layer so they inherit the drag `x/y/rotate/scale` + * motion values and track the piece during drags. Overlays + * that don't apply to this piece's facts return null so the + * DOM stays clean. + */} + {overlays?.map((Overlay, i) => ( + + )) as ReactNode}