Compare commits

...

50 Commits

Author SHA1 Message Date
semantic-release-bot
593aed81cc chore(release): 4.13.14 [skip ci]
## [4.13.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.13...v4.13.14) (2025-10-19)

### Bug Fixes

* **docker:** install py3-pip for Python dependency installation ([0f55909](0f55909533))

### Documentation

* add Panda CSS styling framework documentation ([c92d7d9](c92d7d9d89))
* **arcade:** fix incorrect Tailwind references - use Panda CSS ([34a377d](34a377d91b))
2025-10-19 00:30:55 +00:00
Thomas Hallock
0f55909533 fix(docker): install py3-pip for Python dependency installation
The pip3 command was not available, causing the Docker build to fail when
trying to install Python dependencies from requirements.txt. Added py3-pip
to the Alpine package installation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:29:42 -05:00
Thomas Hallock
c92d7d9d89 docs: add Panda CSS styling framework documentation
Added critical section to .claude/CLAUDE.md documenting that this
project uses Panda CSS, NOT Tailwind CSS.

This prevents future confusion and incorrect references to Tailwind
in code, comments, or documentation.

Includes:
- Framework identification and configuration locations
- Import patterns and token syntax
- Common mistakes to avoid with examples
- Reference to GAME_THEMES.md for arcade styling

This mistake was made earlier in this session when documenting the
game theme system, so documenting it now to prevent recurrence.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:28:38 -05:00
Thomas Hallock
34a377d91b docs(arcade): fix incorrect Tailwind references - use Panda CSS
Corrected documentation and comments that incorrectly referenced
Tailwind CSS. This project uses Panda CSS for styling.

Fixed in:
- `/src/lib/arcade/game-themes.ts` - Updated all comments
- `.claude/GAME_THEMES.md` - Fixed documentation references

The color system uses Panda CSS's preset colors (blue.100, blue.200, etc.)
not Tailwind. The hex values are the same, but we should be accurate
about which framework we're using.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:03:25 -05:00
semantic-release-bot
3dcdfb4986 chore(release): 4.13.13 [skip ci]
## [4.13.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.12...v4.13.13) (2025-10-19)

### Bug Fixes

* **docker:** install Python dependencies for flashcard generation ([c9b7e92](c9b7e92f39))

### Code Refactoring

