Compare commits

...

16 Commits

Author SHA1 Message Date
semantic-release-bot
095221564f chore(release): 4.63.10 [skip ci]
## [4.63.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.9...v4.63.10) (2025-10-22)

### Bug Fixes

* **complement-race:** show only first active player's passengers on train ([2bfd5d2](2bfd5d2bda))
2025-10-22 15:46:43 +00:00
Thomas Hallock
2bfd5d2bda fix(complement-race): show only first active player's passengers on train
In multiplayer steam sprint games, the train was displaying passengers from
all players (including inactive ones) instead of only showing passengers
belonging to the first active player.

The issue was that boardedPassengers was filtering for any claimed passenger,
not checking if the claiming player was still active.

Changes to SteamTrainJourney.tsx:
- Filter boardedPassengers to only include passengers where claimedBy matches
  the firstActivePlayer's ID
- Add firstActivePlayer.id to the useMemo dependency array
- Update comment to clarify filtering logic

Now the train correctly displays only the first active player's passengers
in their train cars, matching the player emoji shown as the engineer.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 10:45:33 -05:00
semantic-release-bot
6dabb71600 chore(release): 4.63.9 [skip ci]
## [4.63.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.8...v4.63.9) (2025-10-21)

### Bug Fixes

* **homepage:** use app-wide abacus config in interactive flashcards ([cf1be2d](cf1be2d173))
2025-10-21 17:09:40 +00:00
Thomas Hallock
cf1be2d173 fix(homepage): use app-wide abacus config in interactive flashcards
The interactive flashcards section at the bottom of the homepage was
hardcoding beadShape="circle" instead of respecting the app-wide abacus
configuration context that all other abaci on the site use.

Changes to InteractiveFlashcards.tsx:
- Import useAbacusConfig hook from @soroban/abacus-react
- Call useAbacusConfig() in DraggableCard component
- Use appConfig.beadShape instead of hardcoded "circle"

This ensures visual consistency across all abacus displays on the site
and respects user preferences for bead styling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 12:08:26 -05:00
semantic-release-bot
0169ab5128 chore(release): 4.63.8 [skip ci]
## [4.63.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.7...v4.63.8) (2025-10-21)

### Bug Fixes

* **mobile:** restore abacus visibility in "Your Journey" section ([c96036d](c96036d86b))
2025-10-21 17:08:03 +00:00
Thomas Hallock
c96036d86b fix(mobile): restore abacus visibility in "Your Journey" section
On mobile screens (base breakpoint), the level details cards were taking
up all vertical space within the 500px maxHeight constraint, pushing the
abacus completely out of view.

Solution: Hide level details on mobile (display: { base: 'none', lg: 'grid' })
so mobile users see only the slider and abacus, while desktop users see
all components. Also added minH: 0 to containers to ensure proper flex
shrinking behavior.

Changes to LevelSliderDisplay.tsx:
- Level details grid now hidden on base breakpoint, visible on lg+
- Simplified grid columns to single value since it's desktop-only
- Added minH: 0 to flex containers for proper shrinking

Tested on iPhone 14 (390px viewport).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 12:06:48 -05:00
semantic-release-bot
653db575ff chore(release): 4.63.7 [skip ci]
## [4.63.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.6...v4.63.7) (2025-10-21)

### Bug Fixes

* **mobile:** reduce height of Your Journey section on mobile ([8944035](89440355bf))
2025-10-21 17:03:24 +00:00
Thomas Hallock
89440355bf fix(mobile): reduce height of Your Journey section on mobile
Added maxHeight constraint and reduced padding to ensure the abacus
stays visible while scrubbing the slider on mobile devices.

Changes:
- Added maxHeight: 500px on mobile (base), none on desktop (md)
- Reduced padding from 6 to 4 on mobile (base)

This ensures users can see both the slider and abacus simultaneously
on iPhone displays without scrolling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 12:02:10 -05:00
semantic-release-bot
632e840ca7 chore(release): 4.63.6 [skip ci]
## [4.63.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.5...v4.63.6) (2025-10-21)

### Bug Fixes

* **mobile:** optimize Your Journey section for iPhone displays ([9167fb4](9167fb40d6))
2025-10-21 16:57:23 +00:00
Thomas Hallock
9167fb40d6 fix(mobile): optimize Your Journey section for iPhone displays
Fixed horizontal overflow issues on small screens (iPhone 14, 390px viewport):

Changes to LevelSliderDisplay component:
- Made abacus display container stack vertically on mobile (base) and horizontal on large screens (lg)
- Made level details grid responsive: 2 cols (mobile), 3 cols (sm), 2 cols (lg)
- Removed fixed widths from detail cards, using flexible widths with min/max constraints
- Added horizontal scroll (overflow-x: auto) to abacus container as fallback
- Reduced slider thumb size on mobile: 120px x 96px (base) vs 180px x 128px (md)
- Scaled down bead in slider thumb to 75% on mobile
- Reduced emoji tick sizes: 2xl (mobile), 3xl (sm), 4xl (md)

These changes ensure the "Your Journey" slider and abacus display fit properly
on mobile devices without horizontal overflow while maintaining the desktop experience.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:56:07 -05:00
semantic-release-bot
1d7486ed48 chore(release): 4.63.5 [skip ci]
## [4.63.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.4...v4.63.5) (2025-10-21)

### Bug Fixes

* **flashcards:** store grab offset in local coordinates to prevent jump ([39d93a9](39d93a9e9f))
2025-10-21 16:29:31 +00:00
Thomas Hallock
39d93a9e9f fix(flashcards): store grab offset in local coordinates to prevent jump
Problem: Cards jumped when grabbed, especially if already rotated. The grab
point would slip during rotation instead of staying under the cursor.

Root cause: Grab offset was stored in screen space with the card already
rotated. When we later rotated this offset during drag, we were applying
rotation on top of rotation.

Solution: Convert grab offset to local (unrotated) coordinates when grabbing:
- Calculate screen-space offset from card center
- Rotate by -currentRotation to get local coordinates
- Store in grabOffsetRef
- During drag, rotate this local offset by the current rotation angle

This ensures the grab point stays perfectly under the cursor regardless of
the card's initial or current rotation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:28:18 -05:00
semantic-release-bot
6d1bad142b chore(release): 4.63.4 [skip ci]
## [4.63.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.3...v4.63.4) (2025-10-21)

### Bug Fixes

* **flashcards:** keep grab point under cursor with proper coordinate conversion ([1869216](1869216d2f))
2025-10-21 16:28:08 +00:00
Thomas Hallock
1869216d2f fix(flashcards): keep grab point under cursor with proper coordinate conversion
Fixed the issue where cards would slip out from under the cursor when rotating.
The grab point now stays perfectly under your cursor throughout the drag.

The problem: Simple delta positioning didn't account for rotation causing the
grab point to rotate away from the cursor position.

The solution: Properly convert between coordinate systems:
1. Calculate rotated grab offset (2D rotation matrix)
2. Determine card center: cursor position - rotated grab offset (viewport space)
3. Convert from viewport coordinates to container coordinates
4. Calculate top-left position from center for CSS translate()

Key changes:
- Pass containerRef down to DraggableCard components
- Get container bounds for viewport→container conversion
- Apply rotation matrix to grab offset
- Position card so rotated grab point aligns with cursor

Now when you grab a card by its edge and drag, the exact point you grabbed
stays glued to your cursor while the card rotates around its center.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:26:54 -05:00
semantic-release-bot
e4ae3aefef chore(release): 4.63.3 [skip ci]
## [4.63.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.2...v4.63.3) (2025-10-21)

### Bug Fixes

* **flashcards:** revert to simple delta positioning to prevent card jumping ([d018b69](d018b699c4))
2025-10-21 16:22:59 +00:00
Thomas Hallock
d018b699c4 fix(flashcards): revert to simple delta positioning to prevent card jumping
Reverted the complex rotation-compensated positioning that was causing
cards to jump to the right of the viewport on click.

The issue: The previous attempt to keep the grab point "stuck" to the cursor
while rotating was mixing screen coordinates with container-relative coords,
causing cards to teleport on pointer down.

The solution: Use simple delta positioning (cursor movement from drag start)
and let CSS handle rotation around the card center. While this means the grab
point won't stay perfectly under the cursor as the card rotates, it's much
better than cards jumping unexpectedly.

The rotation still works - cards rotate based on grab point physics - but
the positioning is now stable and predictable.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 11:21:49 -05:00
5 changed files with 141 additions and 48 deletions

View File

@@ -1,3 +1,59 @@
## [4.63.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.9...v4.63.10) (2025-10-22)
### Bug Fixes
* **complement-race:** show only first active player's passengers on train ([2bfd5d2](https://github.com/antialias/soroban-abacus-flashcards/commit/2bfd5d2bda7f7d2d83c69f75600ab461fde15d92))
## [4.63.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.8...v4.63.9) (2025-10-21)
### Bug Fixes
* **homepage:** use app-wide abacus config in interactive flashcards ([cf1be2d](https://github.com/antialias/soroban-abacus-flashcards/commit/cf1be2d1730543bd30836a87d9cbdfd2cf48360e))
## [4.63.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.7...v4.63.8) (2025-10-21)
### Bug Fixes
* **mobile:** restore abacus visibility in "Your Journey" section ([c96036d](https://github.com/antialias/soroban-abacus-flashcards/commit/c96036d86b6de2e25f7ecd3d00dd36221badc3b1))
## [4.63.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.6...v4.63.7) (2025-10-21)
### Bug Fixes
* **mobile:** reduce height of Your Journey section on mobile ([8944035](https://github.com/antialias/soroban-abacus-flashcards/commit/89440355bf494e54072d2d1a1f228c33ec43d52d))
## [4.63.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.5...v4.63.6) (2025-10-21)
### Bug Fixes
* **mobile:** optimize Your Journey section for iPhone displays ([9167fb4](https://github.com/antialias/soroban-abacus-flashcards/commit/9167fb40d68b7bdbe310b647083586434ceb6043))
## [4.63.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.4...v4.63.5) (2025-10-21)
### Bug Fixes
* **flashcards:** store grab offset in local coordinates to prevent jump ([39d93a9](https://github.com/antialias/soroban-abacus-flashcards/commit/39d93a9e9f48a7d1ce10763cad62a600851a41d5))
## [4.63.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.3...v4.63.4) (2025-10-21)
### Bug Fixes
* **flashcards:** keep grab point under cursor with proper coordinate conversion ([1869216](https://github.com/antialias/soroban-abacus-flashcards/commit/1869216d2fda77303c0b79d4f613c6dcdaf5324b))
## [4.63.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.2...v4.63.3) (2025-10-21)
### Bug Fixes
* **flashcards:** revert to simple delta positioning to prevent card jumping ([d018b69](https://github.com/antialias/soroban-abacus-flashcards/commit/d018b699c46aea90e9cdc3309e797ff2d7447ecf))
## [4.63.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.1...v4.63.2) (2025-10-21)

View File

@@ -164,9 +164,14 @@ export function SteamTrainJourney({
// Memoize filtered passenger lists to avoid recalculating on every render
// Arcade room multiplayer uses claimedBy/deliveredBy instead of isBoarded/isDelivered
// Only show passengers claimed by the first active player
const boardedPassengers = useMemo(
() => displayPassengers.filter((p) => p.claimedBy !== null && p.deliveredBy === null),
[displayPassengers]
() =>
displayPassengers.filter(
(p) =>
p.claimedBy === firstActivePlayer?.id && p.claimedBy !== null && p.deliveredBy === null
),
[displayPassengers, firstActivePlayer?.id]
)
const nonDeliveredPassengers = useMemo(

View File

@@ -1,6 +1,6 @@
'use client'
import { AbacusReact } from '@soroban/abacus-react'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { useEffect, useRef, useState } from 'react'
import { css } from '../../styled-system/css'
@@ -81,7 +81,7 @@ export function InteractiveFlashcards() {
})}
>
{cards.map((card) => (
<DraggableCard key={card.id} card={card} />
<DraggableCard key={card.id} card={card} containerRef={containerRef} />
))}
</div>
)
@@ -89,9 +89,12 @@ export function InteractiveFlashcards() {
interface DraggableCardProps {
card: Flashcard
containerRef: React.RefObject<HTMLDivElement>
}
function DraggableCard({ card }: DraggableCardProps) {
function DraggableCard({ card, containerRef }: DraggableCardProps) {
const appConfig = useAbacusConfig()
// Track position - starts at initial, updates when dragged
const [position, setPosition] = useState({ x: card.initialX, y: card.initialY })
const [rotation, setRotation] = useState(card.initialRotation) // Now dynamic!
@@ -124,17 +127,27 @@ function DraggableCard({ card }: DraggableCardProps) {
cardY: position.y,
}
// Calculate grab offset from card center
// Calculate grab offset from card center IN LOCAL COORDINATES (unrotated)
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect()
const cardCenterX = rect.left + rect.width / 2
const cardCenterY = rect.top + rect.height / 2
// Screen-space offset from center
const screenOffsetX = e.clientX - cardCenterX
const screenOffsetY = e.clientY - cardCenterY
// Convert to local coordinates by rotating by -rotation
const currentRotationRad = (rotation * Math.PI) / 180
const cosRot = Math.cos(-currentRotationRad)
const sinRot = Math.sin(-currentRotationRad)
grabOffsetRef.current = {
x: e.clientX - cardCenterX,
y: e.clientY - cardCenterY,
x: screenOffsetX * cosRot - screenOffsetY * sinRot,
y: screenOffsetX * sinRot + screenOffsetY * cosRot,
}
console.log(
`[GrabPoint] Grabbed at offset: (${grabOffsetRef.current.x.toFixed(0)}, ${grabOffsetRef.current.y.toFixed(0)})px from center`
`[GrabPoint] Grabbed at local offset: (${grabOffsetRef.current.x.toFixed(0)}, ${grabOffsetRef.current.y.toFixed(0)})px (screen offset: ${screenOffsetX.toFixed(0)}, ${screenOffsetY.toFixed(0)}px, rotation: ${rotation.toFixed(1)}°)`
)
}
@@ -206,8 +219,8 @@ function DraggableCard({ card }: DraggableCardProps) {
)
}
// Update card position - keep the grab point "stuck" to the cursor
// As the card rotates, the grab point rotates with it, so we need to account for rotation
// Update card position - keep grab point under cursor while rotating
// Calculate the rotated grab offset
const rotationRad = (clampedRotation * Math.PI) / 180
const cosRot = Math.cos(rotationRad)
const sinRot = Math.sin(rotationRad)
@@ -216,35 +229,37 @@ function DraggableCard({ card }: DraggableCardProps) {
const rotatedGrabX = grabOffsetRef.current.x * cosRot - grabOffsetRef.current.y * sinRot
const rotatedGrabY = grabOffsetRef.current.x * sinRot + grabOffsetRef.current.y * cosRot
// Current cursor position
const cursorX = e.clientX
const cursorY = e.clientY
// Card center should be at: cursor position - rotated grab offset
// But we need to position the card element (top-left), not the center
// Get card dimensions to calculate offset from center to top-left
if (cardRef.current) {
const rect = cardRef.current.getBoundingClientRect()
const cardWidth = rect.width
const cardHeight = rect.height
// Card center position in screen space
const cardCenterX = cursorX - rotatedGrabX
const cardCenterY = cursorY - rotatedGrabY
// Convert center position to top-left position (what we store in position state)
// Note: position.x/y is used in translate(), which positions the element
setPosition({
x: cardCenterX - cardWidth / 2,
y: cardCenterY - cardHeight / 2,
})
} else {
// Fallback to simple delta if we don't have card dimensions yet
// Get container bounds for coordinate conversion
if (!containerRef.current || !cardRef.current) {
// Fallback to simple delta if refs not ready
setPosition({
x: dragStartRef.current.cardX + deltaX,
y: dragStartRef.current.cardY + deltaY,
})
return
}
const containerRect = containerRef.current.getBoundingClientRect()
const cardRect = cardRef.current.getBoundingClientRect()
// Current cursor position in viewport space
const cursorViewportX = e.clientX
const cursorViewportY = e.clientY
// Card center should be at: cursor - rotated grab offset (viewport space)
const cardCenterViewportX = cursorViewportX - rotatedGrabX
const cardCenterViewportY = cursorViewportY - rotatedGrabY
// Convert card center from viewport space to container space
const cardCenterContainerX = cardCenterViewportX - containerRect.left
const cardCenterContainerY = cardCenterViewportY - containerRect.top
// position.x/y represents translate() which positions the top-left corner
// So we need: top-left = center - (width/2, height/2)
setPosition({
x: cardCenterContainerX - cardRect.width / 2,
y: cardCenterContainerY - cardRect.height / 2,
})
}
const handlePointerUp = (e: React.PointerEvent) => {
@@ -324,7 +339,7 @@ function DraggableCard({ card }: DraggableCardProps) {
transformOrigin: 'center',
})}
>
<AbacusReact value={card.number} columns={3} beadShape="circle" />
<AbacusReact value={card.number} columns={3} beadShape={appConfig.beadShape} />
</div>
{/* Number display */}

View File

@@ -412,8 +412,9 @@ export function LevelSliderDisplay({
? 'violet.500'
: 'amber.500',
rounded: 'xl',
p: { base: '6', md: '8' },
p: { base: '4', md: '8' },
height: { base: 'auto', md: '700px' },
maxHeight: { base: '500px', md: 'none' },
display: 'flex',
flexDirection: 'column',
})}
@@ -448,7 +449,7 @@ export function LevelSliderDisplay({
key={index}
onClick={() => setCurrentIndex(index)}
className={css({
fontSize: '4xl',
fontSize: { base: '2xl', sm: '3xl', md: '4xl' },
opacity: index === currentIndex ? '1' : '0.3',
transition: 'all 0.2s',
cursor: 'pointer',
@@ -500,8 +501,8 @@ export function LevelSliderDisplay({
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
w: '180px',
h: '128px',
w: { base: '120px', md: '180px' },
h: { base: '96px', md: '128px' },
bg: 'transparent',
cursor: 'grab',
transition: 'transform 0.15s ease-out, left 0.3s ease-out',
@@ -514,7 +515,12 @@ export function LevelSliderDisplay({
_active: { cursor: 'grabbing' },
})}
>
<div className={css({ opacity: 0.75 })}>
<div
className={css({
opacity: 0.75,
transform: { base: 'scale(0.75)', md: 'scale(1)' },
})}
>
<StandaloneBead
size={128}
color={currentLevel.color === 'violet' ? '#8b5cf6' : '#22c55e'}
@@ -607,14 +613,16 @@ export function LevelSliderDisplay({
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', lg: 'row' },
gap: '4',
p: '6',
p: { base: '4', md: '6' },
bg: 'rgba(0, 0, 0, 0.3)',
rounded: 'lg',
border: '1px solid',
borderColor: 'gray.700',
overflow: 'hidden',
flex: 1,
minH: 0, // Allow flex shrinking
})}
>
{/* Level Details (only for Kyu levels) */}
@@ -633,12 +641,14 @@ export function LevelSliderDisplay({
<div
className={css({
flex: '0 0 auto',
display: 'grid',
display: { base: 'none', lg: 'grid' },
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '2',
p: '2',
w: '100%',
maxW: '400px',
alignContent: 'center',
justifyItems: 'center',
})}
>
{sections.map((section, idx) => {
@@ -666,8 +676,10 @@ export function LevelSliderDisplay({
justifyContent: 'center',
gap: '1.5',
opacity: hasData ? 1 : 0.3,
width: '170px',
height: '150px',
w: { base: '100%', sm: 'auto' },
minW: { sm: '140px' },
maxW: { base: '170px', sm: '170px' },
minH: '150px',
_hover: hasData
? {
borderColor: 'gray.500',
@@ -733,13 +745,18 @@ export function LevelSliderDisplay({
) : null
})()}
{/* Abacus (right-aligned for Kyu, centered for Dan) */}
{/* Abacus (centered on mobile, right-aligned for Kyu on desktop, centered for Dan) */}
<div
className={css({
display: 'flex',
justifyContent: currentLevel.type === 'kyu' ? 'flex-end' : 'center',
justifyContent:
currentLevel.type === 'kyu' ? { base: 'center', lg: 'flex-end' } : 'center',
alignItems: 'center',
flex: 1,
overflowX: 'auto',
overflowY: 'hidden',
minW: 0, // Allow flex shrinking
minH: 0, // Allow flex shrinking
})}
>
<animated.div

View File

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