Compare commits

...

18 Commits

Author SHA1 Message Date
semantic-release-bot
16d978db9a chore(release): 4.52.0 [skip ci]
## [4.52.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.51.0...v4.52.0) (2025-10-20)

### Features

* **homepage:** add interactive draggable flashcards with physics ([e711c52](e711c52757))
2025-10-20 22:39:00 +00:00
Thomas Hallock
e711c52757 feat(homepage): add interactive draggable flashcards with physics
Add a fun, interactive flashcard display to the homepage's flashcard
generator section. Users can drag and throw 8-15 randomly generated
flashcards around with realistic physics-based momentum.

Features:
- Drag and drop flashcards with mouse/touch
- Throw cards with velocity-based physics
- 8-15 randomly generated flashcards (100-999 range)
- Real AbacusReact components for each card
- Client-side rendering to avoid hydration errors

Technical implementation:
- Uses @use-gesture/react for drag gesture handling
- Uses @react-spring/web for smooth physics animations
- Cards generated client-side with useEffect to prevent SSR mismatch
- Each card maintains its own spring-based position and rotation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:37:48 -05:00
semantic-release-bot
009162e22c chore(release): 4.51.0 [skip ci]
## [4.51.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.1...v4.51.0) (2025-10-20)

### Features

* **homepage:** create fancy flashcard display with spread-out cards ([cd30944](cd30944c5e))
2025-10-20 22:33:29 +00:00
Thomas Hallock
cd30944c5e feat(homepage): create fancy flashcard display with spread-out cards
Transform the flashcard generator section into an eye-catching display
featuring five rotated flashcards spread out in a fan pattern, each
showing real AbacusReact components with different numbers.

Features:
- Five flashcards (123, 456, 789, 321, 654) rotated at different angles
- Spread out horizontally for a dynamic, fanned-out effect
- Interactive hover effects - cards lift and scale on hover
- Real AbacusReact components showing actual bead positions
- Feature boxes highlighting capabilities (formats, customization, sizes)
- Blue glow effect on hover for the entire section
- Matches the visual style of other homepage sections

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:32:15 -05:00
semantic-release-bot
3e58cb5f92 chore(release): 4.50.1 [skip ci]
## [4.50.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.0...v4.50.1) (2025-10-20)

### Bug Fixes

* **homepage:** set fixed width for learning panel to prevent layout shift ([dc19622](dc19622bbb))
* **homepage:** set fixed width for tutorial panel to prevent layout shift ([aba9f8a](aba9f8a94d))
2025-10-20 22:29:57 +00:00
Thomas Hallock
aba9f8a94d fix(homepage): set fixed width for tutorial panel to prevent layout shift
Set the tutorial panel (left side) to a fixed width of 500px on desktop
to prevent the layout from shifting when switching between tutorials
with different text lengths.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:28:48 -05:00
Thomas Hallock
dc19622bbb fix(homepage): set fixed width for learning panel to prevent layout shift
Changed the "What You'll Learn" panel from maxW to a fixed width (420px)
on desktop screens to prevent it from shifting when switching between
tutorials with different text lengths.

Also added minW: '0' to the tutorial panel to allow proper flex shrinking.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:27:58 -05:00
semantic-release-bot
1babfde328 chore(release): 4.50.0 [skip ci]
## [4.50.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.1...v4.50.0) (2025-10-20)

### Features

* **homepage:** add interactive learning panels with animated mini-tutorials ([76d6f19](76d6f19d51))
2025-10-20 22:23:24 +00:00
Thomas Hallock
76d6f19d51 feat(homepage): add interactive learning panels with animated mini-tutorials
Transform "What You'll Learn" section into an interactive experience where
users can click skill panels to see corresponding tutorials with animated
abacus demonstrations.

Changes:
- Make skill panels clickable to switch between different tutorials
- Replace static emojis with animated MiniAbacus components for each skill
- Create skill-specific tutorials: basic numbers, friends of 5,
  multiplication, and mental calculation
- Add visual indication for selected panel (yellow glow effect)
- Update MiniAbacus component to cycle through custom value sequences
- Default to "Friends techniques" panel (Friends of 5 tutorial)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:22:11 -05:00
semantic-release-bot
9ad35e65d3 chore(release): 4.49.1 [skip ci]
## [4.49.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.0...v4.49.1) (2025-10-20)

### Code Refactoring

* **homepage:** streamline homepage sections ([d362a77](d362a770d6))
2025-10-20 22:19:50 +00:00
Thomas Hallock
d362a770d6 refactor(homepage): streamline homepage sections
Simplified homepage by removing redundant sections and elevating
the flashcard creator to a standalone section.

Changes:
- Remove 'For Kids & Families' section entirely
- Remove 'Interactive Abacus' pane from Additional Tools
- Promote 'Flashcard Creator' to standalone top-level section

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:18:22 -05:00
semantic-release-bot
095cdda4ca chore(release): 4.49.0 [skip ci]
## [4.49.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.5...v4.49.0) (2025-10-20)

### Features

* add vibrant gradients and equal heights to game cards ([a1a135a](a1a135a858)), closes [#4](https://github.com/antialias/soroban-abacus-flashcards/issues/4) [#00f2](https://github.com/antialias/soroban-abacus-flashcards/issues/00f2) [#667](https://github.com/antialias/soroban-abacus-flashcards/issues/667) [#764ba2](https://github.com/antialias/soroban-abacus-flashcards/issues/764ba2) [#f093](https://github.com/antialias/soroban-abacus-flashcards/issues/f093) [#f5576](https://github.com/antialias/soroban-abacus-flashcards/issues/f5576) [#43e97](https://github.com/antialias/soroban-abacus-flashcards/issues/43e97) [#38f9d7](https://github.com/antialias/soroban-abacus-flashcards/issues/38f9d7)

### Code Refactoring

* make homepage game cards dynamic from game registry ([7f51652](7f516526fb))
2025-10-20 22:14:22 +00:00
Thomas Hallock
a1a135a858 feat: add vibrant gradients and equal heights to game cards
Updated the game theme system and game cards to have vibrant, eye-catching
gradients and consistent heights:

Theme System Changes:
- Updated all game themes from pastel to vibrant gradients
- Blue: Vibrant cyan (#4facfe to #00f2fe)
- Purple: Vibrant purple (#667eea to #764ba2)
- Pink: Vibrant pink (#f093fb to #f5576c)
- Green: Vibrant green/teal (#43e97b to #38f9d7)
- Plus updated indigo, teal, orange, yellow, red, gray themes

Game Manifest Updates:
- Memory Lightning: now uses purple theme
- Matching Pairs: now uses pink theme
- Complement Race: continues using blue (cyan) theme
- Card Sorting: now uses green theme

Homepage Game Cards:
- Added height: '100%' and flexbox to make all cards equal height
- Cards now stretch uniformly regardless of content length
- Maintains responsive hover effects and text readability overlays

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:13:09 -05:00
Thomas Hallock
7f516526fb refactor: make homepage game cards dynamic from game registry
Previously game cards were hardcoded. Now they automatically populate
from getAvailableGames() in the game registry. This means:
- Adding a new game to the registry automatically shows it on homepage
- Game metadata (icon, title, description, gradient, chips) comes from
  game manifests
- No manual homepage updates needed when games are added

Changes:
- Import getAvailableGames from game registry
- Replace hardcoded GameCard components with .map() over available games
- Format maxPlayers into player count string (e.g., "1-4 players")

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:08:55 -05:00
semantic-release-bot
9f706e9dce chore(release): 4.48.5 [skip ci]
## [4.48.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.4...v4.48.5) (2025-10-20)

### Bug Fixes

* **homepage:** add dark gradient overlay for better text contrast on game cards ([6410b21](6410b21f82))
2025-10-20 22:03:34 +00:00
Thomas Hallock
6410b21f82 fix(homepage): add dark gradient overlay for better text contrast on game cards
Added layered approach for better text readability:
- Vibrant gradient background layer (bottom)
- Dark gradient overlay from transparent (top 10%) to semi-dark (bottom 50%)
- Content with relative z-index positioning

This keeps the colorful gradients visible while ensuring all text
(titles, descriptions, tags) is clearly readable on all four cards.

Gradient goes from barely visible at top to moderately dark at bottom,
creating a natural fade that doesn't overwhelm the vibrant colors.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:02:24 -05:00
semantic-release-bot
3dc9f48d12 chore(release): 4.48.4 [skip ci]
## [4.48.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.3...v4.48.4) (2025-10-20)

### Bug Fixes

* **homepage:** improve text contrast on game cards with text shadows ([b6410c7](b6410c7c22))
2025-10-20 21:49:42 +00:00
Thomas Hallock
b6410c7c22 fix(homepage): improve text contrast on game cards with text shadows
Added text shadows to all game card text elements for better readability
on bright gradient backgrounds:
- Icon emoji: subtle shadow
- Title: strong shadow for maximum contrast
- Description: medium shadow (also increased opacity to 0.95)
- Player count: medium shadow (increased opacity to 0.85)
- Tags: medium shadow

This ensures all text is clearly readable on all four gradient backgrounds
(purple, pink, cyan, and green).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 16:48:35 -05:00
11 changed files with 685 additions and 267 deletions

View File

@@ -1,3 +1,65 @@
## [4.52.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.51.0...v4.52.0) (2025-10-20)
### Features
* **homepage:** add interactive draggable flashcards with physics ([e711c52](https://github.com/antialias/soroban-abacus-flashcards/commit/e711c527574412de2f9d451c7985c4f8667d269a))
## [4.51.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.1...v4.51.0) (2025-10-20)
### Features
* **homepage:** create fancy flashcard display with spread-out cards ([cd30944](https://github.com/antialias/soroban-abacus-flashcards/commit/cd30944c5e067f84d00dfdf41c37580acc589548))
## [4.50.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.0...v4.50.1) (2025-10-20)
### Bug Fixes
* **homepage:** set fixed width for learning panel to prevent layout shift ([dc19622](https://github.com/antialias/soroban-abacus-flashcards/commit/dc19622bbba2fead8cd9c0b2bda3a38abba0bd41))
* **homepage:** set fixed width for tutorial panel to prevent layout shift ([aba9f8a](https://github.com/antialias/soroban-abacus-flashcards/commit/aba9f8a94d50590cf94b6cd87f85b497e89045e7))
## [4.50.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.1...v4.50.0) (2025-10-20)
### Features
* **homepage:** add interactive learning panels with animated mini-tutorials ([76d6f19](https://github.com/antialias/soroban-abacus-flashcards/commit/76d6f19d51fe4b9594998ae4e0a8823aff389854))
## [4.49.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.0...v4.49.1) (2025-10-20)
### Code Refactoring
* **homepage:** streamline homepage sections ([d362a77](https://github.com/antialias/soroban-abacus-flashcards/commit/d362a770d63405efee5ef8a896d34e783dd11de2))
## [4.49.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.5...v4.49.0) (2025-10-20)
### Features
* add vibrant gradients and equal heights to game cards ([a1a135a](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a135a8586e314c9d695bec6c4e58ec24e5c9cb)), closes [#4](https://github.com/antialias/soroban-abacus-flashcards/issues/4) [#00f2](https://github.com/antialias/soroban-abacus-flashcards/issues/00f2) [#667](https://github.com/antialias/soroban-abacus-flashcards/issues/667) [#764ba2](https://github.com/antialias/soroban-abacus-flashcards/issues/764ba2) [#f093](https://github.com/antialias/soroban-abacus-flashcards/issues/f093) [#f5576](https://github.com/antialias/soroban-abacus-flashcards/issues/f5576) [#43e97](https://github.com/antialias/soroban-abacus-flashcards/issues/43e97) [#38f9d7](https://github.com/antialias/soroban-abacus-flashcards/issues/38f9d7)
### Code Refactoring
* make homepage game cards dynamic from game registry ([7f51652](https://github.com/antialias/soroban-abacus-flashcards/commit/7f516526fb5f5b60c1782db5c8c3e29f05caafa7))
## [4.48.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.4...v4.48.5) (2025-10-20)
### Bug Fixes
* **homepage:** add dark gradient overlay for better text contrast on game cards ([6410b21](https://github.com/antialias/soroban-abacus-flashcards/commit/6410b21f829810af27e42d188295630bd67d6b6b))
## [4.48.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.3...v4.48.4) (2025-10-20)
### Bug Fixes
* **homepage:** improve text contrast on game cards with text shadows ([b6410c7](https://github.com/antialias/soroban-abacus-flashcards/commit/b6410c7c225f01f42d095ca270b8da7903cbfbb0))
## [4.48.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.2...v4.48.3) (2025-10-20)

View File

@@ -103,7 +103,8 @@
"Bash(node -e:*)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')",
"Bash(git rev-parse HEAD)"
"Bash(git rev-parse HEAD)",
"Bash(gh run watch --exit-status 18662351595)"
],
"deny": [],
"ask": []

View File

@@ -53,6 +53,7 @@
"@tanstack/react-form": "^0.19.0",
"@tanstack/react-query": "^5.90.2",
"@types/jsdom": "^21.1.7",
"@use-gesture/react": "^10.3.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.4.1",
"drizzle-orm": "^0.44.6",

View File

@@ -8,24 +8,34 @@ import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
import { PageWithNav } from '@/components/PageWithNav'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
import { getAvailableGames } from '@/lib/arcade/game-registry'
import { InteractiveFlashcards } from '@/components/InteractiveFlashcards'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
import { token } from '../../styled-system/tokens'
// Mini abacus that cycles through random 3-digit numbers
function MiniAbacus() {
const [currentValue, setCurrentValue] = useState(123)
// Mini abacus that cycles through a sequence of values
function MiniAbacus({
values,
columns = 3,
interval = 2500,
}: {
values: number[]
columns?: number
interval?: number
}) {
const [currentIndex, setCurrentIndex] = useState(0)
const appConfig = useAbacusConfig()
useEffect(() => {
// Cycle through random 3-digit numbers every 2.5 seconds
const interval = setInterval(() => {
const randomNum = Math.floor(Math.random() * 1000) // 0-999
setCurrentValue(randomNum)
}, 2500)
if (values.length === 0) return
return () => clearInterval(interval)
}, [])
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % values.length)
}, interval)
return () => clearInterval(timer)
}, [values, interval])
// Dark theme styles for the abacus
const darkStyles = {
@@ -53,8 +63,8 @@ function MiniAbacus() {
>
<div className={css({ transform: 'scale(0.6)', transformOrigin: 'center center' })}>
<AbacusReact
value={currentValue}
columns={3}
value={values[currentIndex] || 0}
columns={columns}
beadShape={appConfig.beadShape}
customStyles={darkStyles}
/>
@@ -64,15 +74,46 @@ function MiniAbacus() {
}
export default function HomePage() {
// Extract just the "Friends of 5" step (2+3=5) for homepage demo
const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques"
const fullTutorial = getTutorialForEditor()
const friendsOf5Tutorial = {
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Learn the "Friends of 5" technique: adding 3 to make 5',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
}
// Create different tutorials for each skill level
const skillTutorials = [
// Skill 0: Read and set numbers (0-9999)
{
...fullTutorial,
id: 'read-numbers-demo',
title: 'Read and Set Numbers',
description: 'Master abacus number representation from zero to thousands',
steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
},
// Skill 1: Friends techniques (5 = 2+3)
{
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Add and subtract using complement pairs: 5 = 2+3',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
},
// Skill 2: Multiply & divide (12×34)
{
...fullTutorial,
id: 'multiply-demo',
title: 'Multiplication',
description: 'Fluent multi-digit calculations with advanced techniques',
steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
},
// Skill 3: Mental calculation (Speed math)
{
...fullTutorial,
id: 'mental-calc-demo',
title: 'Mental Calculation',
description: 'Visualize and compute without the physical tool (Anzan)',
steps: fullTutorial.steps.slice(-3),
},
]
const selectedTutorial = skillTutorials[selectedSkillIndex]
return (
<HomeHeroProvider>
@@ -123,9 +164,16 @@ export default function HomePage() {
})}
>
{/* Tutorial on the left */}
<div className={css({ flex: '1' })}>
<div
className={css({
flex: '1',
minW: { base: '100%', md: '500px' },
maxW: { base: '100%', md: '500px' },
})}
>
<TutorialPlayer
tutorial={friendsOf5Tutorial}
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
@@ -140,8 +188,7 @@ export default function HomePage() {
<div
className={css({
flex: '0 0 auto',
minW: '340px',
maxW: { base: '100%', md: '420px' },
w: { base: '100%', md: '420px' },
})}
>
<h3
@@ -157,122 +204,145 @@ export default function HomePage() {
<div className={stack({ gap: '5' })}>
{[
{
icon: '🔢',
title: 'Read and set numbers',
desc: 'Master abacus number representation from zero to thousands',
example: '0-9999',
badge: 'Foundation',
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3,
},
{
icon: '🤝',
title: 'Friends techniques',
desc: 'Add and subtract using complement pairs and mental shortcuts',
example: '5 = 2+3',
badge: 'Core',
values: [2, 5, 3],
columns: 1,
},
{
icon: '',
title: 'Multiply & divide',
desc: 'Fluent multi-digit calculations with advanced techniques',
example: '12×34',
badge: 'Advanced',
values: [12, 24, 36, 48],
columns: 2,
},
{
icon: '🧠',
title: 'Mental calculation',
desc: 'Visualize and compute without the physical tool (Anzan)',
example: 'Speed math',
badge: 'Expert',
values: [7, 14, 21, 28, 35],
columns: 2,
},
].map((skill, i) => (
<div
key={i}
className={css({
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
borderRadius: 'xl',
p: '4',
border: '1px solid',
borderColor: 'rgba(255, 255, 255, 0.15)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s',
_hover: {
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.4)',
},
})}
>
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
<div
className={css({
fontSize: '3xl',
width: '75px',
height: '115px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bg: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
{i === 0 ? <MiniAbacus /> : skill.icon}
</div>
<div className={stack({ gap: '2', flex: '1' })}>
<div className={hstack({ gap: '2', alignItems: 'center' })}>
<div
className={css({
color: 'white',
fontSize: 'md',
fontWeight: 'bold',
})}
>
{skill.title}
].map((skill, i) => {
const isSelected = i === selectedSkillIndex
return (
<div
key={i}
onClick={() => setSelectedSkillIndex(i)}
className={css({
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
borderRadius: 'xl',
p: '4',
border: '1px solid',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.4)'
: 'rgba(255, 255, 255, 0.15)',
boxShadow: isSelected
? '0 6px 16px rgba(250, 204, 21, 0.2)'
: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s',
cursor: 'pointer',
_hover: {
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.5)'
: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: isSelected
? '0 8px 20px rgba(250, 204, 21, 0.3)'
: '0 6px 16px rgba(0, 0, 0, 0.4)',
},
})}
>
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
<div
className={css({
fontSize: '3xl',
width: '75px',
height: '115px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bg: isSelected
? 'rgba(250, 204, 21, 0.15)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
<MiniAbacus values={skill.values} columns={skill.columns} />
</div>
<div className={stack({ gap: '2', flex: '1' })}>
<div className={hstack({ gap: '2', alignItems: 'center' })}>
<div
className={css({
color: 'white',
fontSize: 'md',
fontWeight: 'bold',
})}
>
{skill.title}
</div>
<div
className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400',
fontSize: '2xs',
fontWeight: 'semibold',
px: '2',
py: '0.5',
borderRadius: 'md',
})}
>
{skill.badge}
</div>
</div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div>
<div
className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400',
fontSize: '2xs',
fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2',
py: '0.5',
py: '1',
borderRadius: 'md',
w: 'fit-content',
})}
>
{skill.badge}
{skill.example}
</div>
</div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div>
<div
className={css({
color: 'yellow.400',
fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2',
py: '1',
borderRadius: 'md',
w: 'fit-content',
})}
>
{skill.example}
</div>
</div>
</div>
</div>
))}
)
})}
</div>
</div>
</div>
@@ -298,85 +368,24 @@ export default function HomePage() {
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
<GameCard
icon="🧠"
title="Memory Lightning"
description="Memorize soroban numbers"
players="1-8 players"
tags={['Memory', 'Pattern Recognition']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Match complement numbers"
players="1-4 players"
tags={['Friends of 5', 'Friends of 10']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<GameCard
icon="🏁"
title="Complement Race"
description="Race against time"
players="1-4 players"
tags={['Speed', 'Practice', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange numbers visually"
players="Solo challenge"
tags={['Visual Literacy', 'Ordering']}
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
href="/games"
/>
</div>
</section>
{/* For Kids & Families Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
For Kids & Families
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Simple enough for kids to start on their own, structured enough for parents to
trust
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🧒"
title="Self-Directed for Children"
features={[
'Big, obvious buttons and clear instructions',
'Progress at your own pace',
'Works with or without a physical abacus',
]}
accentColor="purple"
/>
<FeaturePanel
icon="👨‍👩‍👧"
title="Trusted by Parents"
features={[
'Structured curriculum following Japanese methods',
'Traditional kyu/dan progression levels',
'Track progress and celebrate achievements',
]}
accentColor="blue"
/>
{getAvailableGames().map((game) => {
const playersText =
game.manifest.maxPlayers === 1
? 'Solo challenge'
: `1-${game.manifest.maxPlayers} players`
return (
<GameCard
key={game.manifest.name}
icon={game.manifest.icon}
title={game.manifest.displayName}
description={game.manifest.description}
players={playersText}
tags={game.manifest.chips}
gradient={game.manifest.gradient}
href="/games"
/>
)
})}
</div>
</section>
@@ -540,8 +549,8 @@ export default function HomePage() {
</Link>
</section>
{/* Additional Tools Section */}
<section className={stack({ gap: '6' })}>
{/* Flashcard Generator Section */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
@@ -551,36 +560,123 @@ export default function HomePage() {
mb: '2',
})}
>
Additional Tools
Create Custom Flashcards
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Design beautiful flashcards for learning and practice
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
features={[
'Multiple formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
accentColor="blue"
ctaText="Create Flashcards →"
ctaHref="/create"
/>
<FeaturePanel
icon="🧮"
title="Interactive Abacus"
features={[
'Practice anytime in your browser',
'Multiple color schemes and bead styles',
'Sound effects and animations',
]}
accentColor="purple"
ctaText="Try the Abacus →"
ctaHref="/guide"
/>
{/* Interactive Flashcards Display */}
<div
className={css({
maxW: '1200px',
mx: 'auto',
mb: '8',
})}
>
<InteractiveFlashcards />
</div>
{/* Features and CTA */}
<Link
href="/create"
className={css({
display: 'block',
transition: 'all 0.3s ease',
_hover: {
transform: 'translateY(-4px)',
},
})}
>
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: { base: '6', md: '8' },
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '1200px',
mx: 'auto',
transition: 'all 0.3s ease',
_hover: {
borderColor: 'blue.500',
shadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
{/* Features */}
<div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}>
{[
{
icon: '📄',
title: 'Multiple Formats',
desc: 'PDF, PNG, SVG, HTML',
},
{
icon: '🎨',
title: 'Customizable',
desc: 'Bead shapes, colors, layouts',
},
{
icon: '📐',
title: 'All Paper Sizes',
desc: 'A3, A4, A5, US Letter',
},
].map((feature, i) => (
<div
key={i}
className={css({
textAlign: 'center',
p: '4',
rounded: 'lg',
bg: 'rgba(255, 255, 255, 0.05)',
})}
>
<div className={css({ fontSize: '2xl', mb: '2' })}>{feature.icon}</div>
<div
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'white',
mb: '1',
})}
>
{feature.title}
</div>
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>
{feature.desc}
</div>
</div>
))}
</div>
{/* CTA Button */}
<div className={css({ textAlign: 'center' })}>
<div
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '2',
bg: 'blue.600',
color: 'white',
px: '6',
py: '3',
rounded: 'lg',
fontWeight: 'semibold',
transition: 'all 0.2s',
_hover: {
bg: 'blue.500',
},
})}
>
<span>Create Flashcards</span>
<span>→</span>
</div>
</div>
</div>
</Link>
</section>
</div>
</div>
@@ -607,48 +703,112 @@ function GameCard({
href: string
}) {
return (
<Link href={href}>
<Link href={href} style={{ height: '100%', display: 'block' }}>
<div
style={{ background: gradient }}
className={css({
rounded: 'xl',
p: '6',
shadow: 'lg',
transition: 'all 0.3s ease',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
height: '100%',
display: 'flex',
flexDirection: 'column',
_hover: {
transform: 'translateY(-6px) scale(1.02)',
shadow: '0 25px 50px rgba(0, 0, 0, 0.3)',
},
})}
>
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
{title}
</h3>
<p className={css({ fontSize: 'sm', color: 'rgba(255, 255, 255, 0.9)', mb: '2' })}>
{description}
</p>
<p className={css({ fontSize: 'xs', color: 'rgba(255, 255, 255, 0.7)', mb: '3' })}>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
})}
>
{tag}
</span>
))}
{/* Vibrant gradient background */}
<div
style={{ background: gradient }}
className={css({
position: 'absolute',
inset: 0,
zIndex: 0,
})}
/>
{/* Dark gradient overlay for text readability */}
<div
className={css({
position: 'absolute',
inset: 0,
background: 'linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.5) 100%)',
zIndex: 1,
})}
/>
{/* Content */}
<div
className={css({
position: 'relative',
zIndex: 2,
flex: 1,
display: 'flex',
flexDirection: 'column',
})}
>
<div
className={css({
fontSize: '3xl',
mb: '3',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
})}
>
{icon}
</div>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'white',
mb: '2',
textShadow: '0 2px 8px rgba(0, 0, 0, 0.5)',
})}
>
{title}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'rgba(255, 255, 255, 0.95)',
mb: '2',
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
})}
>
{description}
</p>
<p
className={css({
fontSize: 'xs',
color: 'rgba(255, 255, 255, 0.85)',
mb: '3',
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
})}
>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)',
})}
>
{tag}
</span>
))}
</div>
</div>
</div>
</Link>

View File

@@ -24,7 +24,7 @@ const manifest: GameManifest = {
maxPlayers: 1, // Single player only
difficulty: 'Intermediate',
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
...getGameTheme('teal'),
...getGameTheme('green'),
available: true,
}

View File

@@ -23,7 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
...getGameTheme('purple'),
...getGameTheme('pink'),
available: true,
}

View File

@@ -23,7 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 8,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
...getGameTheme('blue'),
...getGameTheme('purple'),
available: true,
}

View File

@@ -0,0 +1,191 @@
'use client'
import { AbacusReact } from '@soroban/abacus-react'
import { useDrag } from '@use-gesture/react'
import { useEffect, useState } from 'react'
import { animated, config, useSpring } from '@react-spring/web'
import { css } from '../../styled-system/css'
interface Flashcard {
id: number
number: number
initialX: number
initialY: number
initialRotation: number
zIndex: number
}
const CONTAINER_WIDTH = 800
const CONTAINER_HEIGHT = 500
/**
* InteractiveFlashcards - A fun, physics-based flashcard display
* Users can drag and throw flashcards around with realistic momentum
*/
export function InteractiveFlashcards() {
// Generate 8-15 random flashcards (client-side only to avoid hydration errors)
const [cards, setCards] = useState<Flashcard[]>([])
useEffect(() => {
const count = Math.floor(Math.random() * 8) + 8 // 8-15 cards
const generated: Flashcard[] = []
for (let i = 0; i < count; i++) {
generated.push({
id: i,
number: Math.floor(Math.random() * 900) + 100, // 100-999
initialX: Math.random() * (CONTAINER_WIDTH - 200) + 100,
initialY: Math.random() * (CONTAINER_HEIGHT - 200) + 100,
initialRotation: Math.random() * 40 - 20, // -20 to 20 degrees
zIndex: i,
})
}
setCards(generated)
}, [])
return (
<div
className={css({
position: 'relative',
width: '100%',
height: { base: '400px', md: '500px' },
overflow: 'hidden',
bg: 'rgba(0, 0, 0, 0.3)',
rounded: 'xl',
border: '1px solid rgba(255, 255, 255, 0.1)',
})}
>
{/* Hint text */}
<div
className={css({
position: 'absolute',
top: '4',
left: '50%',
transform: 'translateX(-50%)',
color: 'white',
opacity: '0.6',
fontSize: 'sm',
fontWeight: 'medium',
zIndex: 1000,
pointerEvents: 'none',
})}
>
Drag and throw the flashcards!
</div>
{cards.map((card) => (
<DraggableCard key={card.id} card={card} />
))}
</div>
)
}
interface DraggableCardProps {
card: Flashcard
}
function DraggableCard({ card }: DraggableCardProps) {
const [{ x, y, rotation, scale }, api] = useSpring(() => ({
x: card.initialX,
y: card.initialY,
rotation: card.initialRotation,
scale: 1,
config: config.wobbly,
}))
const [zIndex, setZIndex] = useState(card.zIndex)
const bind = useDrag(
({ down, movement: [mx, my], velocity: [vx, vy], direction: [dx, dy] }) => {
// Bring card to front when dragging
if (down) {
setZIndex(1000)
}
api.start({
x: down ? card.initialX + mx : card.initialX + mx + vx * 100 * dx,
y: down ? card.initialY + my : card.initialY + my + vy * 100 * dy,
scale: down ? 1.1 : 1,
rotation: down ? card.initialRotation + mx / 20 : card.initialRotation + vx * 10,
immediate: down,
config: down ? config.stiff : config.wobbly,
})
// Update initial position after release for next drag
if (!down) {
card.initialX = card.initialX + mx + vx * 100 * dx
card.initialY = card.initialY + my + vy * 100 * dy
card.initialRotation = card.initialRotation + vx * 10
}
},
{
// Prevent scrolling when dragging
preventDefault: true,
filterTaps: true,
}
)
return (
<animated.div
{...bind()}
style={{
x,
y,
rotation,
scale,
zIndex,
position: 'absolute',
touchAction: 'none',
cursor: 'grab',
}}
className={css({
userSelect: 'none',
_active: {
cursor: 'grabbing',
},
})}
>
<div
className={css({
bg: 'white',
rounded: 'lg',
p: '4',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
minW: '120px',
border: '2px solid rgba(0, 0, 0, 0.1)',
transition: 'box-shadow 0.2s',
_hover: {
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.4)',
},
})}
>
{/* Abacus visualization */}
<div
className={css({
transform: 'scale(0.6)',
transformOrigin: 'center',
})}
>
<AbacusReact value={card.number} columns={3} beadShape="circle" />
</div>
{/* Number display */}
<div
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.800',
fontFamily: 'mono',
})}
>
{card.number}
</div>
</div>
</animated.div>
)
}