* **arcade:** standardize game card themes with preset system ([0209975](0209975af6)), closes [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4) [#5eead4](https://github.com/antialias/soroban-abacus-flashcards/issues/5eead4)
2025-10-19 00:01:40 +00:00
Thomas Hallock
0209975af6 refactor(arcade): standardize game card themes with preset system
Create a centralized theme system to prevent inconsistent game card
styling. All games now use standard color presets instead of manually
specifying gradients.

## Changes

**New Theme System:**
- Created `/src/lib/arcade/game-themes.ts` with 10 standard themes
- All themes use Tailwind 100-200 colors for consistent pastel appearance
- Exported `getGameTheme()` helper and `GAME_THEMES` constants via SDK
- Added comprehensive documentation in `.claude/GAME_THEMES.md`

**Migrated All Games:**
- card-sorting: Uses `getGameTheme('teal')`
- memory-quiz: Uses `getGameTheme('blue')`
- matching: Uses `getGameTheme('purple')`
- complement-race: Uses `getGameTheme('blue')`

**Benefits:**
-  Prevents future styling inconsistencies
-  One-line theme setup instead of three properties
-  TypeScript autocomplete for available themes
-  Centralized maintenance - update all games by changing theme definition
-  Clear documentation prevents mistakes

**Before:**
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)',  // Manual, error-prone
borderColor: 'teal.200',
```

**After:**
```typescript
...getGameTheme('teal')  // Simple, consistent, discoverable
```

This fixes the root cause where card-sorting needed manual gradient
adjustments - now all games automatically get professional styling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:00:38 -05:00
Thomas Hallock
c9b7e92f39 fix(docker): install Python dependencies for flashcard generation
The Python scripts in packages/core require pyyaml, pillow, imagehash, and
other dependencies to generate flashcards. Install these from requirements.txt
during Docker build.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 19:00:25 -05:00
semantic-release-bot
c56a47cb60 chore(release): 4.13.12 [skip ci]
## [4.13.12](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.11...v4.13.12) (2025-10-18)

### Bug Fixes

* **card-sorting:** use blue gradient matching other game cards ([bdb84f5](bdb84f5d90))
2025-10-18 23:47:18 +00:00
Thomas Hallock
bdb84f5d90 fix(card-sorting): use blue gradient matching other game cards
Change card sorting game to use the same blue gradient as Memory Lightning
and Speed Complement Race to ensure consistent appearance on game chooser.

Updated to: linear-gradient(135deg, #dbeafe, #bfdbfe) (blue-100 to blue-200)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:46:06 -05:00
semantic-release-bot
33838b7fa7 chore(release): 4.13.11 [skip ci]
## [4.13.11](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.10...v4.13.11) (2025-10-18)

### Bug Fixes

* **card-sorting:** match game selector background to other games ([db62519](db62519f9b)), closes [#ccfbf1](https://github.com/antialias/soroban-abacus-flashcards/issues/ccfbf1) [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4)
* **docker:** copy core package with Python scripts to production image ([33e9ad2](33e9ad2f79))
2025-10-18 23:43:33 +00:00
Thomas Hallock
33e9ad2f79 fix(docker): copy core package with Python scripts to production image
The flashcard generator was failing in production because the packages/core
directory (which contains the Python scripts for PDF generation) wasn't being
copied to the production Docker image. The SorobanGenerator class needs these
scripts at runtime to generate flashcards.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:42:24 -05:00
Thomas Hallock
db62519f9b fix(card-sorting): match game selector background to other games
Change card sorting game gradient from bright teal (teal-200 to teal-300)
to softer pastel teal (teal-100 to teal-200) to match the lighter gradient
style used by other arcade games (Memory Lightning, Matching Pairs).

Updated gradient: linear-gradient(135deg, #ccfbf1, #99f6e4)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:42:20 -05:00
semantic-release-bot
ec978de0b3 chore(release): 4.13.10 [skip ci]
## [4.13.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.9...v4.13.10) (2025-10-18)

### Bug Fixes

* add Typst to Docker image for flashcard generation ([d9a7694](d9a7694031))

### Code Refactoring

* remove 'Complete Soroban Learning Platform' section ([42dcbff](42dcbff857))
2025-10-18 23:27:21 +00:00
Thomas Hallock
d9a7694031 fix: add Typst to Docker image for flashcard generation
The flashcard generator requires both Python and Typst to work.
Added typst to the runtime dependencies in the Dockerfile.

This fixes the issue where flashcards work in dev but fail in production.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:26:07 -05:00
Thomas Hallock
42dcbff857 refactor: remove 'Complete Soroban Learning Platform' section
Removed stats banner section as platform is still in development.
Also removed unused StatItem component.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:14:43 -05:00
semantic-release-bot
5923d341a0 chore(release): 4.13.9 [skip ci]
## [4.13.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.8...v4.13.9) (2025-10-18)

### Bug Fixes

* set color on abacus container div for numeral visibility ([cd47960](cd4796024e)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
2025-10-18 23:12:51 +00:00
Thomas Hallock
cd4796024e fix: set color on abacus container div for numeral visibility
Removed customStyles approach that wasn't working.
Added color: #1f2937 to container div - numerals inherit from parent.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:11:43 -05:00
semantic-release-bot
cff948708f chore(release): 4.13.8 [skip ci]
## [4.13.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.7...v4.13.8) (2025-10-18)

### Bug Fixes

* use color instead of fill for numeral styling ([ea10c16](ea10c16811))
2025-10-18 23:10:18 +00:00
Thomas Hallock
ea10c16811 fix: use color instead of fill for numeral styling
SVG foreign objects use CSS color property, not fill.
Changed numerals customStyle from fill to color for dark gray text.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:09:12 -05:00
semantic-release-bot
474d31576f chore(release): 4.13.7 [skip ci]
## [4.13.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.6...v4.13.7) (2025-10-18)

### Bug Fixes

* add dark color for abacus numerals ([73ff32c](73ff32c243)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
* use app-wide abacus config and remove instruction text ([0a50c73](0a50c733b0))
2025-10-18 23:07:53 +00:00
Thomas Hallock
73ff32c243 fix: add dark color for abacus numerals
Added customStyles to make numerals dark gray (#1f2937) with bold weight
for good contrast on white background.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:06:47 -05:00
Thomas Hallock
0a50c733b0 fix: use app-wide abacus config and remove instruction text
- Added useAbacusConfig() hook to get app-wide settings
- AbacusReact now respects beadShape, colorScheme, hideInactiveBeads from app config
- Removed "Click the beads to interact!" instruction text
- Simplified pane layout (just centered)

Now matches the styling/settings used throughout the rest of the app.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:06:47 -05:00
semantic-release-bot
1386378ca1 chore(release): 4.13.6 [skip ci]
## [4.13.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.5...v4.13.6) (2025-10-18)

### Bug Fixes

* simplify abacus pane with light background ([30f48ab](30f48ab897))
2025-10-18 23:05:58 +00:00
Thomas Hallock
30f48ab897 fix: simplify abacus pane with light background
- Changed background from dark to white for better contrast
- Removed duplicate number display (abacus has built-in numerals)
- Removed custom styles (defaults work well on light background)
- Updated instruction text color to gray.700 for readability

Abacus component isn't designed for dark mode yet, so light pane is simpler.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:04:49 -05:00
semantic-release-bot
d2f6b8b46c chore(release): 4.13.5 [skip ci]
## [4.13.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.4...v4.13.5) (2025-10-18)

### Bug Fixes

* correct AbacusReact API usage and add structural styling ([247377f](247377fca3)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78)
2025-10-18 23:01:22 +00:00
Thomas Hallock
247377fca3 fix: correct AbacusReact API usage and add structural styling
Fixed critical issues based on storybook examples:
- Changed callback syntax: onValueChange prop directly (not in callbacks object)
- Added reckoningBar styling: golden stroke (#fbbf24, 4px width)
- Added columnPosts styling: purple stroke (#a78bfa, 3px width)
- Now structural elements are visible on dark background and interactive works

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 18:00:23 -05:00
semantic-release-bot
be39401716 chore(release): 4.13.4 [skip ci]
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](d2a3b7ae2e))
2025-10-18 22:56:13 +00:00
Thomas Hallock
d2a3b7ae2e fix(card-sorting): increase card tile sizes to contain abacuses
Make card tiles larger to properly contain AbacusReact SVGs without overflow.

**Issue:**
Card tiles were too small (90px × 90px and 90px × 110px) to contain the
AbacusReact SVGs, causing abacuses to overflow their containers.

**Fix:**
- **Available cards**: Increased from 90px × 90px to 140px × 140px
- **Position slots**: Increased from 90px × 110px to 140px × 160px

**Result:**
Abacus SVGs now fit comfortably within their card containers without
overflowing, while maintaining the overflow: hidden constraint as a
safety measure.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:283-284,419-420

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:55:15 -05:00
semantic-release-bot
39ab605279 chore(release): 4.13.3 [skip ci]
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase SVG size to fill card containers ([cf9d893](cf9d893f3f))
2025-10-18 22:53:10 +00:00
Thomas Hallock
cf9d893f3f fix(card-sorting): increase SVG size to fill card containers
Make AbacusReact SVGs larger by allowing them to fill their containers.

**Issue:**
SVG containers were set to fixed small sizes (74px × 74px and 70px × 70px),
making the abacuses appear too small within their cards.

**Fix:**
- **Available cards**: Changed SVG container from 74px × 74px to 100% × 100%
  to fill the entire 90px card (accounting for 8px padding)
- **Position slots**: Changed SVG container from 70px × 70px to flex: 1,
  width: 100% to fill available space while leaving room for label

**Result:**
Abacuses now appear larger and fill their card containers better while
still being constrained by overflow: hidden to prevent breaking out.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-329,456-473

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:51:57 -05:00
semantic-release-bot
e6d0bd4953 chore(release): 4.13.2 [skip ci]
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)

### Bug Fixes

* show initial value and improve numeral contrast ([1b57f6d](1b57f6ddec)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
2025-10-18 22:51:19 +00:00
Thomas Hallock
1b57f6ddec fix: show initial value and improve numeral contrast
- Changed back to controlled value (with state update) to display initial 1234567
- Added customStyles for numerals: golden color (#fbbf24), bold, larger font
- Numerals now have excellent contrast against dark background

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:50:12 -05:00
semantic-release-bot
d38ea312a7 chore(release): 4.13.1 [skip ci]
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)

### Bug Fixes

* use defaultValue for interactive abacus control ([06aca98](06aca986ac))
2025-10-18 22:47:40 +00:00
Thomas Hallock
06aca986ac fix: use defaultValue for interactive abacus control
Changed from controlled (value) to uncontrolled (defaultValue) pattern
to allow AbacusReact to fully manage its own state and be interactive.
Still tracks value changes via onValueChange callback for display.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:46:32 -05:00
semantic-release-bot
a126466037 chore(release): 4.13.0 [skip ci]
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)

### Features

* make home page abacus interactive with audio ([9a53d7e](9a53d7e5db))
2025-10-18 22:45:39 +00:00
Thomas Hallock
9a53d7e5db feat: make home page abacus interactive with audio
Enhanced the hero abacus to be a fully interactive showcase:
- Increased size from default to 2.2x scale factor
- Enabled interactive mode (click beads to change value)
- Enabled smooth animations for bead movements
- Enabled audio feedback with volume at 0.4
- Added live value display below abacus in large golden text
- Added instructional text "Click the beads to interact!"
- Shows place value numbers on each column

The abacus now serves as a "try it now" demo right on the landing page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:44:40 -05:00
semantic-release-bot
d2d8f7740f chore(release): 4.12.0 [skip ci]
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)

### Features

* redesign home page with component showcase ([29af265](29af265958))
2025-10-18 22:44:06 +00:00
Thomas Hallock
29af265958 feat: redesign home page with component showcase
Replaced outdated landing page with component-based "storybook" design:
- Large featured AbacusReact component (1,234,567 with place-value colors)
- Color scheme showcase with 4 actual AbacusReact instances
- Dark, fancy theme with purple gradients and golden accents
- All game links now point to /games instead of /arcade
- Hover animations and visual polish throughout
- Dot pattern background texture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:42:55 -05:00
semantic-release-bot
291bcc581d chore(release): 4.11.1 [skip ci]
## [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](26edec1bbf))
2025-10-18 22:41:08 +00:00
Thomas Hallock
26edec1bbf fix(card-sorting): center AbacusReact SVGs in card tiles
Improve centering of abacus SVGs within both available cards and position slots.

**Issue:**
AbacusReact SVGs were not properly centered within their card containers,
appearing off-center or misaligned.

**Fix:**
- **Available cards**: Added maxHeight: '100%' constraint and display: 'block'
  with margin: '0 auto' to SVG styling
- **Position slots**: Changed container to use flex: 1 and proper flex centering,
  constrained SVG to maxWidth: '70px' with centering styles

**Changes:**
- Available card SVG container: Added display flex with center alignment
- Available card SVG: maxHeight: '100%', display: 'block', margin: '0 auto'
- Position slot SVG container: width: '100%', flex: 1, flex centering
- Position slot SVG: maxWidth: '70px', maxHeight: '100%', display: 'block', margin: '0 auto'

Now AbacusReact SVGs render centered within their card tiles regardless of
the actual SVG dimensions.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-330,457-475

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:40:03 -05:00
semantic-release-bot
da4fdc90e0 chore(release): 4.11.0 [skip ci]
## [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](ee6c4f2f4f))
2025-10-18 22:34:07 +00:00
Thomas Hallock
ee6c4f2f4f feat(home): redesign home page to showcase complete platform
Replace outdated "flashcard generator" landing page with comprehensive
platform showcase highlighting all three pillars: arcade games,
interactive learning, and flashcard creation.

**New Home Page Structure:**
- Compact hero with 3 CTAs: Play Games, Learn, Create
- 4 arcade game cards with player counts and mode tags
- Two-column feature sections for Learning & Flashcards
- Multiplayer features grid (4 cards)
- Stats banner: 4 games, 8 max players, 3 learning modes, 4+ formats

**Visual Design:**
- Smaller, denser components to fit more content
- Information-rich showcase vs marketing fluff
- Purple gradient hero matching guide branding
- Responsive grid layouts for all screen sizes

**Result:**
Home page now accurately represents the full platform:
multiplayer arcade games + interactive tutorials + flashcard tools.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:33:01 -05:00
semantic-release-bot
9b9f0cdbcb chore(release): 4.10.6 [skip ci]
## [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](e14ffe44d6))
2025-10-18 22:24:03 +00:00
Thomas Hallock
e14ffe44d6 fix(card-sorting): position slots flow horizontally with wrap
Fix position slots to match Python original - they should flow horizontally
and wrap, not stack vertically.

**Before:**
- Container: display: flex, flexDirection: 'column' (vertical stack)
- Each slot + insert button pair wrapped in a div
- One slot per line with insert button below it

**After (matching Python lines 3101-3111):**
- Container: display: flex, flexWrap: 'wrap' (horizontal flow)
- Slots and insert buttons flow together as siblings
- Wraps naturally based on container width
- Dashed border container with semi-transparent background

**Changes:**
- Position slots container: flex-wrap instead of flex-direction: column
- Removed wrapper div around slot+button pairs
- Added React keys to slots and buttons
- Added container styling (padding, background, border)

Now matches the Python original where all slots and + buttons flow
together horizontally and wrap as needed.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:362-524

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:22:56 -05:00
semantic-release-bot
d5bc0bb27c chore(release): 4.10.5 [skip ci]
## [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](0790074ffc))
2025-10-18 22:11:17 +00:00
Thomas Hallock
0790074ffc refactor(arcade): merge /arcade/room into /arcade route
Simplify arcade routing by moving room page to main arcade route.
The /arcade route now handles both game selection and gameplay,
eliminating the need for a separate /arcade/room route.

**Changes:**
- Move /arcade/room/page.tsx → /arcade/page.tsx
- Update import paths for styled-system (room/ → arcade/)
- Remove legacy GAMES_CONFIG handling (now registry-only)
- Delete obsolete EnhancedChampionArena component

**Result:**
- Users navigate directly to /arcade for all arcade features
- Game selection UI shows when no game selected in room
- Selected game renders when room has gameName set
- Simpler, more intuitive URL structure

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:10:10 -05:00
semantic-release-bot
1a44daf2ce chore(release): 4.10.4 [skip ci]
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** match Python card layout with flex wrap ([9679d68](9679d68154))
2025-10-18 21:35:53 +00:00
Thomas Hallock
9679d68154 fix(card-sorting): match Python card layout with flex wrap
Fix available cards layout to match the Python original implementation.

**Before:**
- Grid layout with responsive columns (1/2/3 cols)
- Cards stretched to fill grid cells
- Each card on its own line on mobile

**After (matching Python web_generator.py lines 3077-3087):**
- Flex container with wrap
- Fixed 90px x 90px card size
- Cards flow naturally and wrap based on container width
- Dashed border container with semi-transparent background
- Revealed numbers positioned absolutely in top-right corner
- Matching hover/selection animations from original

**Visual Changes:**
- Container: flex wrap, centered, dashed border, rgba background
- Cards: 90px x 90px fixed size
- Revealed numbers: absolute positioned badge (top-right)
- Selection state: scale(1.1), blue border, blue background
- Hover: translateY(-5px) lift effect

src/arcade-games/card-sorting/components/PlayingPhase.tsx:265-348

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:34:50 -05:00
semantic-release-bot
80ba94203d chore(release): 4.10.3 [skip ci]
## [4.10.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.2...v4.10.3) (2025-10-18)

### Bug Fixes

* **arcade:** remove broken query param from game URLs ([87631af](87631af678))
2025-10-18 21:15:27 +00:00
Thomas Hallock
87631af678 fix(arcade): remove broken query param from game URLs
Remove ?game= query parameter from GameSelector URLs. The /arcade/room
page doesn't use query params - it uses the room's gameName field from
the database and handles game selection through its own UI.

This was causing the URL to have a stale ?game= param that didn't match
the actual selected game, breaking testing and navigation.

**Changed:**
- GameSelector.tsx: url changed from `/arcade/room?game=${name}` to `/arcade/room`

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:14:14 -05:00
25 changed files with 1665 additions and 629 deletions

View File

@@ -1,3 +1,175 @@
## [4.13.14](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.13...v4.13.14) (2025-10-19)
### Bug Fixes
* **docker:** install py3-pip for Python dependency installation ([0f55909](https://github.com/antialias/soroban-abacus-flashcards/commit/0f55909533414bdc07f113b93bb8bfa21367959b))
### Documentation
* add Panda CSS styling framework documentation ([c92d7d9](https://github.com/antialias/soroban-abacus-flashcards/commit/c92d7d9d89a72e012c30fc5ac88fa96e7a526f83))
* **arcade:** fix incorrect Tailwind references - use Panda CSS ([34a377d](https://github.com/antialias/soroban-abacus-flashcards/commit/34a377d91b37ad47968b85aedd112f9fcf72ad63))
## [4.13.13](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.12...v4.13.13) (2025-10-19)
### Bug Fixes
* **docker:** install Python dependencies for flashcard generation ([c9b7e92](https://github.com/antialias/soroban-abacus-flashcards/commit/c9b7e92f39ee7aa7f13606c2836763144df102e7))
### Code Refactoring
* **arcade:** standardize game card themes with preset system ([0209975](https://github.com/antialias/soroban-abacus-flashcards/commit/0209975af642944cc5a434c0b44205a87e634e7e)), closes [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4) [#5eead4](https://github.com/antialias/soroban-abacus-flashcards/issues/5eead4)
## [4.13.12](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.11...v4.13.12) (2025-10-18)
### Bug Fixes
* **card-sorting:** use blue gradient matching other game cards ([bdb84f5](https://github.com/antialias/soroban-abacus-flashcards/commit/bdb84f5d909542060fa886a83a5af62c4a785a98))
## [4.13.11](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.10...v4.13.11) (2025-10-18)
### Bug Fixes
* **card-sorting:** match game selector background to other games ([db62519](https://github.com/antialias/soroban-abacus-flashcards/commit/db62519f9beb0b4bc6120e1fd5ec251cfde5c3c1)), closes [#ccfbf1](https://github.com/antialias/soroban-abacus-flashcards/issues/ccfbf1) [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4)
* **docker:** copy core package with Python scripts to production image ([33e9ad2](https://github.com/antialias/soroban-abacus-flashcards/commit/33e9ad2f79b591f1c5ee57a6691e1bcf48420859))
## [4.13.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.9...v4.13.10) (2025-10-18)
### Bug Fixes
* add Typst to Docker image for flashcard generation ([d9a7694](https://github.com/antialias/soroban-abacus-flashcards/commit/d9a769403187bf70fb069be7ffe77417a62271a5))
### Code Refactoring
* remove 'Complete Soroban Learning Platform' section ([42dcbff](https://github.com/antialias/soroban-abacus-flashcards/commit/42dcbff85708ad378550634cbf7a3345eccb578e))
## [4.13.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.8...v4.13.9) (2025-10-18)
### Bug Fixes
* set color on abacus container div for numeral visibility ([cd47960](https://github.com/antialias/soroban-abacus-flashcards/commit/cd4796024e41f731ae5d83c82f6582e19d6eaf99)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
## [4.13.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.7...v4.13.8) (2025-10-18)
### Bug Fixes
* use color instead of fill for numeral styling ([ea10c16](https://github.com/antialias/soroban-abacus-flashcards/commit/ea10c16811eb969b9963417079c330ae9ff295ba))
## [4.13.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.6...v4.13.7) (2025-10-18)
### Bug Fixes
* add dark color for abacus numerals ([73ff32c](https://github.com/antialias/soroban-abacus-flashcards/commit/73ff32c2432beb62710e57aa8b3b4793eca43fda)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
* use app-wide abacus config and remove instruction text ([0a50c73](https://github.com/antialias/soroban-abacus-flashcards/commit/0a50c733b089c7c341f0fdef47da78d1c61a3cb5))
## [4.13.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.5...v4.13.6) (2025-10-18)
### Bug Fixes
* simplify abacus pane with light background ([30f48ab](https://github.com/antialias/soroban-abacus-flashcards/commit/30f48ab8976976688e089b07ece7fdae6d7ada79))
## [4.13.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.4...v4.13.5) (2025-10-18)
### Bug Fixes
* correct AbacusReact API usage and add structural styling ([247377f](https://github.com/antialias/soroban-abacus-flashcards/commit/247377fca35ee3433e02ad594ecc1c4f391f0143)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78)
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](https://github.com/antialias/soroban-abacus-flashcards/commit/d2a3b7ae2e3f6819b8d9ace32be22f04f748d1bc))
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase SVG size to fill card containers ([cf9d893](https://github.com/antialias/soroban-abacus-flashcards/commit/cf9d893f3fdbef6e91cd0ba283d602b9215569f1))
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)
### Bug Fixes
* show initial value and improve numeral contrast ([1b57f6d](https://github.com/antialias/soroban-abacus-flashcards/commit/1b57f6ddecf3a118f2e4fadd1a91be1256f5a034)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)
### Bug Fixes
* use defaultValue for interactive abacus control ([06aca98](https://github.com/antialias/soroban-abacus-flashcards/commit/06aca986ace4d76b70f2fd2f5e57f66758185b38))
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)
### Features
* make home page abacus interactive with audio ([9a53d7e](https://github.com/antialias/soroban-abacus-flashcards/commit/9a53d7e5db18853aca4e2e0c7abc799217feaecf))
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)
### Features
* redesign home page with component showcase ([29af265](https://github.com/antialias/soroban-abacus-flashcards/commit/29af265958f9fdab0253b92e153c01575840454d))
## [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)
### Bug Fixes
* **card-sorting:** match Python card layout with flex wrap ([9679d68](https://github.com/antialias/soroban-abacus-flashcards/commit/9679d68154ac8b6a2f905ec7d17a34b39bc00237))
## [4.10.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.2...v4.10.3) (2025-10-18)
### Bug Fixes
* **arcade:** remove broken query param from game URLs ([87631af](https://github.com/antialias/soroban-abacus-flashcards/commit/87631af6788bd7b42e671374e55ec0ad8435900c))
## [4.10.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.1...v4.10.2) (2025-10-18)

View File

@@ -33,8 +33,8 @@ RUN turbo build --filter=@soroban/web
FROM node:18-alpine AS runner
WORKDIR /app
# Install Python and build tools for better-sqlite3 (needed at runtime)
RUN apk add --no-cache python3 py3-setuptools make g++
# Install Python, pip, build tools for better-sqlite3, and Typst (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
@@ -55,6 +55,12 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/drizzle ./apps/web/drizz
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
# Copy core package (needed for Python flashcard generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
# Install Python dependencies for flashcard generation
RUN pip3 install --no-cache-dir -r packages/core/requirements.txt
# Copy package.json files for module resolution
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/package.json ./apps/web/

View File

@@ -0,0 +1,440 @@
# Arcade Routing Architecture - Complete Overview
## 1. Current /arcade Page
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/app/arcade/page.tsx` (lines 1-129)
**Purpose:** The main arcade landing page - displays the "Champion Arena"
**Key Components:**
- `ArcadeContent()` - Renders the main arcade interface
- Uses `EnhancedChampionArena` component which contains `GameSelector`
- The `GameSelector` displays all available games as cards
- `GameSelector` includes both legacy games and registry games
**Current Flow:**
1. User navigates to `/arcade`
2. Page renders `FullscreenProvider` wrapper
3. Displays `PageWithNav` with title "🏟️ Champion Arena"
4. Content area shows `EnhancedChampionArena``GameSelector`
5. `GameSelector` renders `GameCard` components for each game
6. When user clicks a game card, `GameCard` calls `router.push(config.url)`
7. For registry games, `config.url` is `/arcade/room?game={gameName}`
8. For legacy games, URL would be direct to their page
**State Management:**
- `GameModeContext` provides player selection (emoji, name, color)
- `PageWithNav` wraps content and provides mini-nav with:
- Active player list
- Add player button
- Game mode indicator (single/battle/tournament)
- Exit session handler
## 2. Current /arcade/room Page
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/app/arcade/room/page.tsx` (lines 1-359)
**Purpose:** "Magical place" that shows either a game OR the game chooser, driven by room state
**Three States:**
### State 1: Loading
- Shows "Loading room..." message
- Waits for `useRoomData()` hook to resolve
### State 2: Game Selection UI (when `!roomData.gameName`)
- Shows large game selection buttons
- User clicks to select a game
- Calls `setRoomGame()` mutation to save selection to room
- Invokes `handleGameSelect()` which:
1. Checks if game exists in registry via `hasGame(gameType)`
2. If registry game: calls `setRoomGame({roomId, gameName: gameType})`
3. If legacy game: maps to internal name via `GAME_TYPE_TO_NAME`, then calls `setRoomGame()`
4. Game selection is persisted to the room database
### State 3: Game Display (when `roomData.gameName` is set)
- Checks game registry first via `hasGame(roomData.gameName)`
- If registry game:
- Gets game definition via `getGame(roomData.gameName)`
- Renders: `<Provider><GameComponent /></Provider>`
- Provider and GameComponent come from game registry definition
- If legacy game:
- Switch statement with TODO for individual games
- Currently only shows "Game not yet supported"
**Key Hook:**
- `useRoomData()` - Fetches current room from API and subscribes to socket updates
- Returns `roomData` with fields: `id`, `name`, `code`, `gameName`, `gameConfig`, `members`, `memberPlayers`
- Also returns `isLoading` boolean
**Navigation Flow:**
1. User navigates to `/arcade`
2. `GameCard` onClick calls `router.push('/arcade/room?game={gameName}')`
3. User arrives at `/arcade/room`
4. If NOT in a room yet: Shows error with link back to `/arcade`
5. If in room but no game selected: Shows game selection UI
6. If game selected: Loads and displays game
## 3. The "Mini App Nav" - GameContextNav Component
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/nav/GameContextNav.tsx` (lines 1-372)
**What It Is:**
The "mini app nav" is actually a sophisticated component within the `PageWithNav` wrapper that intelligently shows different UI based on context:
**Components & Props:**
- `navTitle` - Current page title (e.g., "Champion Arena", "Choose Game", "Speed Complement Race")
- `navEmoji` - Icon emoji for current page
- `gameMode` - Computed from active player count: 'none' | 'single' | 'battle' | 'tournament'
- `activePlayers` - Array of selected players
- `inactivePlayers` - Array of available but unselected players
- `shouldEmphasize` - Boolean to emphasize player selection
- `showFullscreenSelection` - Boolean to show fullscreen mode for player selection
- `roomInfo` - Optional arcade room data (roomId, roomName, gameName, playerCount, joinCode)
- `networkPlayers` - Remote players from room members
**Three Display Modes:**
### Mode 1: Fullscreen Player Selection
- When `showFullscreenSelection === true`
- Displays:
- Large title with emoji
- Game mode indicator
- Fullscreen player selection UI
- Shows all inactive players for selection
### Mode 2: Solo Mode (NOT in room)
- When `roomInfo` is undefined
- Shows:
- **Game Title Section** (left side):
- `GameTitleMenu` with game title and emoji
- Menu options: Setup, New Game, Quit
- `GameModeIndicator`
- **Player Section** (right side):
- `ActivePlayersList` - shows selected players
- `AddPlayerButton` - add more players
### Mode 3: Room Mode (IN a room)
- When `roomInfo` is defined
- Shows:
- **Hidden:** Game title section (display: none)
- **Room Info Pane** (left side):
- `RoomInfo` component with room details
- Game mode indicator with color/emoji
- Room name, player count, join code
- `NetworkPlayerIndicator` components for remote players
- **Player Section** (may be hidden):
- Shows local active players
- Add player button (for local players only)
**Key Sub-Components:**
- `GameTitleMenu` - Menu for game options (setup, new game, quit)
- `GameModeIndicator` - Shows 🎯 Solo, ⚔️ Battle, 🏆 Tournament, 👥 Select
- `RoomInfo` - Displays room metadata
- `NetworkPlayerIndicator` - Shows remote players with scores/streaks
- `ActivePlayersList` - List of selected players
- `AddPlayerButton` - Button to add more players with popover
- `FullscreenPlayerSelection` - Large player picker for fullscreen mode
- `PendingInvitations` - Banner for room invitations
**State Management:**
- Lifted from `PageWithNav` to preserve state across remounts:
- `showPopover` / `setShowPopover` - AddPlayerButton popover state
- `activeTab` / `setActiveTab` - 'add' or 'invite' tab selection
## 4. Navigation Flow
### Flow 1: Solo Player → Game Selection → Room Creation → Game Start
```
/arcade (Champion Arena)
↓ [Select players - updates GameModeContext]
↓ [Click game card - GameCard.onClick → router.push]
/arcade/room (if not in room, shows game selector)
↓ [Select game - calls setRoomGame mutation]
↓ [Room created, gameName saved to roomData]
↓ [useRoomData refetch updates roomData.gameName]
/arcade/room (now displays the game)
↓ [Game Provider and Component render]
```
### Flow 2: Multiplayer - Room Invitation
```
User A: Creates room via Champion Arena
User B: Receives invitation
User B: Joins room via /arcade/room
User B: Sees same game selection (if set) or game selector (if not set)
```
### Flow 3: Exit Game
```
/arcade/room (in-game)
↓ [Click "Quit" or "Exit Session" in GameContextNav]
↓ [onExitSession callback → router.push('/arcade')]
/arcade (back to champion arena)
↓ Player selection reset by GameModeContext
```
## 5. Game Chooser / Game Selection System
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**How It Works:**
1. `GameSelector` component gets all games from both sources:
- Legacy `GAMES_CONFIG` (currently empty)
- Registry games via `getAllGames()`
2. For each game, creates `GameCard` component with configuration including `url` field
3. Game Cards rendered in 2-column grid (responsive)
4. When card clicked:
- `GameCard` checks `activePlayerCount` against game's `maxPlayers`
- If valid: calls `router.push(config.url)` - client-side navigation via Next.js
- If invalid: blocks navigation with warning
**Two Game Systems:**
### Registry Games (NEW - Modular)
- Location: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/arcade-games/`
- File: `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/lib/arcade/game-registry.ts`
- Examples: `complement-race`, `memory-quiz`, `matching`
- Each game has: `manifest` (metadata), `Provider` (context), `GameComponent` (UI)
- Games registered globally via `registerGame()` function
### Legacy Games (OLD)
- Location: Directly in `/app/arcade/` directory
- Examples: `/app/arcade/complement-race/page.tsx`
- Currently, only complement-race is partially migrated
- Direct URL structure: `/arcade/{gameName}/page.tsx`
**Game Config Structure (for display):**
```javascript
{
name: string, // Display name
fullName?: string, // Longer name for detailed view
description: string, // Short description
longDescription?: string, // Detailed description
icon: emoji, // Game icon emoji
gradient: css gradient, // Background gradient
borderColor: css color, // Border color for availability
maxPlayers: number, // Player limit for validation
chips?: string[], // Feature labels
color?: 'green'|'purple'|'blue', // Color theme
difficulty?: string, // Difficulty level
available: boolean, // Is game available
}
```
## 6. Key Components Summary
### PageWithNav - Main Layout Wrapper
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/PageWithNav.tsx` (lines 1-192)
**Responsibilities:**
- Wraps all game/arcade pages
- Manages GameContextNav state (mini-nav)
- Handles player configuration dialog
- Shows moderation notifications
- Renders top navigation bar via `AppNavBar`
**Key Props:**
- `navTitle` - Passed to GameContextNav
- `navEmoji` - Passed to GameContextNav
- `gameName` - Internal game name for API
- `emphasizePlayerSelection` - Highlight player controls
- `onExitSession` - Callback when user exits
- `onSetup`, `onNewGame` - Game-specific callbacks
- `children` - Page content
### AppNavBar - Top Navigation Bar
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/AppNavBar.tsx` (lines 1-625)
**Variants:**
- `full` - Standard navigation (default for non-game pages)
- `minimal` - Game navigation (auto-selected for `/arcade` and `/games`)
**Minimal Nav Features:**
- Hamburger menu (left) with:
- Site navigation (Home, Create, Guide, Games)
- Controls (Fullscreen, Exit Arcade)
- Abacus style dropdown
- Centered game context (navSlot)
- Fullscreen indicator badge
### EnhancedChampionArena - Main Arcade Display
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/EnhancedChampionArena.tsx` (lines 1-40)
**Responsibilities:**
- Container for game selector
- Full-height flex layout
- Passes configuration to `GameSelector`
### GameSelector - Game Grid
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameSelector.tsx` (lines 1-112)
**Responsibilities:**
- Fetches all games from registry
- Arranges in responsive grid
- Shows header "🎮 Available Games"
- Renders GameCard for each game
### GameCard - Individual Game Button
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/components/GameCard.tsx` (lines 1-241)
**Responsibilities:**
- Displays game with icon, name, description
- Shows feature chips and player count indicator
- Validates player count against game requirements
- Handles click to navigate to game
- Two variants: compact and detailed
## 7. State Management
### GameModeContext
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/contexts/GameModeContext.tsx` (lines 1-325)
**Manages:**
- Local players (Map<string, Player>)
- Active players (Set<string>)
- Game mode (computed from active player count)
- Player CRUD operations (add, update, remove)
**Key Features:**
- Fetches players from user's local DB via `useUserPlayers()`
- Creates 4 default players if none exist
- When in room: merges room members' players (marked as isLocal: false)
- Syncs to room members via `notifyRoomOfPlayerUpdate()`
**Computed Values:**
- `activePlayerCount` - Size of activePlayers set
- `gameMode`:
- 1 player → 'single'
- 2 players → 'battle'
- 3+ players → 'tournament'
### useRoomData Hook
**File:** `/Users/antialias/projects/soroban-abacus-flashcards/apps/web/src/hooks/useRoomData.ts` (lines 1-450+)
**Manages:**
- Current room fetching via TanStack Query
- Socket.io real-time updates
- Room state (members, players, game name)
- Moderation events (kicked, banned, invitations)
**Key Operations:**
- `fetchCurrentRoom()` - GET `/api/arcade/rooms/current`
- `createRoomApi()` - POST `/api/arcade/rooms`
- `joinRoomApi()` - POST `/api/arcade/rooms/{id}/join`
- `leaveRoomApi()` - POST `/api/arcade/rooms/{id}/leave`
- `setRoomGame()` - Updates room's gameName and gameConfig
**Socket Events:**
- `join-user-channel` - Personal notifications
- `join-room` - Subscribe to room updates
- `room-joined` - Refresh when entering room
- `member-joined` - When player joins
- `member-left` - When player leaves
- `room-players-updated` - When players change
- Moderation events (kicked, banned, etc.)
## 8. Routing Summary
**Current URL Structure:**
```
/ → Home page (Soroban Generator)
/create → Create flashcards
/guide → Tutorial guide
/games → Games library (external game pages)
/arcade → Champion Arena (main landing with game selector)
/arcade/room → Active game display or game selection UI
/arcade/room?game={name} → Query param for game selection (optional)
/arcade/complement-race → OLD: Direct complement-race page (legacy)
/arcade/complement-race/practice → Complement-race practice mode
/arcade/complement-race/sprint → Complement-race sprint mode
/arcade/complement-race/survival → Complement-race survival mode
/arcade/memory-quiz → Memory quiz game page (legacy structure)
```
**Query Parameters:**
- `/arcade/room?game={gameName}` - Optional game selection (parsed by GameCard)
## 9. Key Differences: /arcade vs /arcade/room
| Aspect | /arcade | /arcade/room |
|--------|---------|--------------|
| **Purpose** | Game selection hub | Active game display or selection within room |
| **Displays** | GameSelector with all games | Selected game OR game selector if no game in room |
| **Room Context** | Optional (can start solo) | Usually in a room (fetches via useRoomData) |
| **Navigation** | Click game → /arcade/room | Click game → Saves to room → Displays game |
| **GameContextNav** | Shows player selector | Shows room info when joined |
| **Player State** | Local only | Local + remote (room members) |
| **Exit Button** | Usually hidden | Shows "Exit Session" to return to /arcade |
| **Socket Connection** | Optional | Always connected (in room) |
| **Page Transition** | User controls | Driven by room state updates |
## 10. Planning the Merge (/arcade/room → /arcade)
**Challenges to Consider:**
1. **URL Consolidation:**
- `/arcade/room` would become a sub-path or handled by `/arcade` with state
- Query param `?game={name}` could drive game selection
- Current: `/arcade/room?game=complement-race`
- Could become: `/arcade?game=complement-race&mode=play`
2. **Route Disambiguation:**
- `/arcade` needs to handle: game selection display, game display, game loading
- Same page different modes based on state
- Or: Sub-routes like `/arcade/select`, `/arcade/play`
3. **State Layering:**
- Local game mode (solo player, GameModeContext)
- Room state (multiplayer, useRoomData)
- Both need to coexist
4. **Navigation Preservation:**
- Currently: `GameCard``router.push('/arcade/room?game=X')`
- After merge: Would need new logic
- Fullscreen state must persist (uses Next.js router, not reload)
5. **PageWithNav Behavior:**
- Mini-nav shows game selection UI vs room info
- Currently determined by `roomInfo` presence
- After merge: Need same logic but one route
6. **Game Display:**
- Currently: `/arcade/room` fetches game from registry
- New: `/arcade` would need same game registry lookup
- Game Provider/Component rendering must work identically
**Merge Strategy Options:**
### Option A: Single Route with Modes
```
/arcade
├── Mode: browse (default, show GameSelector)
├── Mode: select (game selected, show GameSelector for confirmation)
└── Mode: play (in-game, show game display)
```
### Option B: Sub-routes
```
/arcade
├── /arcade (selector)
├── /arcade/play (game display)
└── /arcade/configure (player config)
```
### Option C: Query-Parameter Driven
```
/arcade
├── /arcade (default - selector)
├── /arcade?game=X (game loading)
└── /arcade?game=X&playing=true (in-game)
```
**Recommendation:** Option C (Query-driven) is closest to current architecture and requires minimal changes to existing logic.

View File

@@ -90,6 +90,37 @@ npm run check # Biome check (format + lint + organize imports)
**Remember: Always run `npm run pre-commit` before creating commits.**
## Styling Framework
**CRITICAL: This project uses Panda CSS, NOT Tailwind CSS.**
- All styling is done with Panda CSS (`@pandacss/dev`)
- Configuration: `/panda.config.ts`
- Generated system: `/styled-system/`
- Import styles using: `import { css } from '../../styled-system/css'`
- Token syntax: `color: 'blue.200'`, `borderColor: 'gray.300'`, etc.
**Common Mistakes to Avoid:**
- ❌ Don't reference "Tailwind" in code, comments, or documentation
- ❌ Don't use Tailwind utility classes (e.g., `className="bg-blue-500"`)
- ✅ Use Panda CSS `css()` function for all styling
- ✅ Use Panda's token system (defined in `panda.config.ts`)
**Color Tokens:**
```typescript
// Correct (Panda CSS)
css({
bg: 'blue.200',
borderColor: 'gray.300',
color: 'brand.600'
})
// Incorrect (Tailwind)
className="bg-blue-200 border-gray-300 text-brand-600"
```
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
## Known Issues
### @soroban/abacus-react TypeScript Module Resolution

View File

@@ -0,0 +1,154 @@
# Game Theme Standardization
## Problem
Previously, each game manually specified `color`, `gradient`, and `borderColor` in their manifest. This led to:
- Inconsistent appearance across game cards
- No guidance on what colors/gradients to use
- Easy to choose saturated colors that don't match the pastel style
- Duplication and maintenance burden
## Solution
**Standard theme presets** in `/src/lib/arcade/game-themes.ts`
All games now use predefined color themes that ensure consistent, professional appearance.
## Usage
### 1. Import from the Game SDK
```typescript
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
```
### 2. Use the Theme Spread Operator
```typescript
const manifest: GameManifest = {
name: 'my-game',
displayName: 'My Awesome Game',
icon: '🎮',
description: 'A fun game',
longDescription: 'More details...',
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['🎯 Feature 1', '⚡ Feature 2'],
...getGameTheme('blue'), // ← Just add this!
available: true,
}
```
That's it! The theme automatically provides:
- `color: 'blue'`
- `gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)'`
- `borderColor: 'blue.200'`
## Available Themes
All themes use Panda CSS's 100-200 color range for soft pastel appearance:
| Theme | Color Range | Use Case |
|-------|-------------|----------|
| `blue` | blue-100 to blue-200 | Memory, puzzle games |
| `purple` | purple-100 to purple-200 | Strategic, battle games |
| `green` | green-100 to green-200 | Growth, achievement games |
| `teal` | teal-100 to teal-200 | Creative, sorting games |
| `indigo` | indigo-100 to indigo-200 | Deep thinking games |
| `pink` | pink-100 to pink-200 | Fun, casual games |
| `orange` | orange-100 to orange-200 | Speed, energy games |
| `yellow` | yellow-100 to yellow-200 | Bright, happy games |
| `red` | red-100 to red-200 | Competition, challenge |
| `gray` | gray-100 to gray-200 | Neutral games |
## Examples
### Current Games
```typescript
// Memory Lightning - blue theme
...getGameTheme('blue')
// Matching Pairs Battle - purple theme
...getGameTheme('purple')
// Card Sorting Challenge - teal theme
...getGameTheme('teal')
// Speed Complement Race - blue theme
...getGameTheme('blue')
```
## Benefits
**Consistency** - All games have the same professional pastel look
**Simple** - One line instead of three properties
**Maintainable** - Update all games by changing the theme definition
**Discoverable** - TypeScript autocomplete shows available themes
**No mistakes** - Can't accidentally use wrong color values
## Advanced Usage
If you need to inspect or customize a theme:
```typescript
import { GAME_THEMES } from '@/lib/arcade/game-sdk'
import type { GameTheme } from '@/lib/arcade/game-sdk'
// Access a specific theme
const blueTheme: GameTheme = GAME_THEMES.blue
// Use it
const manifest: GameManifest = {
// ... other fields
...blueTheme,
// Or customize:
color: blueTheme.color,
gradient: 'linear-gradient(135deg, #custom, #values)', // override
borderColor: blueTheme.borderColor,
}
```
## Adding New Themes
To add a new theme, edit `/src/lib/arcade/game-themes.ts`:
```typescript
export const GAME_THEMES = {
// ... existing themes
mycolor: {
color: 'mycolor',
gradient: 'linear-gradient(135deg, #lighter, #darker)', // Use Panda CSS 100-200 range
borderColor: 'mycolor.200',
},
} as const satisfies Record<string, GameTheme>
```
Then update the TypeScript type:
```typescript
export type GameThemeName = keyof typeof GAME_THEMES
```
## Migration Checklist
When creating a new game:
- [x] Import `getGameTheme` from `@/lib/arcade/game-sdk`
- [x] Use `...getGameTheme('theme-name')` in manifest
- [x] Remove manual `color`, `gradient`, `borderColor` properties
- [x] Choose a theme that matches your game's vibe
## Summary
**Old way** (error-prone, inconsistent):
```typescript
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)', // Too saturated!
borderColor: 'teal.200',
```
**New way** (simple, consistent):
```typescript
...getGameTheme('teal')
```

