Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106b348585 | ||
|
|
7668cc9b11 | ||
|
|
93527e6e0b | ||
|
|
ef4ca57a6c | ||
|
|
095221564f | ||
|
|
2bfd5d2bda | ||
|
|
6dabb71600 | ||
|
|
cf1be2d173 | ||
|
|
0169ab5128 | ||
|
|
c96036d86b | ||
|
|
653db575ff | ||
|
|
89440355bf | ||
|
|
632e840ca7 | ||
|
|
9167fb40d6 | ||
|
|
1d7486ed48 | ||
|
|
39d93a9e9f | ||
|
|
6d1bad142b | ||
|
|
1869216d2f | ||
|
|
e4ae3aefef | ||
|
|
d018b699c4 | ||
|
|
be323bfbc5 | ||
|
|
50fc3fdf7f |
77
CHANGELOG.md
77
CHANGELOG.md
@@ -1,3 +1,80 @@
|
||||
## [4.64.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.11...v4.64.0) (2025-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **complement-race:** add ghost trains for multiplayer visibility ([7668cc9](https://github.com/antialias/soroban-abacus-flashcards/commit/7668cc9b113b3eae2acb1b852b0ad48c979e6604))
|
||||
|
||||
## [4.63.11](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.10...v4.63.11) (2025-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **complement-race:** actually filter by isActive instead of just id ([ef4ca57](https://github.com/antialias/soroban-abacus-flashcards/commit/ef4ca57a6c3f35d1bddc6a70952f478058fbc6b5))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** correct pivot point to rotate around card center ([50fc3fd](https://github.com/antialias/soroban-abacus-flashcards/commit/50fc3fdf7f2c9b7412f6d7d890f5e0d52cb86a9b))
|
||||
|
||||
## [4.63.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.0...v4.63.1) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { PlayerState } from '@/arcade-games/complement-race/types'
|
||||
import type { RailroadTrackGenerator } from '../../lib/RailroadTrackGenerator'
|
||||
|
||||
interface GhostTrainProps {
|
||||
player: PlayerState
|
||||
trainPosition: number
|
||||
trackGenerator: RailroadTrackGenerator
|
||||
pathRef: React.RefObject<SVGPathElement>
|
||||
}
|
||||
|
||||
/**
|
||||
* GhostTrain - Renders a semi-transparent train for other players in multiplayer
|
||||
* Shows opponent positions in real-time during steam sprint races
|
||||
*/
|
||||
export function GhostTrain({ player, trainPosition, trackGenerator, pathRef }: GhostTrainProps) {
|
||||
const ghostRef = useRef<SVGGElement>(null)
|
||||
|
||||
// Calculate train transform using same logic as local player
|
||||
const trainTransform = useMemo(() => {
|
||||
if (!pathRef.current) {
|
||||
return { x: 0, y: 0, rotation: 0, opacity: 0 }
|
||||
}
|
||||
|
||||
const pathLength = pathRef.current.getTotalLength()
|
||||
const targetDistance = (trainPosition / 100) * pathLength
|
||||
const point = pathRef.current.getPointAtLength(targetDistance)
|
||||
|
||||
// Calculate tangent for rotation
|
||||
const tangentDelta = 1
|
||||
const tangentDistance = Math.min(targetDistance + tangentDelta, pathLength)
|
||||
const tangentPoint = pathRef.current.getPointAtLength(tangentDistance)
|
||||
const rotation =
|
||||
(Math.atan2(tangentPoint.y - point.y, tangentPoint.x - point.x) * 180) / Math.PI
|
||||
|
||||
return {
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
rotation,
|
||||
opacity: 0.35, // Ghost effect - 35% opacity
|
||||
}
|
||||
}, [trainPosition, pathRef])
|
||||
|
||||
// Don't render if position data isn't ready
|
||||
if (trainTransform.opacity === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<g
|
||||
ref={ghostRef}
|
||||
data-component="ghost-train"
|
||||
data-player-id={player.id}
|
||||
transform={`translate(${trainTransform.x}, ${trainTransform.y}) rotate(${trainTransform.rotation}) scale(-1, 1)`}
|
||||
opacity={trainTransform.opacity}
|
||||
style={{
|
||||
transition: 'opacity 0.3s ease-in',
|
||||
}}
|
||||
>
|
||||
{/* Ghost locomotive */}
|
||||
<text
|
||||
data-element="ghost-locomotive"
|
||||
x={0}
|
||||
y={0}
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontSize: '100px',
|
||||
filter: `drop-shadow(0 2px 8px ${player.color || 'rgba(100, 100, 255, 0.6)'})`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
🚂
|
||||
</text>
|
||||
|
||||
{/* Player name label - positioned above train */}
|
||||
<text
|
||||
data-element="ghost-label"
|
||||
x={0}
|
||||
y={-60}
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
fill: player.color || '#6366f1',
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))',
|
||||
pointerEvents: 'none',
|
||||
transform: 'scaleX(-1)', // Counter the parent's scaleX(-1)
|
||||
}}
|
||||
>
|
||||
{player.name || `Player ${player.id.slice(0, 4)}`}
|
||||
</text>
|
||||
|
||||
{/* Score indicator - positioned below train */}
|
||||
<text
|
||||
data-element="ghost-score"
|
||||
x={0}
|
||||
y={50}
|
||||
textAnchor="middle"
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
fill: 'rgba(255, 255, 255, 0.9)',
|
||||
filter: 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5))',
|
||||
pointerEvents: 'none',
|
||||
transform: 'scaleX(-1)', // Counter the parent's scaleX(-1)
|
||||
}}
|
||||
>
|
||||
{player.score}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { GameHUD } from './GameHUD'
|
||||
import { RailroadTrackPath } from './RailroadTrackPath'
|
||||
import { TrainAndCars } from './TrainAndCars'
|
||||
import { TrainTerrainBackground } from './TrainTerrainBackground'
|
||||
import { GhostTrain } from './GhostTrain'
|
||||
|
||||
const BoardingPassengerAnimation = memo(({ animation }: { animation: BoardingAnimation }) => {
|
||||
const spring = useSpring({
|
||||
@@ -92,7 +93,7 @@ export function SteamTrainJourney({
|
||||
currentQuestion,
|
||||
currentInput,
|
||||
}: SteamTrainJourneyProps) {
|
||||
const { state } = useComplementRace()
|
||||
const { state, multiplayerState, localPlayerId } = useComplementRace()
|
||||
|
||||
const { getSkyGradient, getTimeOfDayPeriod } = useSteamJourney()
|
||||
const _skyGradient = getSkyGradient()
|
||||
@@ -101,7 +102,7 @@ export function SteamTrainJourney({
|
||||
const { profile: _profile } = useUserProfile()
|
||||
|
||||
// Get the first active player's emoji
|
||||
const activePlayers = Array.from(players.values()).filter((p) => p.id)
|
||||
const activePlayers = Array.from(players.values()).filter((p) => p.isActive)
|
||||
const firstActivePlayer = activePlayers[0]
|
||||
const playerEmoji = firstActivePlayer?.emoji ?? '👤'
|
||||
|
||||
@@ -164,9 +165,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(
|
||||
@@ -186,6 +192,14 @@ export function SteamTrainJourney({
|
||||
[]
|
||||
)
|
||||
|
||||
// Get other players for ghost trains (filter out local player)
|
||||
const otherPlayers = useMemo(() => {
|
||||
if (!multiplayerState?.players || !localPlayerId) return []
|
||||
return Object.entries(multiplayerState.players)
|
||||
.filter(([playerId, player]) => playerId !== localPlayerId && player.isActive)
|
||||
.map(([_, player]) => player)
|
||||
}, [multiplayerState?.players, localPlayerId])
|
||||
|
||||
if (!trackData) return null
|
||||
|
||||
return (
|
||||
@@ -247,7 +261,18 @@ export function SteamTrainJourney({
|
||||
disembarkingAnimations={disembarkingAnimations}
|
||||
/>
|
||||
|
||||
{/* Train, cars, and passenger animations */}
|
||||
{/* Ghost trains - other players in multiplayer */}
|
||||
{otherPlayers.map((player) => (
|
||||
<GhostTrain
|
||||
key={player.id}
|
||||
player={player}
|
||||
trainPosition={trainPosition} // For now, use same position calculation
|
||||
trackGenerator={trackGenerator}
|
||||
pathRef={pathRef}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Train, cars, and passenger animations - local player */}
|
||||
<TrainAndCars
|
||||
boardingAnimations={boardingAnimations}
|
||||
disembarkingAnimations={disembarkingAnimations}
|
||||
|
||||
@@ -99,6 +99,8 @@ interface CompatibleGameState {
|
||||
*/
|
||||
interface ComplementRaceContextValue {
|
||||
state: CompatibleGameState // Return adapted state
|
||||
multiplayerState: ComplementRaceState // Raw multiplayer state for rendering other players
|
||||
localPlayerId: string | undefined // Local player ID for filtering
|
||||
dispatch: (action: { type: string; [key: string]: any }) => void // Compatibility layer
|
||||
lastError: string | null
|
||||
startGame: () => void
|
||||
@@ -914,6 +916,8 @@ export function ComplementRaceProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
const contextValue: ComplementRaceContextValue = {
|
||||
state: compatibleState, // Use transformed state
|
||||
multiplayerState, // Expose raw multiplayer state for ghost trains
|
||||
localPlayerId, // Expose local player ID for filtering
|
||||
dispatch,
|
||||
lastError,
|
||||
startGame,
|
||||
|
||||
@@ -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,10 +219,46 @@ function DraggableCard({ card }: DraggableCardProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Update card position
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -290,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 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.63.1",
|
||||
"version": "4.64.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user