Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
291bcc581d | ||
|
|
26edec1bbf | ||
|
|
da4fdc90e0 | ||
|
|
ee6c4f2f4f | ||
|
|
9b9f0cdbcb | ||
|
|
e14ffe44d6 | ||
|
|
d5bc0bb27c | ||
|
|
0790074ffc |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -1,3 +1,31 @@
|
||||
## [4.11.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.0...v4.11.1) (2025-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](https://github.com/antialias/soroban-abacus-flashcards/commit/26edec1bbf038264405ec9d161edcd18f67a6fc6))
|
||||
|
||||
## [4.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.6...v4.11.0) (2025-10-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **home:** redesign home page to showcase complete platform ([ee6c4f2](https://github.com/antialias/soroban-abacus-flashcards/commit/ee6c4f2f4f39e3b30f59c54866c3857c218fb80f))
|
||||
|
||||
## [4.10.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.5...v4.10.6) (2025-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](https://github.com/antialias/soroban-abacus-flashcards/commit/e14ffe44d66d0c97bc0cc4e0c255698e88ce723a))
|
||||
|
||||
## [4.10.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.4...v4.10.5) (2025-10-18)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **arcade:** merge /arcade/room into /arcade route ([0790074](https://github.com/antialias/soroban-abacus-flashcards/commit/0790074ffc5008bce9a162fe0ddbd1d5c214c4f7))
|
||||
|
||||
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)
|
||||
|
||||
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
"Bash(pnpm exec turbo build --filter=@soroban/web)",
|
||||
"Bash(do gh run list --limit 1 --json conclusion,status,name,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
|
||||
"Bash(do gh run list --limit 1 --json conclusion,status,name --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - \\(.name)\"\"')",
|
||||
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')"
|
||||
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
|
||||
"WebFetch(domain:abaci.one)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -5,16 +5,9 @@ import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
|
||||
import { GAMES_CONFIG } from '@/components/GameSelector'
|
||||
import type { GameType } from '@/components/GameSelector'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
|
||||
|
||||
// Map GameType keys to internal game names
|
||||
// Note: "battle-arena" removed - now handled by game registry as "matching"
|
||||
const GAME_TYPE_TO_NAME: Record<GameType, string> = {
|
||||
'complement-race': 'complement-race',
|
||||
'master-organizer': 'master-organizer',
|
||||
}
|
||||
|
||||
/**
|
||||
* /arcade - Renders the game for the user's current room
|
||||
* Since users can only be in one room at a time, this is a simple singular route
|
||||
@@ -84,7 +77,7 @@ export default function RoomPage() {
|
||||
const handleGameSelect = (gameType: GameType) => {
|
||||
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
|
||||
|
||||
// Check if it's a registry game first
|
||||
// All games are now in the registry
|
||||
if (hasGame(gameType)) {
|
||||
const gameDef = getGame(gameType)
|
||||
if (!gameDef?.manifest.available) {
|
||||
@@ -95,47 +88,12 @@ export default function RoomPage() {
|
||||
console.log('[RoomPage] Selecting registry game:', gameType)
|
||||
setRoomGame({
|
||||
roomId: roomData.id,
|
||||
gameName: gameType, // Use the game name directly for registry games
|
||||
gameName: gameType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy game handling
|
||||
const gameConfig = GAMES_CONFIG[gameType as keyof typeof GAMES_CONFIG]
|
||||
if (!gameConfig) {
|
||||
console.log('[RoomPage] Unknown game type:', gameType)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[RoomPage] Game config:', {
|
||||
name: gameConfig.name,
|
||||
available: 'available' in gameConfig ? gameConfig.available : true,
|
||||
})
|
||||
|
||||
if ('available' in gameConfig && gameConfig.available === false) {
|
||||
console.log('[RoomPage] Game not available, blocking selection')
|
||||
return // Don't allow selecting unavailable games
|
||||
}
|
||||
|
||||
// Map GameType to internal game name
|
||||
const internalGameName = GAME_TYPE_TO_NAME[gameType]
|
||||
console.log('[RoomPage] Mapping:', {
|
||||
gameType,
|
||||
internalGameName,
|
||||
mappingExists: !!internalGameName,
|
||||
})
|
||||
|
||||
console.log('[RoomPage] Calling setRoomGame with:', {
|
||||
roomId: roomData.id,
|
||||
gameName: internalGameName,
|
||||
preservingGameConfig: true,
|
||||
})
|
||||
|
||||
// Don't pass gameConfig - we want to preserve existing settings for all games
|
||||
setRoomGame({
|
||||
roomId: roomData.id,
|
||||
gameName: internalGameName,
|
||||
})
|
||||
console.log('[RoomPage] Unknown game type:', gameType)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -178,7 +136,7 @@ export default function RoomPage() {
|
||||
})}
|
||||
>
|
||||
{/* Legacy games */}
|
||||
{Object.entries(GAMES_CONFIG).map(([gameType, config]) => {
|
||||
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
|
||||
const isAvailable = !('available' in config) || config.available !== false
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -3,133 +3,400 @@
|
||||
import Link from 'next/link'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack, stack } from '../../styled-system/patterns'
|
||||
import { container, grid, hstack, stack } from '../../styled-system/patterns'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<PageWithNav navTitle="Soroban Flashcards" navEmoji="🧮">
|
||||
<div
|
||||
className={css({
|
||||
minHeight: '100vh',
|
||||
bg: 'gradient-to-br from-brand.50 to-brand.100',
|
||||
})}
|
||||
>
|
||||
{/* Hero Section */}
|
||||
<main className={container({ maxW: '6xl', px: '4' })}>
|
||||
<div
|
||||
className={stack({
|
||||
gap: '12',
|
||||
py: '16',
|
||||
align: 'center',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{/* Hero Content */}
|
||||
<div className={stack({ gap: '6', maxW: '4xl' })}>
|
||||
<PageWithNav navTitle="Soroban Mastery Platform" navEmoji="🧮">
|
||||
<div className={css({ bg: 'gray.50', minHeight: '100vh' })}>
|
||||
{/* Compact Hero */}
|
||||
<div
|
||||
className={css({
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
color: 'white',
|
||||
py: { base: '8', md: '12' },
|
||||
})}
|
||||
>
|
||||
<div className={container({ maxW: '6xl', px: '4' })}>
|
||||
<div className={css({ textAlign: 'center', maxW: '4xl', mx: 'auto' })}>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '4xl', md: '6xl' },
|
||||
fontSize: { base: '3xl', md: '5xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '4',
|
||||
lineHeight: 'tight',
|
||||
})}
|
||||
>
|
||||
Beautiful Soroban <span className={css({ color: 'brand.600' })}>Flashcards</span>
|
||||
Master Soroban Through{' '}
|
||||
<span className={css({ color: 'yellow.300' })}>Play & Practice</span>
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
color: 'gray.600',
|
||||
maxW: '2xl',
|
||||
mx: 'auto',
|
||||
})}
|
||||
>
|
||||
Create stunning, educational flashcards with authentic Japanese abacus
|
||||
representations. Perfect for teachers, students, and mental math enthusiasts.
|
||||
<p className={css({ fontSize: { base: 'md', md: 'lg' }, opacity: 0.95, mb: '6' })}>
|
||||
Interactive tutorials, multiplayer games, and beautiful flashcards—your complete
|
||||
soroban learning ecosystem
|
||||
</p>
|
||||
|
||||
<div className={hstack({ gap: '4', justify: 'center', mt: '8' })}>
|
||||
<div className={hstack({ gap: '3', justify: 'center', flexWrap: 'wrap' })}>
|
||||
<Link
|
||||
href="/create"
|
||||
href="/arcade"
|
||||
className={css({
|
||||
px: '8',
|
||||
py: '4',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
rounded: 'xl',
|
||||
shadow: 'card',
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'yellow.400',
|
||||
color: 'gray.900',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
_hover: { bg: 'yellow.300', transform: 'translateY(-2px)' },
|
||||
transition: 'all',
|
||||
_hover: {
|
||||
bg: 'brand.700',
|
||||
transform: 'translateY(-2px)',
|
||||
shadow: 'modal',
|
||||
},
|
||||
})}
|
||||
>
|
||||
✨ Start Creating →
|
||||
🎮 Play Games
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/guide"
|
||||
className={css({
|
||||
px: '8',
|
||||
py: '4',
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'white',
|
||||
color: 'brand.700',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
rounded: 'xl',
|
||||
shadow: 'card',
|
||||
border: '2px solid',
|
||||
borderColor: 'brand.200',
|
||||
color: 'purple.700',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
_hover: { bg: 'gray.100', transform: 'translateY(-2px)' },
|
||||
transition: 'all',
|
||||
_hover: {
|
||||
borderColor: 'brand.400',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
📚 Learn Soroban
|
||||
📚 Learn
|
||||
</Link>
|
||||
<Link
|
||||
href="/create"
|
||||
className={css({
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'purple.600',
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
_hover: { bg: 'purple.700', transform: 'translateY(-2px)' },
|
||||
transition: 'all',
|
||||
})}
|
||||
>
|
||||
🎨 Create
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Grid */}
|
||||
<div
|
||||
{/* Main Content Grid */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
|
||||
<div className={stack({ gap: '8' })}>
|
||||
{/* Arcade Games Section */}
|
||||
<section>
|
||||
<div className={hstack({ justify: 'space-between', mb: '4' })}>
|
||||
<h2 className={css({ fontSize: '2xl', fontWeight: 'bold', color: 'gray.900' })}>
|
||||
🕹️ Multiplayer Arcade
|
||||
</h2>
|
||||
<Link
|
||||
href="/arcade"
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'purple.600',
|
||||
fontWeight: 'semibold',
|
||||
_hover: { color: 'purple.700' },
|
||||
})}
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
</div>
|
||||
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '4' })}>
|
||||
<GameCard
|
||||
icon="🧠"
|
||||
title="Memory Lightning"
|
||||
description="Memorize soroban numbers"
|
||||
players="1-8 players"
|
||||
tags={['Co-op', 'Competitive']}
|
||||
/>
|
||||
<GameCard
|
||||
icon="⚔️"
|
||||
title="Matching Pairs"
|
||||
description="Turn-based card battles"
|
||||
players="1-4 players"
|
||||
tags={['Pattern Recognition']}
|
||||
/>
|
||||
<GameCard
|
||||
icon="🏁"
|
||||
title="Speed Race"
|
||||
description="Race AI with complements"
|
||||
players="1-4 players + AI"
|
||||
tags={['Practice', 'Sprint', 'Survival']}
|
||||
/>
|
||||
<GameCard
|
||||
icon="🔢"
|
||||
title="Card Sorting"
|
||||
description="Arrange cards visually"
|
||||
players="Solo challenge"
|
||||
tags={['Visual Literacy']}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Two Column Layout */}
|
||||
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '6' })}>
|
||||
{/* Interactive Learning */}
|
||||
<section
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
shadow: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
📚 Interactive Learning
|
||||
</h2>
|
||||
<div className={stack({ gap: '3' })}>
|
||||
<FeatureItem
|
||||
icon="🔍"
|
||||
title="Reading Numbers"
|
||||
description="Visual tutorials on interpreting bead positions"
|
||||
/>
|
||||
<FeatureItem
|
||||
icon="🧮"
|
||||
title="Arithmetic Operations"
|
||||
description="Step-by-step interactive practice: +, −, ×, ÷"
|
||||
/>
|
||||
<FeatureItem
|
||||
icon="🎯"
|
||||
title="Guided Tutorials"
|
||||
description="Hands-on exercises with instant feedback"
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
href="/guide"
|
||||
className={css({
|
||||
display: 'block',
|
||||
mt: '4',
|
||||
py: '2',
|
||||
textAlign: 'center',
|
||||
bg: 'purple.50',
|
||||
color: 'purple.700',
|
||||
fontWeight: 'semibold',
|
||||
rounded: 'lg',
|
||||
_hover: { bg: 'purple.100' },
|
||||
})}
|
||||
>
|
||||
Start Learning →
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
{/* Flashcard Creator */}
|
||||
<section
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
shadow: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
🎨 Flashcard Creator
|
||||
</h2>
|
||||
<div className={stack({ gap: '3' })}>
|
||||
<FeatureItem
|
||||
icon="📄"
|
||||
title="Multiple Formats"
|
||||
description="PDF, PNG, SVG, interactive HTML"
|
||||
/>
|
||||
<FeatureItem
|
||||
icon="🎨"
|
||||
title="Custom Styling"
|
||||
description="Bead shapes, color schemes, fonts, layouts"
|
||||
/>
|
||||
<FeatureItem
|
||||
icon="📐"
|
||||
title="Paper Options"
|
||||
description="A3, A4, A5, US Letter • Portrait/Landscape"
|
||||
/>
|
||||
</div>
|
||||
<Link
|
||||
href="/create"
|
||||
className={css({
|
||||
display: 'block',
|
||||
mt: '4',
|
||||
py: '2',
|
||||
textAlign: 'center',
|
||||
bg: 'blue.50',
|
||||
color: 'blue.700',
|
||||
fontWeight: 'semibold',
|
||||
rounded: 'lg',
|
||||
_hover: { bg: 'blue.100' },
|
||||
})}
|
||||
>
|
||||
Create Flashcards →
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Multiplayer Features */}
|
||||
<section>
|
||||
<h2
|
||||
className={css({ fontSize: '2xl', fontWeight: 'bold', color: 'gray.900', mb: '4' })}
|
||||
>
|
||||
🌐 Multiplayer Features
|
||||
</h2>
|
||||
<div className={grid({ columns: { base: 1, sm: 2, md: 4 }, gap: '4' })}>
|
||||
<FeatureCard
|
||||
icon="🎭"
|
||||
title="Player Characters"
|
||||
description="Custom names, emojis, and colors for each player"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="🏠"
|
||||
title="Private Rooms"
|
||||
description="Create rooms with codes, passwords, or approval-only access"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="⚡"
|
||||
title="Real-time Play"
|
||||
description="Socket.io powered instant multiplayer sync"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="📊"
|
||||
title="Stats & Progress"
|
||||
description="Track wins, accuracy, and performance across games"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<section
|
||||
className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1', md: '3' },
|
||||
gap: '8',
|
||||
mt: '16',
|
||||
w: 'full',
|
||||
bg: 'gradient-to-r',
|
||||
gradientFrom: 'purple.600',
|
||||
gradientTo: 'indigo.600',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
color: 'white',
|
||||
})}
|
||||
>
|
||||
<FeatureCard
|
||||
icon="🎨"
|
||||
title="Beautiful Design"
|
||||
description="Vector graphics, color schemes, authentic bead positioning"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="⚡"
|
||||
title="Instant Generation"
|
||||
description="Create PDFs, interactive HTML, PNGs, and SVGs in seconds"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="🎯"
|
||||
title="Educational Focus"
|
||||
description="Perfect for teachers, students, and soroban enthusiasts"
|
||||
/>
|
||||
</div>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
mb: '4',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
Complete Soroban Learning Platform
|
||||
</h2>
|
||||
<div className={grid({ columns: { base: 2, md: 4 }, gap: '6', textAlign: 'center' })}>
|
||||
<StatItem number="4" label="Arcade Games" />
|
||||
<StatItem number="8" label="Max Players" />
|
||||
<StatItem number="3" label="Learning Modes" />
|
||||
<StatItem number="4+" label="Export Formats" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
||||
function GameCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
players,
|
||||
tags,
|
||||
}: {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
players: string
|
||||
tags: string[]
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'lg',
|
||||
p: '4',
|
||||
shadow: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
transition: 'all',
|
||||
_hover: { shadow: 'md', transform: 'translateY(-2px)' },
|
||||
})}
|
||||
>
|
||||
<div className={css({ fontSize: '2xl', mb: '2' })}>{icon}</div>
|
||||
<h3 className={css({ fontSize: 'md', fontWeight: 'bold', color: 'gray.900', mb: '1' })}>
|
||||
{title}
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.600', mb: '2' })}>{description}</p>
|
||||
<p className={css({ fontSize: 'xs', color: 'gray.500', mb: '2' })}>{players}</p>
|
||||
<div className={hstack({ gap: '1', flexWrap: 'wrap' })}>
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
px: '2',
|
||||
py: '0.5',
|
||||
bg: 'purple.100',
|
||||
color: 'purple.700',
|
||||
rounded: 'full',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureItem({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
|
||||
<div className={css({ fontSize: 'xl', flexShrink: 0 })}>{icon}</div>
|
||||
<div>
|
||||
<h4 className={css({ fontSize: 'sm', fontWeight: 'semibold', color: 'gray.900' })}>
|
||||
{title}
|
||||
</h4>
|
||||
<p className={css({ fontSize: 'xs', color: 'gray.600' })}>{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
@@ -142,44 +409,31 @@ function FeatureCard({
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
p: '8',
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
rounded: 'lg',
|
||||
p: '4',
|
||||
shadow: 'sm',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
textAlign: 'center',
|
||||
transition: 'all',
|
||||
_hover: {
|
||||
transform: 'translateY(-4px)',
|
||||
shadow: 'modal',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '4xl',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
<div className={css({ fontSize: '2xl', mb: '2' })}>{icon}</div>
|
||||
<h3 className={css({ fontSize: 'sm', fontWeight: 'bold', color: 'gray.900', mb: '1' })}>
|
||||
{title}
|
||||
</h3>
|
||||
<p
|
||||
className={css({
|
||||
color: 'gray.600',
|
||||
lineHeight: 'relaxed',
|
||||
})}
|
||||
>
|
||||
<p className={css({ fontSize: 'xs', color: 'gray.600', lineHeight: 'relaxed' })}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatItem({ number, label }: { number: string; label: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className={css({ fontSize: '3xl', fontWeight: 'bold', mb: '1' })}>{number}</div>
|
||||
<div className={css({ fontSize: 'sm', opacity: 0.9 })}>{label}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,7 +321,10 @@ export function PlayingPhase() {
|
||||
justifyContent: 'center',
|
||||
'& svg': {
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
margin: '0 auto',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
@@ -362,8 +365,14 @@ export function PlayingPhase() {
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.25rem',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '15px',
|
||||
background: 'rgba(255,255,255,0.7)',
|
||||
borderRadius: '8px',
|
||||
border: '2px dashed #2c5f76',
|
||||
})}
|
||||
>
|
||||
{/* Insert button before first position */}
|
||||
@@ -402,9 +411,10 @@ export function PlayingPhase() {
|
||||
const isEmpty = card === null
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<>
|
||||
{/* Position slot */}
|
||||
<div
|
||||
key={`slot-${index}`}
|
||||
onClick={() => handleSlotClick(index)}
|
||||
className={css({
|
||||
width: '90px',
|
||||
@@ -449,10 +459,17 @@ export function PlayingPhase() {
|
||||
__html: card.svgContent,
|
||||
}}
|
||||
className={css({
|
||||
width: '70px',
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'& svg': {
|
||||
width: '100%',
|
||||
maxWidth: '70px',
|
||||
maxHeight: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
margin: '0 auto',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
@@ -483,6 +500,7 @@ export function PlayingPhase() {
|
||||
|
||||
{/* Insert button after this position */}
|
||||
<button
|
||||
key={`insert-${index + 1}`}
|
||||
type="button"
|
||||
onClick={() => handleInsertClick(index + 1)}
|
||||
disabled={!selectedCardId}
|
||||
@@ -498,7 +516,6 @@ export function PlayingPhase() {
|
||||
cursor: selectedCardId ? 'pointer' : 'default',
|
||||
opacity: selectedCardId ? 1 : 0.3,
|
||||
transition: 'all 0.2s',
|
||||
marginTop: '0.25rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -511,7 +528,7 @@ export function PlayingPhase() {
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.10.4",
|
||||
"version": "4.11.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user