View File

@@ -97,7 +97,10 @@
"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)",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run ID: \\(.databaseId)\"\"')",
"Bash(node -e:*)"
],
"deny": [],
"ask": []

View File

@@ -158,7 +158,7 @@ export default function RoomDetailPage() {
const startGame = () => {
if (!room) return
// Navigate to the room game page
router.push('/arcade/room')
router.push('/arcade')
}
const joinRoom = async () => {

View File

@@ -1,128 +1,315 @@
'use client'
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
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 { EnhancedChampionArena } from '../../components/EnhancedChampionArena'
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
function ArcadeContent() {
const { setFullscreenElement } = useFullscreen()
const arcadeRef = useRef<HTMLDivElement>(null)
/**
* /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
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade regardless of selection, setup, or gameplay.
*
* Note: We show a friendly message with a link if no room exists to avoid navigation loops.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
useEffect(() => {
// Register this component's main div as the fullscreen element
if (arcadeRef.current) {
setFullscreenElement(arcadeRef.current)
}
}, [setFullscreenElement])
return (
<div
ref={arcadeRef}
className={css({
minHeight: 'calc(100vh - 80px)', // Account for mini nav height
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
position: 'relative',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
py: { base: '4', md: '6' },
})}
>
{/* Animated background elements */}
// Show loading state
if (isLoading) {
return (
<div
className={css({
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: `
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%)
`,
animation: 'arcadeFloat 20s ease-in-out infinite',
})}
/>
{/* Main Champion Arena - takes remaining space */}
<div
className={css({
flex: 1,
style={{
display: 'flex',
px: { base: '2', md: '4' },
position: 'relative',
zIndex: 1,
minHeight: 0, // Important for flex children
})}
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
<EnhancedChampionArena
onConfigurePlayer={() => {}}
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
gap: '1rem',
}}
>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
return
}
console.log('[RoomPage] Unknown game type:', gameType)
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.05)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
/>
</div>
</div>
)
}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
function ArcadePageWithRedirect() {
return (
<PageWithNav navTitle="Champion Arena" navEmoji="🏟️" emphasizePlayerSelection={true}>
<ArcadeContent />
</PageWithNav>
)
}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
export default function ArcadePage() {
return (
<FullscreenProvider>
<ArcadePageWithRedirect />
</FullscreenProvider>
)
}
// Arcade-specific animations
const arcadeAnimations = `
@keyframes arcadeFloat {
0%, 100% {
transform: translateY(0px) rotate(0deg);
opacity: 0.7;
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
33% {
transform: translateY(-20px) rotate(1deg);
opacity: 1;
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
66% {
transform: translateY(-10px) rotate(-1deg);
opacity: 0.8;
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}
@keyframes arcadePulse {
0%, 100% {
box-shadow: 0 0 20px rgba(96, 165, 250, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(96, 165, 250, 0.6);
}
}
`
// Inject arcade animations
if (typeof document !== 'undefined' && !document.getElementById('arcade-animations')) {
const style = document.createElement('style')
style.id = 'arcade-animations'
style.textContent = arcadeAnimations
document.head.appendChild(style)
}

