From b0cd194838705bb7bbf21ac9e318eaba491097b2 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Thu, 23 Oct 2025 22:31:24 -0500 Subject: [PATCH] fix(card-sorting): stabilize inferred sequence for locked cards during drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When dragging suffix cards into position, the inferred sequence was constantly recalculating based on the card's real-time position, causing suffix detection to flicker on/off. This created instability when trying to place suffix cards. Solution: - Store stable positions for locked (prefix/suffix) cards in a ref - Use stable positions instead of live positions for inference - Clean up ref when cards are no longer locked - Add useMemo import for stable card states computation This ensures that once a card is locked in correct position, its position in the inferred sequence remains stable even when other cards are being dragged nearby. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/PlayingPhaseDrag.tsx | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/web/src/arcade-games/card-sorting/components/PlayingPhaseDrag.tsx b/apps/web/src/arcade-games/card-sorting/components/PlayingPhaseDrag.tsx index 3bb94b2c..ca3f8ac1 100644 --- a/apps/web/src/arcade-games/card-sorting/components/PlayingPhaseDrag.tsx +++ b/apps/web/src/arcade-games/card-sorting/components/PlayingPhaseDrag.tsx @@ -2,7 +2,7 @@ import { css } from '../../../../styled-system/css' import { useCardSorting } from '../Provider' -import { useState, useEffect, useRef, useCallback } from 'react' +import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { useSpring, animated, to } from '@react-spring/web' import type { SortingCard } from '../types' @@ -1228,8 +1228,26 @@ export function PlayingPhaseDrag() { } }, [state.cardPositions, draggingCardId, cardStates]) - // Infer sequence from current positions - const inferredSequence = inferSequenceFromPositions(cardStates, [ + // Store locked card positions in a ref to keep them stable during drags + const lockedCardPositionsRef = useRef>(new Map()) + + // Create a stable version of cardStates for inference + // Use locked positions from ref for cards that were previously locked + const stableCardStates = useMemo(() => { + const stable = new Map(cardStates) + + // Replace positions of locked cards with their stable positions + for (const [cardId, lockedPos] of lockedCardPositionsRef.current.entries()) { + if (stable.has(cardId)) { + stable.set(cardId, lockedPos) + } + } + + return stable + }, [cardStates]) + + // Infer sequence from stable positions + const inferredSequence = inferSequenceFromPositions(stableCardStates, [ ...state.availableCards, ...state.placedCards.filter((c): c is SortingCard => c !== null), ]) @@ -1273,6 +1291,14 @@ export function PlayingPhaseDrag() { suffixCards.unshift(inferredSequence[inferredIdx]) } + // Clean up locked positions ref - remove cards that are no longer locked + const lockedCardIds = new Set([...prefixCards, ...suffixCards].map((c) => c.id)) + for (const cardId of Array.from(lockedCardPositionsRef.current.keys())) { + if (!lockedCardIds.has(cardId)) { + lockedCardPositionsRef.current.delete(cardId) + } + } + // Check if prefix and suffix overlap (all cards are correct) // In this case, prefix and suffix cards are the same, so skip auto-arrange const prefixAndSuffixOverlap = @@ -1339,6 +1365,9 @@ export function PlayingPhaseDrag() { newStates.set(card.id, { x, y, rotation, zIndex }) hasChanges = true } + + // Store stable position for locked prefix card + lockedCardPositionsRef.current.set(card.id, { x, y, rotation, zIndex }) }) } else if (shouldLog) { console.log('[AutoArrange] Skipping prefix arrange: viewport too narrow') @@ -1367,6 +1396,9 @@ export function PlayingPhaseDrag() { newStates.set(card.id, { x, y, rotation, zIndex }) hasChanges = true } + + // Store stable position for locked suffix card + lockedCardPositionsRef.current.set(card.id, { x, y, rotation, zIndex }) }) } else if (shouldLog) { console.log('[AutoArrange] Skipping suffix arrange: viewport too narrow')