Compare commits

...

4 Commits

Author SHA1 Message Date
semantic-release-bot
af85b3e481 chore(release): 2.14.2 [skip ci]
## [2.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.14.1...v2.14.2) (2025-10-09)

### Bug Fixes

* correct avatar positioning to prevent fly-in animation ([573d0df](573d0df20d))
2025-10-09 21:19:43 +00:00
Thomas Hallock
573d0df20d fix: correct avatar positioning to prevent fly-in animation
Fixed positioning model to work properly with react-spring:

ISSUE:
- Avatar was still flying in from top-right
- Using translate(-50%, -50%) with animated left/top caused positioning issues
- Spring was animating from wrong initial position

SOLUTION:
- Remove transform: translate(-50%, -50%)
- Use negative margins instead: marginLeft: -24px, marginTop: -24px
- Calculate exact center position from getBoundingClientRect
- Set 'from' position in spring to match 'to' on first render
- Use x.to() and y.to() to convert spring values to px strings

POSITIONING MODEL:
1. Get card's bounding rect
2. Calculate avatar center point: (rect.right - 12, rect.top - 12)
3. Apply via left/top with negative margins for centering
4. Spring animates between positions smoothly
5. On first hover: from/to are same (no animation)
6. On subsequent hovers: smooth glide between cards

RESULT:
Avatar appears exactly at the card position with no fly-in animation,
then smoothly glides to new positions as user moves between cards.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:18:51 -05:00
semantic-release-bot
d312969747 chore(release): 2.14.1 [skip ci]
## [2.14.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.14.0...v2.14.1) (2025-10-09)

### Bug Fixes

* prevent avatar fly-in and hide local player's own hover ([7f65a67](7f65a67cef))
2025-10-09 21:10:10 +00:00
Thomas Hallock
7f65a67cef fix: prevent avatar fly-in and hide local player's own hover
Two critical UX fixes for hover avatars:

1. PREVENT FLY-IN FROM TOP-LEFT:
   - Initialize position state as null instead of {x:0, y:0}
   - Don't render avatar until position is calculated from card element
   - Set immediate:true on first render to prevent animation
   - Add opacity spring (0→1) for smooth fade-in
   - Result: Avatar appears exactly where it should, no fly-in animation

2. HIDE YOUR OWN HOVER AVATAR:
   - Filter out local player (isLocal === true) from hover avatar rendering
   - Only show remote players' hover avatars
   - You see opponents hovering, but not your own cursor
   - Result: Cleaner UX, less visual noise on your own session

BEFORE:
- Avatar would fly in from (0,0) top-left corner when first hovering
- You'd see your own avatar following your mouse (distracting)

AFTER:
- Avatar appears smoothly at the correct card position
- Only remote players' avatars are visible
- Feels like watching someone else's cursor, not your own

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 16:09:19 -05:00
3 changed files with 47 additions and 9 deletions

View File

@@ -1,3 +1,17 @@
## [2.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.14.1...v2.14.2) (2025-10-09)
### Bug Fixes
* correct avatar positioning to prevent fly-in animation ([573d0df](https://github.com/antialias/soroban-abacus-flashcards/commit/573d0df20dcdac41021c46feb423dbf3782728f6))
## [2.14.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.14.0...v2.14.1) (2025-10-09)
### Bug Fixes
* prevent avatar fly-in and hide local player's own hover ([7f65a67](https://github.com/antialias/soroban-abacus-flashcards/commit/7f65a67cef3d7f0ebce1bd7417972a6138acfc46))
## [2.14.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.13.0...v2.14.0) (2025-10-09)

View File

@@ -92,23 +92,36 @@ function HoverAvatar({
playerInfo: { emoji: string; name: string; color?: string }
cardElement: HTMLElement | null
}) {
const [position, setPosition] = useState({ x: 0, y: 0 })
const [position, setPosition] = useState<{ x: number; y: number } | null>(null)
// Update position when card element changes
useEffect(() => {
if (cardElement) {
const rect = cardElement.getBoundingClientRect()
// Calculate the actual position we want the avatar centered at (top-right of card)
// Since we're using translate(-50%, -50%), we need the center point
const avatarSize = 48
const avatarCenterX = rect.right - 12 // 12px from right edge
const avatarCenterY = rect.top - 12 // 12px from top edge
setPosition({
x: rect.right - 12, // Position at top-right of card
y: rect.top - 12,
x: avatarCenterX,
y: avatarCenterY,
})
}
}, [cardElement])
// Smooth spring animation for position changes
// Use 'from' to set initial position when avatar first appears
const springProps = useSpring({
x: position.x,
y: position.y,
from: position
? { x: position.x, y: position.y, opacity: 0 }
: { x: 0, y: 0, opacity: 0 },
to: {
x: position?.x ?? 0,
y: position?.y ?? 0,
opacity: position ? 1 : 0,
},
config: {
tension: 280,
friction: 60,
@@ -116,14 +129,21 @@ function HoverAvatar({
},
})
// Don't render until we have a position
if (!position) return null
return (
<animated.div
style={{
position: 'fixed',
left: springProps.x,
top: springProps.y,
// Don't use translate, just position directly at the calculated point
left: springProps.x.to((x) => `${x}px`),
top: springProps.y.to((y) => `${y}px`),
opacity: springProps.opacity,
width: '48px',
height: '48px',
marginLeft: '-24px', // Center horizontally (half of width)
marginTop: '-24px', // Center vertically (half of height)
borderRadius: '50%',
background: playerInfo.color || 'linear-gradient(135deg, #667eea, #764ba2)',
display: 'flex',
@@ -136,7 +156,6 @@ function HoverAvatar({
border: '3px solid white',
zIndex: 1000,
pointerEvents: 'none',
transform: 'translate(-50%, -50%)',
filter: 'drop-shadow(0 0 8px rgba(102, 126, 234, 0.8))',
}}
className={css({
@@ -363,6 +382,11 @@ export function MemoryGrid() {
{state.playerHovers &&
Object.entries(state.playerHovers)
.filter(([_, cardId]) => cardId !== null) // Only show if hovering a card
.filter(([playerId]) => {
// Don't show your own hover avatar (only show remote players)
const player = playerMap.get(playerId)
return player?.isLocal !== true
})
.map(([playerId, cardId]) => {
const playerInfo = getPlayerHoverInfo(playerId)
const cardElement = cardId ? cardRefs.current.get(cardId) : null

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "2.14.0",
"version": "2.14.2",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [