Compare commits

...

6 Commits

Author SHA1 Message Date
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
4 changed files with 106 additions and 21 deletions

View File

@@ -1,3 +1,24 @@
## [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)

View File

@@ -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,10 @@ export function InteractiveFlashcards() {
interface DraggableCardProps {
card: Flashcard
containerRef: React.RefObject<HTMLDivElement>
}
function DraggableCard({ card }: DraggableCardProps) {
function DraggableCard({ card, containerRef }: DraggableCardProps) {
// 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 +125,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,11 +217,46 @@ function DraggableCard({ card }: DraggableCardProps) {
)
}
// Update card position - simple delta from drag start
// The rotation is visual only and happens around the card's center via CSS transform-origin
// 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)
// Rotate the grab offset by the current rotation angle
const rotatedGrabX = grabOffsetRef.current.x * cosRot - grabOffsetRef.current.y * sinRot
const rotatedGrabY = grabOffsetRef.current.x * sinRot + grabOffsetRef.current.y * cosRot
// 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: dragStartRef.current.cardX + deltaX,
y: dragStartRef.current.cardY + deltaY,
x: cardCenterContainerX - cardRect.width / 2,
y: cardCenterContainerY - cardRect.height / 2,
})
}

View File

@@ -448,7 +448,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 +500,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 +514,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,8 +612,9 @@ 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',
@@ -634,11 +640,17 @@ export function LevelSliderDisplay({
className={css({
flex: '0 0 auto',
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridTemplateColumns: {
base: 'repeat(2, 1fr)',
sm: 'repeat(3, 1fr)',
lg: 'repeat(2, 1fr)',
},
gap: '2',
p: '2',
maxW: '400px',
w: '100%',
maxW: { base: '100%', lg: '400px' },
alignContent: 'center',
justifyItems: 'center',
})}
>
{sections.map((section, idx) => {
@@ -666,8 +678,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',
@@ -737,9 +751,13 @@ export function LevelSliderDisplay({
<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
})}
>
<animated.div

View File

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