fix(card-sorting): stabilize inferred sequence for locked cards during drag

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 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-10-23 22:31:24 -05:00
parent d02ab5922c
commit b0cd194838
1 changed files with 35 additions and 3 deletions

View File

@ -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<Map<string, CardState>>(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')