View File

@@ -1,358 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
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 { 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/room - 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
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade/room regardless of selection, setup, or gameplay.
*
* Note: We don't redirect to /arcade if no room exists to avoid navigation loops.
* Instead, we show a friendly message with a link back to the Champion Arena.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
// Show loading state
if (isLoading) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
gap: '1rem',
}}
>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if it's a registry game first
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType, // Use the game name directly for registry games
})
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,
})
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}

View File

@@ -231,7 +231,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
password: roomPassword,
})
// Navigate to the game
router.push('/arcade/room')
router.push('/arcade')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join room')
setIsJoining(false)
@@ -261,7 +261,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
// If user is already in this exact room, just navigate to game
if (roomData && roomData.id === room.id) {
router.push('/arcade/room')
router.push('/arcade')
return
}
@@ -313,7 +313,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
}
const handleCancel = () => {
router.push('/arcade/room') // Stay in current room
router.push('/arcade') // Stay in current room
}
const handlePasswordSubmit = () => {

View File

@@ -1,185 +1,465 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { PageWithNav } from '@/components/PageWithNav'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
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() {
const [abacusValue, setAbacusValue] = useState(1234567)
const appConfig = useAbacusConfig()
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' })}>
<PageWithNav navTitle="Soroban Mastery Platform" navEmoji="🧮">
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero with Large Abacus */}
<div
className={css({
background:
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(88, 28, 135, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
color: 'white',
py: { base: '12', md: '20' },
position: 'relative',
overflow: 'hidden',
})}
>
<div
className={stack({
gap: '12',
py: '16',
align: 'center',
textAlign: 'center',
className={css({
position: 'absolute',
inset: 0,
opacity: 0.1,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
})}
>
{/* Hero Content */}
<div className={stack({ gap: '6', maxW: '4xl' })}>
/>
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
<h1
className={css({
fontSize: { base: '4xl', md: '6xl' },
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
fontWeight: 'bold',
color: 'gray.900',
mb: '6',
lineHeight: 'tight',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Beautiful Soroban <span className={css({ color: 'brand.600' })}>Flashcards</span>
Master the Soroban
</h1>
{/* Large Featured Abacus */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
my: '10',
p: '8',
bg: 'white',
borderRadius: '2xl',
border: '2px solid',
borderColor: 'purple.500/30',
boxShadow: '0 25px 50px -12px rgba(139, 92, 246, 0.25)',
color: '#1f2937',
})}
>
<AbacusReact
value={abacusValue}
columns={7}
beadShape={appConfig.beadShape}
colorScheme={appConfig.colorScheme}
hideInactiveBeads={appConfig.hideInactiveBeads}
interactive={true}
animated={true}
soundEnabled={true}
soundVolume={0.4}
scaleFactor={2.2}
showNumbers={true}
onValueChange={(newValue: number) => setAbacusValue(newValue)}
/>
</div>
<p
className={css({
fontSize: { base: 'lg', md: 'xl' },
color: 'gray.600',
maxW: '2xl',
color: 'gray.300',
mb: '8',
maxW: '3xl',
mx: 'auto',
})}
>
Create stunning, educational flashcards with authentic Japanese abacus
representations. Perfect for teachers, students, and mental math enthusiasts.
Interactive tutorials, multiplayer games, and beautiful flashcardsyour complete
soroban learning ecosystem
</p>
<div className={hstack({ gap: '4', justify: 'center', mt: '8' })}>
<div className={hstack({ gap: '4', justify: 'center', flexWrap: 'wrap' })}>
<Link
href="/create"
href="/games"
className={css({
px: '8',
py: '4',
bg: 'brand.600',
color: 'white',
bg: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
color: 'gray.900',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
shadow: '0 10px 40px rgba(251, 191, 36, 0.3)',
_hover: {
bg: 'brand.700',
transform: 'translateY(-2px)',
shadow: 'modal',
shadow: '0 20px 50px rgba(251, 191, 36, 0.4)',
},
transition: 'all 0.3s ease',
})}
>
Start Creating
🎮 Play Games
</Link>
<Link
href="/guide"
className={css({
px: '8',
py: '4',
bg: 'white',
color: 'brand.700',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
border: '2px solid',
borderColor: 'brand.200',
transition: 'all',
borderColor: 'purple.500',
_hover: {
borderColor: 'brand.400',
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
📚 Learn Soroban
📚 Learn
</Link>
<Link
href="/create"
className={css({
px: '8',
py: '4',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
rounded: 'xl',
border: '2px solid',
borderColor: 'purple.500',
_hover: {
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
🎨 Create
</Link>
</div>
</div>
</div>
</div>
{/* Features Grid */}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1', md: '3' },
gap: '8',
mt: '16',
w: 'full',
})}
>
<FeatureCard
icon="🎨"
title="Beautiful Design"
description="Vector graphics, color schemes, authentic bead positioning"
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Arcade Games Section */}
<section className={stack({ gap: '6', mt: '16' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
<div>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
🕹 Multiplayer Arcade
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>
Compete with friends in real-time soroban games
</p>
</div>
<Link
href="/games"
className={css({
fontSize: 'md',
color: 'yellow.400',
fontWeight: 'semibold',
_hover: { color: 'yellow.300' },
display: { base: 'none', md: 'block' },
})}
>
View All
</Link>
</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={['Co-op', 'Competitive']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<FeatureCard
icon=""
title="Instant Generation"
description="Create PDFs, interactive HTML, PNGs, and SVGs in seconds"
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Turn-based card battles"
players="1-4 players"
tags={['Pattern Recognition']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<FeatureCard
icon="🎯"
title="Educational Focus"
description="Perfect for teachers, students, and soroban enthusiasts"
<GameCard
icon="🏁"
title="Speed Race"
description="Race AI with complements"
players="1-4 players + AI"
tags={['Practice', 'Sprint', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange cards visually"
players="Solo challenge"
tags={['Visual Literacy']}
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
href="/games"
/>
</div>
</section>
{/* Interactive Learning & Flashcard Creator */}
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8', mt: '16' })}>
<FeaturePanel
icon="📚"
title="Interactive Learning"
description="Master soroban through hands-on guided tutorials"
features={[
'Visual tutorials on reading bead positions',
'Step-by-step arithmetic operations',
'Interactive exercises with instant feedback',
]}
ctaText="Start Learning →"
ctaHref="/guide"
accentColor="purple"
/>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
description="Design beautiful soroban flashcards for any purpose"
features={[
'Multiple export formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
ctaText="Create Flashcards →"
ctaHref="/create"
accentColor="blue"
/>
</div>
</main>
</div>
</div>
</PageWithNav>
)
}
function FeatureCard({
icon,
function ColorSchemeCard({
title,
description,
colorScheme,
value,
beadShape,
}: {
icon: string
title: string
description: string
colorScheme: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
value: number
beadShape: 'diamond' | 'circle' | 'square'
}) {
return (
<div
className={css({
p: '8',
bg: 'white',
rounded: '2xl',
shadow: 'card',
textAlign: 'center',
transition: 'all',
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '6',
border: '2px solid',
borderColor: 'gray.700',
transition: 'all 0.3s ease',
_hover: {
borderColor: 'purple.500',
transform: 'translateY(-4px)',
shadow: 'modal',
boxShadow: '0 20px 40px rgba(139, 92, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '4',
})}
>
{icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
mb: '3',
color: 'white',
mb: '2',
})}
>
{title}
</h3>
<p
<p className={css({ fontSize: 'sm', color: 'gray.400', mb: '4' })}>{description}</p>
<div
className={css({
color: 'gray.600',
lineHeight: 'relaxed',
display: 'flex',
justifyContent: 'center',
p: '4',
bg: 'rgba(255, 255, 255, 0.05)',
rounded: 'lg',
})}
>
{description}
</p>
<AbacusReact
value={value}
columns={3}
beadShape={beadShape}
colorScheme={colorScheme}
hideInactiveBeads={false}
/>
</div>
</div>
)
}
function GameCard({
icon,
title,
description,
players,
tags,
gradient,
href,
}: {
icon: string
title: string
description: string
players: string
tags: string[]
gradient: string
href: string
}) {
return (
<Link href={href}>
<div
className={css({
background: gradient,
rounded: 'xl',
p: '6',
shadow: 'lg',
transition: 'all 0.3s ease',
cursor: 'pointer',
_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>
))}
</div>
</div>
</Link>
)
}
function FeaturePanel({
icon,
title,
description,
features,
ctaText,
ctaHref,
accentColor,
}: {
icon: string
title: string
description: string
features: string[]
ctaText: string
ctaHref: string
accentColor: 'purple' | 'blue'
}) {
const borderColor = accentColor === 'purple' ? 'purple.500/30' : 'blue.500/30'
const bgColor = accentColor === 'purple' ? 'purple.500/10' : 'blue.500/10'
const hoverBg = accentColor === 'purple' ? 'purple.500/20' : 'blue.500/20'
return (
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '2px solid',
borderColor,
})}
>
<div className={hstack({ gap: '3', mb: '4' })}>
<span className={css({ fontSize: '3xl' })}>{icon}</span>
<h2 className={css({ fontSize: '2xl', fontWeight: 'bold', color: 'white' })}>{title}</h2>
</div>
<p className={css({ fontSize: 'md', color: 'gray.300', mb: '6' })}>{description}</p>
<div className={stack({ gap: '3', mb: '6' })}>
{features.map((feature, i) => (
<div key={i} className={hstack({ gap: '3' })}>
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}></span>
<span className={css({ color: 'gray.300', fontSize: 'sm' })}>{feature}</span>
</div>
))}
</div>
<Link
href={ctaHref}
className={css({
display: 'block',
textAlign: 'center',
py: '3',
px: '6',
bg: bgColor,
color: 'white',
fontWeight: 'bold',
rounded: 'lg',
border: '2px solid',
borderColor,
_hover: { bg: hoverBg },
transition: 'all 0.2s ease',
})}
>
{ctaText}
</Link>
</div>
)
}

View File

@@ -1,13 +1,6 @@
'use client'
import {
type ReactNode,
useCallback,
useMemo,
createContext,
useContext,
useState,
} from 'react'
import { type ReactNode, useCallback, useMemo, createContext, useContext, useState } from 'react'
import { useArcadeSession } from '@/hooks/useArcadeSession'
import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'

View File

@@ -264,13 +264,15 @@ export function PlayingPhase() {
</h3>
<div
className={css({
display: 'grid',
gridTemplateColumns: {
base: '1',
sm: '2',
md: '3',
},
gap: '0.75rem',
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
justifyContent: 'center',
padding: '15px',
background: 'rgba(255,255,255,0.5)',
borderRadius: '8px',
minHeight: '120px',
border: '2px dashed #2c5f76',
})}
>
{state.availableCards.map((card) => (
@@ -278,39 +280,65 @@ export function PlayingPhase() {
key={card.id}
onClick={() => handleCardClick(card.id)}
className={css({
padding: '0.5rem',
width: '140px',
height: '140px',
padding: '8px',
border: '2px solid',
borderColor: selectedCardId === card.id ? 'blue.500' : 'gray.300',
borderRadius: '0.5rem',
background: selectedCardId === card.id ? 'blue.50' : 'white',
borderColor: selectedCardId === card.id ? '#1976d2' : 'transparent',
borderRadius: '8px',
background: selectedCardId === card.id ? '#e3f2fd' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
transform: selectedCardId === card.id ? 'scale(1.05)' : 'scale(1)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
_hover: {
transform: 'scale(1.05)',
borderColor: 'blue.500',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
borderColor: '#2c5f76',
},
})}
style={
selectedCardId === card.id
? {
transform: 'scale(1.1)',
boxShadow: '0 6px 20px rgba(25, 118, 210, 0.3)',
}
: undefined
}
>
<div
dangerouslySetInnerHTML={{ __html: card.svgContent }}
className={css({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
{state.numbersRevealed && (
<div
className={css({
textAlign: 'center',
marginTop: '0.5rem',
fontSize: 'lg',
position: 'absolute',
top: '5px',
right: '5px',
background: '#ffc107',
color: '#333',
borderRadius: '4px',
padding: '2px 8px',
fontSize: '14px',
fontWeight: 'bold',
color: 'gray.700',
})}
>
{card.number}
@@ -336,8 +364,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 */}
@@ -376,13 +410,14 @@ export function PlayingPhase() {
const isEmpty = card === null
return (
<div key={index}>
<>
{/* Position slot */}
<div
key={`slot-${index}`}
onClick={() => handleSlotClick(index)}
className={css({
width: '90px',
height: '110px',
width: '140px',
height: '160px',
padding: '0.5rem',
borderRadius: '8px',
border: '2px solid',
@@ -423,10 +458,16 @@ export function PlayingPhase() {
__html: card.svgContent,
}}
className={css({
width: '70px',
flex: 1,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
@@ -457,6 +498,7 @@ export function PlayingPhase() {
{/* Insert button after this position */}
<button
key={`insert-${index + 1}`}
type="button"
onClick={() => handleInsertClick(index + 1)}
disabled={!selectedCardId}
@@ -472,7 +514,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',
@@ -485,7 +526,7 @@ export function PlayingPhase() {
>
+
</button>
</div>
</>
)
})}
</div>

View File

@@ -5,7 +5,7 @@
* in ascending order using only visual patterns (no numbers shown).
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { GameComponent } from './components/GameComponent'
import { CardSortingProvider } from './Provider'
@@ -24,9 +24,7 @@ const manifest: GameManifest = {
maxPlayers: 1, // Single player only
difficulty: 'Intermediate',
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
color: 'teal',
gradient: 'linear-gradient(135deg, #99f6e4, #5eead4)',
borderColor: 'teal.200',
...getGameTheme('teal'),
available: true,
}

View File

@@ -3,7 +3,7 @@
* Complete integration into the arcade system with multiplayer support
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { complementRaceValidator } from './Validator'
import { ComplementRaceProvider } from './Provider'
@@ -20,9 +20,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
icon: '🏁',
chips: ['👥 1-4 Players', '🚂 Sprint Mode', '🤖 AI Opponents', '🔥 Speed Challenge'],
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
borderColor: 'blue.200',
...getGameTheme('blue'),
difficulty: 'Intermediate',
available: true,
}

View File

@@ -5,7 +5,7 @@
* Supports both abacus-numeral matching and complement pairs modes.
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { MemoryPairsGame } from './components/MemoryPairsGame'
import { MatchingProvider } from './Provider'
@@ -23,9 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)',
borderColor: 'purple.200',
...getGameTheme('purple'),
available: true,
}

View File

@@ -5,7 +5,7 @@
* Supports both cooperative and competitive multiplayer modes.
*/
import { defineGame } from '@/lib/arcade/game-sdk'
import { defineGame, getGameTheme } from '@/lib/arcade/game-sdk'
import type { GameManifest } from '@/lib/arcade/game-sdk'
import { MemoryQuizGame } from './components/MemoryQuizGame'
import { MemoryQuizProvider } from './Provider'
@@ -23,9 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 8,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)',
borderColor: 'blue.200',
...getGameTheme('blue'),
available: true,
}

View File

@@ -31,7 +31,7 @@ function getAllGameConfigs() {
maxPlayers: gameDef.manifest.maxPlayers,
description: gameDef.manifest.description,
longDescription: gameDef.manifest.longDescription,
url: `/arcade/room?game=${gameDef.manifest.name}`, // Registry games load in room
url: '/arcade', // Arcade page handles game selection through UI
icon: gameDef.manifest.icon,
chips: gameDef.manifest.chips,
color: gameDef.manifest.color,

View File

@@ -80,7 +80,7 @@ export function AddPlayerButton({
})
// Close popover and navigate to room to choose game
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
onError: (error) => {
console.error('Failed to create room:', error)
@@ -111,7 +111,7 @@ export function AddPlayerButton({
}
// Close popover and navigate to room
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
}
)

View File

@@ -833,7 +833,7 @@ export function ModerationNotifications({
// Close the modal
onClose()
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
} catch (error) {
console.error('Failed to join room:', error)
showError(

View File

@@ -68,7 +68,7 @@ export function PendingInvitations({ onInvitationChange, currentRoomId }: Pendin
// Join the room
await joinRoom({ roomId: invitation.roomId })
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
// Refresh invitations
await fetchInvitations()
onInvitationChange?.()

View File

@@ -82,6 +82,13 @@ export { loadManifest } from './load-manifest'
*/
export { defineGame } from './define-game'
/**
* Standard color themes for game cards
* Use these to ensure consistent appearance across all games
*/
export { getGameTheme, GAME_THEMES } from '../game-themes'
export type { GameTheme, GameThemeName } from '../game-themes'
// ============================================================================
// Re-exports for convenience
// ============================================================================

View File

@@ -0,0 +1,88 @@
/**
* Standard color themes for arcade game cards
*
* Use these presets to ensure consistent, professional appearance
* across all game cards on the /arcade game chooser.
*
* All gradients use Panda CSS's 100-200 color range for soft pastel appearance.
*/
export interface GameTheme {
color: string
gradient: string
borderColor: string
}
/**
* Standard theme presets
* These use Panda CSS's color system and provide consistent styling
*/
export const GAME_THEMES = {
blue: {
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
borderColor: 'blue.200',
},
purple: {
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
borderColor: 'purple.200',
},
green: {
color: 'green',
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
borderColor: 'green.200',
},
teal: {
color: 'teal',
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
borderColor: 'teal.200',
},
indigo: {
color: 'indigo',
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
borderColor: 'indigo.200',
},
pink: {
color: 'pink',
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
borderColor: 'pink.200',
},
orange: {
color: 'orange',
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
borderColor: 'orange.200',
},
yellow: {
color: 'yellow',
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
borderColor: 'yellow.200',
},
red: {
color: 'red',
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
borderColor: 'red.200',
},
gray: {
color: 'gray',
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
borderColor: 'gray.200',
},
} as const satisfies Record<string, GameTheme>
export type GameThemeName = keyof typeof GAME_THEMES
/**
* Get a standard theme by name
* Use this in your game manifest instead of hardcoding gradients
*
* @example
* const manifest: GameManifest = {
* name: 'my-game',
* // ... other fields
* ...getGameTheme('blue')
* }
*/
export function getGameTheme(themeName: GameThemeName): GameTheme {
return GAME_THEMES[themeName]
}

View File

@@ -81,7 +81,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
session = await createArcadeSession({
userId,
gameName: room.gameName as GameName,
gameUrl: '/arcade/room',
gameUrl: '/arcade',
initialState,
activePlayers: roomPlayerIds, // Include all room members' active players
roomId: room.id,
@@ -173,7 +173,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
await createArcadeSession({
userId: data.userId,
gameName: 'matching',
gameUrl: '/arcade/room', // Room-based sessions use /arcade/room
gameUrl: '/arcade', // Room-based sessions use /arcade
initialState,
activePlayers,
roomId: room.id,

View File

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