View File

@@ -14,59 +14,59 @@ export interface GameTheme {
}
/**
* Standard theme presets
* These use Panda CSS's color system and provide consistent styling
* Standard theme presets with vibrant gradients
* Updated for eye-catching game cards on the homepage
*/
export const GAME_THEMES = {
blue: {
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
borderColor: '#bfdbfe', // blue.200
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Vibrant cyan
borderColor: '#00f2fe',
},
purple: {
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
borderColor: '#ddd6fe', // purple.200
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Vibrant purple
borderColor: '#764ba2',
},
green: {
color: 'green',
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
borderColor: '#a7f3d0', // green.200
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // Vibrant green/teal
borderColor: '#38f9d7',
},
teal: {
color: 'teal',
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
borderColor: '#99f6e4', // teal.200
gradient: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', // Vibrant teal
borderColor: '#38ef7d',
},
indigo: {
color: 'indigo',
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
borderColor: '#c7d2fe', // indigo.200
gradient: 'linear-gradient(135deg, #5f72bd 0%, #9b23ea 100%)', // Vibrant indigo
borderColor: '#9b23ea',
},
pink: {
color: 'pink',
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
borderColor: '#fbcfe8', // pink.200
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', // Vibrant pink
borderColor: '#f5576c',
},
orange: {
color: 'orange',
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
borderColor: '#fed7aa', // orange.200
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // Vibrant orange/coral
borderColor: '#fee140',
},
yellow: {
color: 'yellow',
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
borderColor: '#fde68a', // yellow.200
gradient: 'linear-gradient(135deg, #ffd89b 0%, #19547b 100%)', // Vibrant yellow/blue
borderColor: '#ffd89b',
},
red: {
color: 'red',
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
borderColor: '#fecaca', // red.200
gradient: 'linear-gradient(135deg, #f85032 0%, #e73827 100%)', // Vibrant red
borderColor: '#e73827',
},
gray: {
color: 'gray',
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
borderColor: '#e5e7eb', // gray.200
gradient: 'linear-gradient(135deg, #868f96 0%, #596164 100%)', // Vibrant gray
borderColor: '#596164',
},
} as const satisfies Record<string, GameTheme>

View File

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

3
pnpm-lock.yaml generated
View File

@@ -134,6 +134,9 @@ importers:
'@types/jsdom':
specifier: ^21.1.7
version: 21.1.7
'@use-gesture/react':
specifier: ^10.3.1
version: 10.3.1(react@18.3.1)
bcryptjs:
specifier: ^2.4.3
version: 2.4.3