Compare commits

...

41 Commits

Author SHA1 Message Date
semantic-release-bot
e7e54619ae chore(release): 4.20.3 [skip ci]
## [4.20.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.2...v4.20.3) (2025-10-19)

### Bug Fixes

* **homepage:** use explicit RGBA colors for Your Journey text ([9c51cc9](9c51cc94ee))
2025-10-19 18:58:39 +00:00
Thomas Hallock
9c51cc94ee fix(homepage): use explicit RGBA colors for Your Journey text
Switched from Panda CSS gray tokens to explicit RGBA values for better compatibility:
- Text: rgba(229, 231, 235, 1) - light gray for maximum readability
- Subtitle: rgba(209, 213, 219, 1) - slightly darker light gray
- Arrows: rgba(156, 163, 175, 1) - medium gray

This ensures proper rendering regardless of theme token configuration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:57:30 -05:00
semantic-release-bot
df674426c5 chore(release): 4.20.2 [skip ci]
## [4.20.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.1...v4.20.2) (2025-10-19)

### Bug Fixes

* **homepage:** improve text contrast in Your Journey section ([24d1200](24d120004d))
* **tutorial:** resolve TypeScript errors in TutorialPlayer ([88f57ce](88f57ce6df))
2025-10-19 18:57:20 +00:00
Thomas Hallock
24d120004d fix(homepage): improve text contrast in Your Journey section
Changed gray text colors to lighter values for better readability on dark background:
- Subtitle text: gray.400 → gray.200
- Stage labels: gray.400 → gray.200
- Navigation arrows: gray.600 → gray.400
- Footer text: gray.500 → gray.300

This addresses readability concerns while maintaining visual hierarchy.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:56:04 -05:00
Thomas Hallock
88f57ce6df fix(tutorial): resolve TypeScript errors in TutorialPlayer
- Remove references to non-existent highlight.columnIndex property
- Remove references to removed currentStep.errorMessages property
- Use placeValue directly for highlight filtering and calculations
- Add generic error message for incorrect bead clicks

All changes maintain existing functionality while fixing type safety issues.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:54:56 -05:00
semantic-release-bot
3a5dc0f1c8 chore(release): 4.20.1 [skip ci]
## [4.20.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.0...v4.20.1) (2025-10-19)

### Bug Fixes

* **homepage:** correct positioning of progression arrows in Your Journey section ([3fff9ef](3fff9ef140))

### Code Refactoring

* **homepage:** move What You'll Learn above tutorial ([ca1c6d8](ca1c6d8602))
2025-10-19 18:51:24 +00:00
Thomas Hallock
3fff9ef140 fix(homepage): correct positioning of progression arrows in Your Journey section
Added position: 'relative' to parent containers to properly anchor the absolutely positioned arrow elements between progression levels. This ensures the arrows display correctly between stages.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:50:23 -05:00
Thomas Hallock
ca1c6d8602 refactor(homepage): move What You'll Learn above tutorial
Repositioned the learning objectives section to appear before the interactive tutorial for better visual hierarchy and user flow.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:49:21 -05:00
semantic-release-bot
e6bcf20807 chore(release): 4.20.0 [skip ci]
## [4.20.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.19.0...v4.20.0) (2025-10-19)

### Features

* **tutorial:** add hideTooltip prop and improve dark mode coaching bar ([1ee25b3](1ee25b3dd2))
2025-10-19 18:47:56 +00:00
Thomas Hallock
1ee25b3dd2 feat(tutorial): add hideTooltip prop and improve dark mode coaching bar
- Added hideTooltip prop to TutorialPlayer to optionally hide guidance panels
- Enhanced coaching bar text for dark mode (brighter yellow with glow effect)
- Applied hideTooltip to homepage tutorial for cleaner presentation
- Updated dark mode header background for better integration

These changes are specific to the homepage dark theme instance while preserving default behavior for all other uses of the tutorial system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:46:54 -05:00
semantic-release-bot
468bdebe3a chore(release): 4.19.0 [skip ci]
## [4.19.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.18.1...v4.19.0) (2025-10-19)

### Features

* **tutorial:** add fill color support for dark mode column posts and reckoning bar ([2eb3ff3](2eb3ff3406))
2025-10-19 18:38:20 +00:00
Thomas Hallock
2eb3ff3406 feat(tutorial): add fill color support for dark mode column posts and reckoning bar
Added fill property to ColumnPostStyle and ReckoningBarStyle interfaces in abacus-react to enable high-contrast colors in dark mode. Updated TutorialPlayer to set fill colors for column posts (30% white) and reckoning bar (40% white) when in dark theme mode.

This improves visibility of the abacus frame elements in dark mode on the homepage tutorial.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:37:10 -05:00
semantic-release-bot
efbe99a9e2 chore(release): 4.18.1 [skip ci]
## [4.18.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.18.0...v4.18.1) (2025-10-19)

### Bug Fixes

* **tutorial:** use correct customStyles API for dark mode frame styling ([fdc882c](fdc882cb04))
2025-10-19 18:30:53 +00:00
Thomas Hallock
fdc882cb04 fix(tutorial): use correct customStyles API for dark mode frame styling
Fixed the dark mode styling to use the correct AbacusReact customStyles API:

Previous (incorrect):
- Used nested `frame` object that doesn't exist in the API
- `frame.column`, `frame.reckoningBar`, `frame.border`

Corrected (per AbacusReact.tsx interface):
- `columnPosts` - Global styling for all column dividers
- `reckoningBar` - Horizontal middle bar styling

Changes:
- Column dividers: rgba(255, 255, 255, 0.2) with 2px stroke
- Reckoning bar: rgba(255, 255, 255, 0.25) with 3px stroke

These properties are at the root level of customStyles, not nested
under a `frame` object. The styling will now properly apply to the
abacus frame elements in dark mode.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:29:45 -05:00
semantic-release-bot
a7778c648d chore(release): 4.18.0 [skip ci]
## [4.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.2...v4.18.0) (2025-10-19)

### Features

* **tutorial:** add dark mode styling for coaching bar and abacus frame ([7e2f580](7e2f580877))
2025-10-19 18:27:22 +00:00
Thomas Hallock
7e2f580877 feat(tutorial): add dark mode styling for coaching bar and abacus frame
Enhanced the dark mode theme support for the tutorial player:

Coaching Bar:
- Updated instruction text color to use yellow.300 for dark mode instead of
  hardcoded yellow.900
- Ensures coaching instructions are readable against dark backgrounds

Abacus Frame:
- Added custom frame styling for dark mode using customStyles prop
- Column dividers: rgba(255, 255, 255, 0.15) with 2px stroke
- Reckoning bar: rgba(255, 255, 255, 0.2) with 3px stroke
- Outer border: rgba(255, 255, 255, 0.15) with 2px stroke
- Provides subtle, elegant appearance that blends with dark theme

The frame styling is automatically applied when theme="dark" and does not
affect light mode or other tutorial instances.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:26:14 -05:00
semantic-release-bot
f18a89974a chore(release): 4.17.2 [skip ci]
## [4.17.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.1...v4.17.2) (2025-10-19)

### Bug Fixes

* **tutorial:** correct column index calculation for variable column counts ([bf1ced4](bf1ced43f8))
2025-10-19 18:25:43 +00:00
Thomas Hallock
bf1ced43f8 fix(tutorial): correct column index calculation for variable column counts
Fixed a critical bug where tooltip overlays were referencing invalid column
indices when using fewer than 5 columns. The issue occurred because column
index calculations assumed a 5-column layout (0-4), but when using
abacusColumns={2}, the valid indices should be 0-1.

Changes:
- Updated targetColumnIndex calculation to use (abacusColumns - 1) - placeValue
  instead of hardcoded 4 - placeValue
- Fixed hasActiveBeadsToLeft logic to use abacusColumns for padding and
  column index conversions
- All column index calculations now properly account for the actual number
  of columns

This resolves the "Cannot read properties of undefined (reading 'heavenActive')"
error that occurred when using fewer than 5 columns on the homepage tutorial.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:24:35 -05:00
semantic-release-bot
6435027147 chore(release): 4.17.1 [skip ci]
## [4.17.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.0...v4.17.1) (2025-10-19)

### Bug Fixes

* **tutorial:** filter bead highlights when using fewer columns ([4d906ec](4d906ec20e))
2025-10-19 18:17:38 +00:00
Thomas Hallock
4d906ec20e fix(tutorial): filter bead highlights when using fewer columns
Fix runtime error when abacusColumns < 5 by filtering all bead highlights
to only include columns that actually exist.

Changes:
- Filter highlightBeads prop to only include valid place values
- Filter stepBeadHighlights to only include valid place values
- Filter customStyles column highlights to only include valid columns
- Add abacusColumns to dependencies of relevant useMemo/useCallback

This prevents accessing undefined column states when rendering with
fewer than 5 columns (e.g., abacusColumns={2} for simple demos).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:16:24 -05:00
semantic-release-bot
ff7b711fe0 chore(release): 4.17.0 [skip ci]
## [4.17.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.16.0...v4.17.0) (2025-10-19)

### Features

* **tutorial:** add dark theme and column control props ([d42f9b2](d42f9b2d9a))

### Styles

* **homepage:** soften tutorial styling for dark theme cohesion ([faaefba](faaefbacff)), closes [#f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f9)
2025-10-19 18:13:26 +00:00
Thomas Hallock
d42f9b2d9a feat(tutorial): add dark theme and column control props
Add `theme` and `abacusColumns` props to TutorialPlayer for better customization:
- theme: 'light' | 'dark' controls all color schemes
- abacusColumns: number controls abacus column count (default 5)

Updated homepage to use:
- abacusColumns={2} for simpler 2+3 demo
- theme="dark" for cohesive integration with dark page design
- Vertical layout with "What You'll Learn" below tutorial

Dark theme styling:
- Transparent dark backgrounds for all containers
- Muted text colors (gray.200-gray.400)
- Subtle borders and shadows
- Removed bright yellow/amber gradients

All changes maintain backward compatibility - defaults to light theme with 5 columns.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 13:12:18 -05:00
Thomas Hallock
faaefbacff style(homepage): soften tutorial styling for dark theme cohesion
Apply visual improvements to homepage tutorial demo only (not tutorial system):
- Hide close (X) button in CoachBar - not needed on homepage
- Soften white backgrounds to light gray (#f9fafb)
- Mute text colors (h2 to gray.800, p to gray.600)
- Add transparency to guidance box (reduced opacity)
- Improve spacing and padding throughout
- Soften all shadows (reduced opacity)
- Mute amber/slate text colors for better dark theme integration

All changes scoped to .homepage-tutorial-demo wrapper via CSS overrides.
Tutorial system remains unchanged.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:59:27 -05:00
semantic-release-bot
8d650c5c52 chore(release): 4.16.0 [skip ci]
## [4.16.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.15.1...v4.16.0) (2025-10-19)

### Features

* **tutorial:** add hideNavigation prop to TutorialPlayer ([79ea52a](79ea52af80))

### Styles

* **homepage:** update tutorial container to match dark theme ([6b017b0](6b017b0fe9))
2025-10-19 17:49:06 +00:00
Thomas Hallock
79ea52af80 feat(tutorial): add hideNavigation prop to TutorialPlayer
Add `hideNavigation` prop to TutorialPlayer component that hides
the header and footer navigation controls, allowing the tutorial
content to be embedded cleanly without navigation chrome.

Perfect for single-step tutorial demos like the homepage.

Changes:
- Add hideNavigation prop to TutorialPlayerProps
- Wrap header section in conditional rendering
- Wrap navigation footer in conditional rendering
- Update homepage to use hideNavigation={true}
- Adjust minHeight when navigation is hidden

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:47:54 -05:00
Thomas Hallock
6b017b0fe9 style(homepage): update tutorial container to match dark theme
Change tutorial container background from bright white to dark
semi-transparent black (rgba(0, 0, 0, 0.4)) with gray border to
match the homepage's dark aesthetic. Improves visual cohesion.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:43:02 -05:00
semantic-release-bot
8f8b1e80db chore(release): 4.15.1 [skip ci]
## [4.15.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.15.0...v4.15.1) (2025-10-19)

### Bug Fixes

* **tutorial:** resolve React hydration error in TutorialPlayer ([c883d9e](c883d9e4c1))

### Code Refactoring

* replace demo component with real TutorialPlayer system ([19b03bc](19b03bc77c))
2025-10-19 17:42:44 +00:00
Thomas Hallock
c883d9e4c1 fix(tutorial): resolve React hydration error in TutorialPlayer
Change <p> tag to <div> tag to fix HTML nesting violation. The
<p> tag was containing <DecompositionWithReasons> which renders
a <div>, causing a hydration error. In HTML, <p> cannot contain
block-level elements like <div>.

Fixed: apps/web/src/components/tutorial/TutorialPlayer.tsx:1386

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:41:33 -05:00
Thomas Hallock
19b03bc77c refactor: replace demo component with real TutorialPlayer system
Replaced the simple FriendsOfFiveDemo component with the actual TutorialPlayer
from the existing tutorial system, showing the "Friends of 5" lesson (2+3=5).

Changes:
- Removed FriendsOfFiveDemo.tsx (redundant custom component)
- Updated homepage to use TutorialPlayer with filtered tutorial steps
- Added TUTORIAL_SYSTEM.md documentation explaining the full tutorial system
- Homepage now demonstrates the real learning system instead of a mock

The tutorial system includes:
- Step-by-step guidance with bead highlighting
- Real-time feedback and validation
- Multi-step instruction support
- Pedagogical decomposition
- Auto-advancement on correct completion
- Full tutorial editor at /tutorial-editor

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:37:49 -05:00
semantic-release-bot
be68af0d56 chore(release): 4.15.0 [skip ci]
## [4.15.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.6...v4.15.0) (2025-10-19)

### Features

* redesign homepage with educational vision and interactive demo ([2f09cb5](2f09cb5539))
2025-10-19 17:32:38 +00:00
Thomas Hallock
2f09cb5539 feat: redesign homepage with educational vision and interactive demo
Redesigned the homepage to communicate the platform's educational
mission: providing a structured, self-directed path to soroban fluency
for students and families.

Key changes:
- New hero section with "Learn → Practice → Play → Master" journey
- Interactive Friends of 5 demo embedded directly on homepage
- Clear sections: Learn by Doing, Available Now, For Kids & Families
- Progression visualization (10 Kyu → Dan levels)
- Honest development status badge
- Removed aspirational language, focused on what's ready now
- Added comprehensive education roadmap documentation

The homepage now serves as both a visual introduction and working
demonstration of the learning system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:31:30 -05:00
semantic-release-bot
aa6cea07df chore(release): 4.14.6 [skip ci]
## [4.14.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.5...v4.14.6) (2025-10-19)

### Bug Fixes

* replace native alerts with inline confirmations in ModerationPanel ([ebe123e](ebe123ed7e))

### Documentation

* add UI style guide documenting no native alerts rule ([9afd3a7](9afd3a7e92))
2025-10-19 17:08:16 +00:00
Thomas Hallock
ebe123ed7e fix: replace native alerts with inline confirmations in ModerationPanel
Removed native browser confirm() dialogs and replaced with React state-based inline confirmations:
- Removed confirm() from handleKick (kicks happen immediately)
- Removed confirm() from handleTransferOwnership
- Added confirmingTransferOwnership state variable
- Added inline confirmation UI with Cancel/Confirm buttons
- Follows pattern documented in UI_STYLE_GUIDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:07:18 -05:00
Thomas Hallock
9afd3a7e92 docs: add UI style guide documenting no native alerts rule
Documented the project's UI pattern requirements:
- Never use native browser dialogs (alert/confirm/prompt)
- Always use inline React-based confirmations
- Included pattern examples and migration checklist
- Referenced ModerationPanel.tsx for real examples

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:07:18 -05:00
semantic-release-bot
efb9c37380 chore(release): 4.14.5 [skip ci]
## [4.14.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.4...v4.14.5) (2025-10-19)

### Bug Fixes

* **rooms:** add real-time ownership transfer updates via WebSocket ([c00cfa3](c00cfa3de0))
2025-10-19 17:02:24 +00:00
Thomas Hallock
c00cfa3de0 fix(rooms): add real-time ownership transfer updates via WebSocket
When ownership is transferred via the moderation modal, both the old
and new host now see the change immediately without requiring a page
reload.

Added missing socket event handler for 'ownership-transferred' event:
- Server already broadcasts event with updated members (route.ts:82)
- Client now listens and updates React Query cache in real-time
- All components using useRoomData() automatically re-render
- Both sessions see host status changes instantly

Fixes issue where ownership transfer required manual page refresh
to see updated host permissions (game selection, moderation access).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 12:01:22 -05:00
semantic-release-bot
da53e084f0 chore(release): 4.14.4 [skip ci]
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)

### Bug Fixes

* **arcade:** add host-only game selection with clear messaging ([22df1b0](22df1b0b66))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](c0680cad0f))
2025-10-19 17:00:53 +00:00
Thomas Hallock
22df1b0b66 fix(arcade): add host-only game selection with clear messaging
Only room hosts can select games. Added clear visual messaging:
- Host: "👑 You're the room host. Select a game to start playing."
- Non-host: " Waiting for [Host Name] to select a game..."
- Error: "⚠️ Only the room host can select a game. Ask [Host] to choose."

Changes:
- Detect host status via currentMember?.isCreator
- Disable game buttons for non-hosts (opacity 0.4, cursor not-allowed)
- Client-side permission check before API call
- Error messages auto-dismiss after 5 seconds
- Error handling in setRoomGame mutation callback

Fixes 403 errors when non-hosts attempt game selection.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
Thomas Hallock
c0680cad0f fix(arcade): add host-only game selection with clear messaging
Non-host room members were getting 403 errors when trying to select games.
Added proper UI restrictions and messaging to clarify only the host can
select games.

**Changes**:

1. **Host Detection**: Check if current user is room creator
   - Find `currentMember` in `roomData.members`
   - Check `isCreator` flag

2. **Visual Restrictions**:
   - Game buttons disabled for non-hosts (opacity: 0.4, cursor: not-allowed)
   - No hover effects when disabled
   - Clear visual feedback

3. **Messaging**:
   - **Host**: "👑 You're the room host. Select a game to start playing."
   - **Non-host**: " Waiting for [Host Name] to select a game..."
   - **Error**: "⚠️ Only the room host can select a game. Ask [Host] to choose."

4. **Error Handling**:
   - Client-side check before API call
   - Server error caught and displayed with host name
   - Auto-dismiss after 5 seconds

**UX Flow**:
- Non-hosts see disabled games with clear "waiting for host" message
- If they somehow click, they get clear error message
- Host sees active games with confirmation they can select

Prevents confusing 403 errors and clarifies room permissions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:59:43 -05:00
semantic-release-bot
0fef1dc9db chore(release): 4.14.3 [skip ci]
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)

### Bug Fixes

* **docker:** add qpdf for PDF linearization and validation ([c92ff39](c92ff3971c))
2025-10-19 16:56:12 +00:00
Thomas Hallock
c92ff3971c fix(docker): add qpdf for PDF linearization and validation
The Python flashcard generator requires qpdf for PDF processing.
Without it, the script exits with code 1 even though it prints
a warning saying it will skip linearization.

Added qpdf to Alpine packages to fix PDF generation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 11:55:02 -05:00
15 changed files with 2455 additions and 571 deletions

View File

@@ -1,3 +1,149 @@
## [4.20.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.2...v4.20.3) (2025-10-19)
### Bug Fixes
* **homepage:** use explicit RGBA colors for Your Journey text ([9c51cc9](https://github.com/antialias/soroban-abacus-flashcards/commit/9c51cc94eec4efcab9c0b9d1190f5b79c0c7d365))
## [4.20.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.1...v4.20.2) (2025-10-19)
### Bug Fixes
* **homepage:** improve text contrast in Your Journey section ([24d1200](https://github.com/antialias/soroban-abacus-flashcards/commit/24d120004dccecc1ce2f08c1b73eec902868fb23))
* **tutorial:** resolve TypeScript errors in TutorialPlayer ([88f57ce](https://github.com/antialias/soroban-abacus-flashcards/commit/88f57ce6df125142d6ea7feec60c475926bd4929))
## [4.20.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.0...v4.20.1) (2025-10-19)
### Bug Fixes
* **homepage:** correct positioning of progression arrows in Your Journey section ([3fff9ef](https://github.com/antialias/soroban-abacus-flashcards/commit/3fff9ef140bf1f462042f8319ed6c5e2a376e4ba))
### Code Refactoring
* **homepage:** move What You'll Learn above tutorial ([ca1c6d8](https://github.com/antialias/soroban-abacus-flashcards/commit/ca1c6d86029c891e019a96ba161e49b08b5be1bf))
## [4.20.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.19.0...v4.20.0) (2025-10-19)
### Features
* **tutorial:** add hideTooltip prop and improve dark mode coaching bar ([1ee25b3](https://github.com/antialias/soroban-abacus-flashcards/commit/1ee25b3dd2f0ee9dd7ed571ba818b7ca5a247f85))
## [4.19.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.18.1...v4.19.0) (2025-10-19)
### Features
* **tutorial:** add fill color support for dark mode column posts and reckoning bar ([2eb3ff3](https://github.com/antialias/soroban-abacus-flashcards/commit/2eb3ff340613301df20bf14f5b461371a27d7f05))
## [4.18.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.18.0...v4.18.1) (2025-10-19)
### Bug Fixes
* **tutorial:** use correct customStyles API for dark mode frame styling ([fdc882c](https://github.com/antialias/soroban-abacus-flashcards/commit/fdc882cb046e3d8835fbca59841e9af5329bcc52))
## [4.18.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.2...v4.18.0) (2025-10-19)
### Features
* **tutorial:** add dark mode styling for coaching bar and abacus frame ([7e2f580](https://github.com/antialias/soroban-abacus-flashcards/commit/7e2f580877af9d21409f427778fa3569c950fcf5))
## [4.17.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.1...v4.17.2) (2025-10-19)
### Bug Fixes
* **tutorial:** correct column index calculation for variable column counts ([bf1ced4](https://github.com/antialias/soroban-abacus-flashcards/commit/bf1ced43f801938b05f01548eea5fe771de1b58f))
## [4.17.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.17.0...v4.17.1) (2025-10-19)
### Bug Fixes
* **tutorial:** filter bead highlights when using fewer columns ([4d906ec](https://github.com/antialias/soroban-abacus-flashcards/commit/4d906ec20e90a9b0b3838f9b8428e0c68992f381))
## [4.17.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.16.0...v4.17.0) (2025-10-19)
### Features
* **tutorial:** add dark theme and column control props ([d42f9b2](https://github.com/antialias/soroban-abacus-flashcards/commit/d42f9b2d9ad630826c55b753dc581c469e8f9083))
### Styles
* **homepage:** soften tutorial styling for dark theme cohesion ([faaefba](https://github.com/antialias/soroban-abacus-flashcards/commit/faaefbacff419b337aa0fac4a101d5106a18c77c)), closes [#f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f9)
## [4.16.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.15.1...v4.16.0) (2025-10-19)
### Features
* **tutorial:** add hideNavigation prop to TutorialPlayer ([79ea52a](https://github.com/antialias/soroban-abacus-flashcards/commit/79ea52af80c8cbb482bbdd87f77caf32ada737ee))
### Styles
* **homepage:** update tutorial container to match dark theme ([6b017b0](https://github.com/antialias/soroban-abacus-flashcards/commit/6b017b0fe92d4277843d9fe2645c22366f219d76))
## [4.15.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.15.0...v4.15.1) (2025-10-19)
### Bug Fixes
* **tutorial:** resolve React hydration error in TutorialPlayer ([c883d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/c883d9e4c1b3a2f52c9d41e3ddce7418399f2649))
### Code Refactoring
* replace demo component with real TutorialPlayer system ([19b03bc](https://github.com/antialias/soroban-abacus-flashcards/commit/19b03bc77c649cf51d7b9a3617417c6ec8229ac7))
## [4.15.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.6...v4.15.0) (2025-10-19)
### Features
* redesign homepage with educational vision and interactive demo ([2f09cb5](https://github.com/antialias/soroban-abacus-flashcards/commit/2f09cb5539f2bb0b8c77359c6f774c3742313e1e))
## [4.14.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.5...v4.14.6) (2025-10-19)
### Bug Fixes
* replace native alerts with inline confirmations in ModerationPanel ([ebe123e](https://github.com/antialias/soroban-abacus-flashcards/commit/ebe123ed7edf24fbc7b8765ed709455a8513d6d5))
### Documentation
* add UI style guide documenting no native alerts rule ([9afd3a7](https://github.com/antialias/soroban-abacus-flashcards/commit/9afd3a7e925fddb76fa587747881b61f7cb077a5))
## [4.14.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.4...v4.14.5) (2025-10-19)
### Bug Fixes
* **rooms:** add real-time ownership transfer updates via WebSocket ([c00cfa3](https://github.com/antialias/soroban-abacus-flashcards/commit/c00cfa3de011720f3399fa340182b347f7e0d456))
## [4.14.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.3...v4.14.4) (2025-10-19)
### Bug Fixes
* **arcade:** add host-only game selection with clear messaging ([22df1b0](https://github.com/antialias/soroban-abacus-flashcards/commit/22df1b0b661efe69fac1a6bd716531c904757412))
* **arcade:** add host-only game selection with clear messaging ([c0680ca](https://github.com/antialias/soroban-abacus-flashcards/commit/c0680cad0fa26af0933e93a06c50317bf443cc7d))
## [4.14.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.2...v4.14.3) (2025-10-19)
### Bug Fixes
* **docker:** add qpdf for PDF linearization and validation ([c92ff39](https://github.com/antialias/soroban-abacus-flashcards/commit/c92ff3971c853e4e55ccd632ff3ee292fcce8315))
## [4.14.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.14.1...v4.14.2) (2025-10-19)

View File

@@ -48,8 +48,8 @@ RUN turbo build --filter=@soroban/web
FROM node:18-alpine AS runner
WORKDIR /app
# 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
# Install Python, pip, build tools for better-sqlite3, Typst, and qpdf (needed at runtime)
RUN apk add --no-cache python3 py3-pip py3-setuptools make g++ typst qpdf
# Create non-root user
RUN addgroup --system --gid 1001 nodejs

View File

@@ -0,0 +1,935 @@
# Soroban Abacus Education Platform - Comprehensive Roadmap
## Vision Statement
**Mission:** Fill the gap in the USA school system by providing a complete, self-directed abacus curriculum that trains students from beginner to mastery using the Japanese kyu/dan ranking system.
**Target Users:**
- Primary: Elementary school students (ages 6-12)
- Secondary: Middle school students and adult learners
- Teachers/Parents: Dashboard for monitoring progress
**Core Experience Principles:**
1. **Integrated Learning Loop:** Tutorial → Practice → Play → Assessment → Progress
2. **Self-Directed:** Simple enough for kids to fire up and start learning independently
3. **Gamified Progression:** Games reinforce lessons, feel like play but teach skills
4. **Physical + Virtual:** Support both physical abacus and AbacusReact component
5. **Mastery-Based:** Students advance through clear skill levels with certification
---
## Current State Assessment
### ✅ What We Have (Well-Built)
**1. Interactive Abacus Component (AbacusReact)**
- Highly polished, production-ready
- Excellent pedagogical features (bead highlighting, direction arrows, tooltips)
- Multiple color schemes and accessibility options
- Interactive and display-only modes
- **Rating: 95% Complete**
**2. Game System (4 Games)**
- Memory Lightning (memorization skills)
- Matching Pairs Battle (pattern recognition, complements)
- Card Sorting (visual literacy, ordering)
- Complement Race (speed calculation, friends-of-5/10)
- Type-safe architecture with game SDK
- Multiplayer and spectator modes
- **Rating: 80% Complete** (games exist but need curriculum integration)
**3. Tutorial Infrastructure**
- Tutorial player with step-based guidance
- Tutorial editor for content creation
- Bead highlighting system for instruction
- Event tracking and progress monitoring
- **Rating: 70% Complete** (infrastructure exists but lacks content)
**4. Real-time Multiplayer**
- Socket.IO integration
- Room-based architecture
- State synchronization
- **Rating: 90% Complete**
**5. Flashcard Generator**
- PDF/PNG/SVG export
- Customizable layouts and themes
- **Rating: 100% Complete**
### ⚠️ What We Have (Partially Built)
**1. Progress Tracking**
- Basic user stats (games played, wins, accuracy)
- No skill-level tracking
- No tutorial completion tracking
- No assessment history
- **Rating: 30% Complete**
**2. Tutorial Content**
- One example tutorial (GuidedAdditionTutorial)
- Type system for tutorials defined
- No comprehensive curriculum
- **Rating: 15% Complete**
**3. Assessment System**
- Per-game scoring exists
- Achievement system exists
- No formal tests or certification
- No placement tests
- **Rating: 25% Complete**
### ❌ What We're Missing (Critical Gaps)
**1. Kyu/Dan Ranking System** - 0% Complete
**2. Structured Curriculum** - 5% Complete
**3. Adaptive Learning** - 0% Complete
**4. Student Dashboard** - 0% Complete
**5. Teacher/Parent Dashboard** - 0% Complete
**6. Formal Assessment/Testing** - 0% Complete
**7. Learning Path Sequencing** - 0% Complete
**8. Content Library** - 10% Complete
---
## Kyu/Dan Level System (Japanese Abacus Standard)
### Beginner Levels (Kyu)
**10 Kyu - "First Steps"**
- Age: 6-7 years
- Skills: Basic bead manipulation, numbers 1-10
- Curriculum: Recognize and set numbers on abacus, understand place value
- Assessment: Set numbers 1-99 correctly, basic addition single digits
- Games: Card Sorting (visual recognition), Memory Lightning (basic)
**9 Kyu - "Number Explorer"**
- Skills: Addition/subtraction with no carry (1-9)
- Curriculum: Friends of 5 concept introduction
- Assessment: 20 problems, 2-digit addition/subtraction, no carry, 80% accuracy
- Games: Complement Race (practice mode), Matching Pairs (numerals)
**8 Kyu - "Complement Apprentice"**
- Skills: Friends of 5 mastery, introduction to friends of 10
- Curriculum: All combinations that make 5, carry concepts
- Assessment: 30 problems including carries using friends of 5, 85% accuracy
- Games: Complement Race (friends-5 sprint), Matching Pairs (complement pairs)
**7 Kyu - "Addition Warrior"**
- Skills: Friends of 10 mastery, 2-digit addition/subtraction with carries
- Curriculum: All combinations that make 10, mixed complement strategies
- Assessment: 40 problems, 2-3 digit calculations, mixed operations, 85% accuracy
- Games: Complement Race (friends-10 sprint), All games at medium difficulty
**6 Kyu - "Speed Calculator"**
- Skills: Multi-digit addition/subtraction (3-4 digits), speed emphasis
- Curriculum: Chain calculations, mental imagery beginning
- Assessment: 50 problems, 3-4 digits, 3 minutes time limit, 90% accuracy
- Games: Complement Race (survival mode), Memory Lightning (medium)
**5 Kyu - "Multiplication Initiate"**
- Skills: Single-digit multiplication (1-5)
- Curriculum: Multiplication tables 1-5, abacus multiplication method
- Assessment: 30 multiplication problems, 40 add/subtract problems, 90% accuracy
- Games: All games at hard difficulty
**4 Kyu - "Multiplication Master"**
- Skills: Full multiplication tables (1-9), 2-digit × 1-digit
- Curriculum: All multiplication patterns, division introduction
- Assessment: 40 multiplication, 20 division, 40 add/subtract, 90% accuracy
**3 Kyu - "Division Explorer"**
- Skills: Division mastery (2-digit ÷ 1-digit), mixed operations
- Curriculum: Division algorithm, remainders, mixed problem solving
- Assessment: 100 mixed problems in 10 minutes, 92% accuracy
**2 Kyu - "Advanced Operator"**
- Skills: Multi-digit multiplication/division, decimals introduction
- Curriculum: 3-digit × 2-digit, decimals, percentages
- Assessment: 120 mixed problems including decimals, 10 minutes, 93% accuracy
**1 Kyu - "Pre-Mastery"**
- Skills: Decimal operations, fractions, complex multi-step problems
- Curriculum: Real-world applications, word problems
- Assessment: 150 mixed problems, 10 minutes, 95% accuracy
- Mental calculation ability without physical abacus
### Master Levels (Dan)
**1 Dan - "Shodan" (First Degree)**
- Skills: Mental imagery without abacus, complex calculations
- Assessment: 200 mixed problems, 10 minutes, 96% accuracy
- Mental arithmetic certification
**2 Dan - "Nidan"**
- Skills: Advanced mental calculation, speed competitions
- Assessment: 250 problems, 10 minutes, 97% accuracy
**3 Dan - "Sandan"**
- Skills: Championship-level speed and accuracy
- Assessment: 300 problems, 10 minutes, 98% accuracy
**4-10 Dan** - Expert/Master levels with increasing complexity
---
## Integrated Learning Experience Design
### The Core Loop (Per Skill/Concept)
```
1. ASSESS → Placement test determines current level
2. LEARN → Tutorial teaches new concept
3. PRACTICE → Guided exercises with immediate feedback
4. PLAY → Games reinforce the skill in fun context
5. TEST → Formal assessment for mastery certification
6. ADVANCE → Unlock next level, update progress
```
### Example: Teaching "Friends of 5"
**1. Assessment (Placement)**
- Quick quiz: "Can you add 3 + 4 using the abacus?"
- Result: Student struggles → Assign Friends of 5 tutorial
**2. Learn (Tutorial)**
- Interactive tutorial: "Friends of 5"
- Steps:
1. Show that 5 = 1+4, 2+3, 3+2, 4+1
2. Demonstrate on abacus: setting 3, adding 2 to make 5
3. Explain heaven bead (top) = 5, earth beads (bottom) = 1 each
4. Interactive: Student sets 3, adds 2 using heaven bead
5. Practice all combinations
**3. Practice (Structured Exercises)**
- 20 problems: Set number, add its friend
- Real-time feedback on bead movements
- Hints available: "Use the heaven bead!"
- Must achieve 90% accuracy to proceed
**4. Play (Game Reinforcement)**
- Complement Race: Friends-5 mode
- Matching Pairs: Match numbers that make 5
- Makes practice feel like play
**5. Test (Formal Assessment)**
- 30 problems mixing friends-5 with previous skills
- Timed: 5 minutes
- Must achieve 85% to certify skill
- Can retake after reviewing mistakes
**6. Advance (Progress Update)**
- Friends of 5 skill marked as "Mastered"
- Unlock: Friends of 10 tutorial
- Update skill matrix
- Award achievement badge
---
## Detailed Curriculum Structure
### Curriculum Database Schema
```typescript
// Skill taxonomy
enum SkillCategory {
NUMBER_SENSE = 'number-sense',
ADDITION = 'addition',
SUBTRACTION = 'subtraction',
MULTIPLICATION = 'multiplication',
DIVISION = 'division',
MENTAL_CALC = 'mental-calculation',
COMPLEMENTS = 'complements',
SPEED = 'speed',
ACCURACY = 'accuracy'
}
// Individual skill (atomic unit)
interface Skill {
id: string
name: string
category: SkillCategory
kyuLevel: number // Which kyu level this skill belongs to
prerequisiteSkills: string[] // Must master these first
description: string
estimatedPracticeTime: number // minutes
}
// Learning module (collection of related skills)
interface Module {
id: string
title: string
kyuLevel: number
description: string
skills: string[] // Skill IDs
estimatedCompletionTime: number // hours
sequence: number // Order within kyu level
}
// Tutorial (teaches one or more skills)
interface Tutorial {
id: string
skillIds: string[]
moduleId: string
type: 'interactive' | 'video' | 'reading'
content: TutorialStep[]
estimatedDuration: number
}
// Practice set (reinforces skills)
interface PracticeSet {
id: string
skillIds: string[]
problemCount: number
timeLimit?: number
passingAccuracy: number
difficulty: 'easy' | 'medium' | 'hard'
}
// Game mapping (which games teach which skills)
interface GameSkillMapping {
gameId: string
skillIds: string[]
difficulty: string
recommendedKyuRange: [number, number]
}
// Assessment (formal test)
interface Assessment {
id: string
type: 'placement' | 'skill-check' | 'kyu-certification'
kyuLevel?: number
skillIds: string[]
problemCount: number
timeLimit: number
passingAccuracy: number
}
```
### Sample Curriculum Map
**10 Kyu Module Sequence:**
1. **Module 1: "Introduction to Abacus" (Week 1)**
- Skill: Understand abacus structure
- Skill: Recognize place values (ones, tens, hundreds)
- Tutorial: "What is an Abacus?"
- Tutorial: "Parts of the Abacus"
- Practice: Set numbers 1-10
- Game: Card Sorting (visual recognition)
2. **Module 2: "Setting Numbers" (Week 2)**
- Skill: Set single-digit numbers (1-9)
- Skill: Set two-digit numbers (10-99)
- Tutorial: "Setting Numbers on Abacus"
- Practice: 50 number-setting exercises
- Game: Memory Lightning (set and remember)
3. **Module 3: "Basic Addition" (Week 3-4)**
- Skill: Add single digits without carry (1+1 through 4+4)
- Tutorial: "Simple Addition"
- Practice: 100 addition problems
- Game: Complement Race (practice mode)
- Assessment: 10 Kyu Certification Test
**9 Kyu Module Sequence:**
1. **Module 4: "Friends of 5 - Introduction" (Week 5)**
- Skill: Recognize pairs that make 5
- Skill: Add using heaven bead (5 bead)
- Tutorial: "Friends of 5 - Part 1"
- Practice: Pattern recognition exercises
- Game: Matching Pairs (complement mode)
2. **Module 5: "Friends of 5 - Application" (Week 6-7)**
- Skill: Add crossing 5 (e.g., 3+4, 2+5)
- Tutorial: "Friends of 5 - Part 2"
- Practice: 200 problems with friends of 5
- Game: Complement Race (friends-5 mode)
- Assessment: 9 Kyu Certification Test
... (Continue through all kyu levels)
---
## Implementation Roadmap
### Phase 1: Foundation (Months 1-3) - "MVP for 10-9 Kyu"
**Goal:** Students can learn and certify 10 Kyu and 9 Kyu levels
**Database Schema Updates:**
- [ ] Create `skills` table
- [ ] Create `modules` table
- [ ] Create `curriculum_tutorials` table (links tutorials to skills)
- [ ] Create `curriculum_practice_sets` table
- [ ] Create `curriculum_assessments` table
- [ ] Create `user_progress` table
- Fields: userId, skillId, status (not_started, in_progress, mastered), attempts, bestScore, lastAttemptAt
- [ ] Create `user_skill_history` table (track all practice attempts)
- [ ] Create `user_assessments` table (formal test results)
- [ ] Create `user_kyu_levels` table
- Fields: userId, currentKyu, currentDan, certifiedAt, expiresAt
- [ ] Extend `user_stats` table: add `currentKyuLevel`, `currentDanLevel`, `skillsMastered`
**Tutorial Content Creation:**
- [ ] 10 Kyu tutorials (5 tutorials):
1. Introduction to Abacus
2. Understanding Place Value
3. Setting Numbers 1-99
4. Basic Addition (single digit, no carry)
5. Basic Subtraction (single digit, no borrow)
- [ ] 9 Kyu tutorials (3 tutorials):
1. Friends of 5 - Concept
2. Friends of 5 - Addition
3. Friends of 5 - Subtraction
**Practice Sets:**
- [ ] Build practice set generator for each skill
- [ ] Implement immediate feedback system
- [ ] Add hint system for common mistakes
- [ ] Track accuracy and time per problem
**Assessment System:**
- [ ] Build placement test component (determines starting level)
- [ ] Build skill-check test component (practice test before certification)
- [ ] Build kyu certification test component (formal test)
- [ ] Implement grading engine
- [ ] Create detailed results/feedback page
- [ ] Allow test retakes with review of mistakes
**Game Integration:**
- [ ] Map existing games to skills
- Memory Lightning → Number recognition, memory
- Card Sorting → Visual pattern recognition, ordering
- Matching Pairs → Complements, pattern matching
- Complement Race → Friends-5, Friends-10, speed
- [ ] Add skill-based game recommendations
- [ ] Track game performance per skill
**Student Dashboard:**
- [ ] Create dashboard showing:
- Current kyu level
- Skills mastered / in progress / locked
- Next recommended activity
- Recent achievements
- Progress bar toward next kyu level
- [ ] Implement simple, kid-friendly UI
- [ ] Add celebratory animations for milestones
**Core User Flow:**
- [ ] Onboarding: Placement test → Assign kyu level
- [ ] Home: Dashboard shows next recommended activity
- [ ] Click "Start Learning" → Next tutorial
- [ ] Complete tutorial → Practice exercises
- [ ] Complete practice → Game suggestion
- [ ] Master all module skills → Unlock certification test
- [ ] Pass certification → Advance to next kyu level
- [ ] Celebration and badge award
**Deliverables:**
- Students can complete 10 Kyu and 9 Kyu
- ~8 tutorials
- ~10 skills defined
- Placement test + 2 certification tests
- Student dashboard
- Progress tracking fully functional
---
### Phase 2: Core Curriculum (Months 4-8) - "8 Kyu through 5 Kyu"
**Goal:** Complete beginner curriculum through multiplication introduction
**Content Creation:**
- [ ] 8 Kyu: Friends of 10 tutorials and practice (4 weeks)
- [ ] 7 Kyu: Mixed complements, 2-digit operations (4 weeks)
- [ ] 6 Kyu: Multi-digit, speed training (6 weeks)
- [ ] 5 Kyu: Multiplication introduction, tables 1-5 (8 weeks)
- Total: ~40 tutorials, ~30 skills
**Enhanced Features:**
- [ ] Adaptive difficulty in practice sets (adjusts based on performance)
- [ ] Spaced repetition system (review mastered skills periodically)
- [ ] Daily recommended practice (10-15 min sessions)
- [ ] Streaks and habit formation
- [ ] Peer comparison (anonymous, optional)
**New Games:**
- [ ] Multiplication tables game
- [ ] Speed drill game (flash calculation)
- [ ] Mental math game (visualization without physical abacus)
**Parent/Teacher Dashboard:**
- [ ] View student progress
- [ ] See time spent learning
- [ ] Review test results
- [ ] Assign specific modules or skills
- [ ] Generate progress reports
**Gamification Enhancements:**
- [ ] Achievement badges for milestones
- [ ] Experience points (XP) system
- [ ] Level-up animations
- [ ] Customizable avatars (unlocked via achievements)
- [ ] Virtual rewards (stickers, themes)
**Deliverables:**
- Complete 8-5 Kyu curriculum
- ~50 total tutorials (cumulative)
- ~40 total skills (cumulative)
- Parent/teacher dashboard
- 2-3 new games
- Enhanced gamification
---
### Phase 3: Advanced Skills (Months 9-14) - "4 Kyu through 1 Kyu"
**Goal:** Advanced operations, real-world applications, mental calculation
**Content Creation:**
- [ ] 4 Kyu: Full multiplication, division introduction (8 weeks)
- [ ] 3 Kyu: Division mastery, mixed operations (8 weeks)
- [ ] 2 Kyu: Decimals, percentages (10 weeks)
- [ ] 1 Kyu: Fractions, word problems, mental calculation (12 weeks)
- Total: ~60 additional tutorials, ~40 additional skills
**Mental Calculation Training:**
- [ ] Visualization exercises (see abacus in mind)
- [ ] Flash anzan (rapid mental calculation)
- [ ] Mental calculation games
- [ ] Transition from physical to mental abacus
**Real-World Applications:**
- [ ] Shopping math (money, change, discounts)
- [ ] Measurement conversions
- [ ] Time calculations
- [ ] Real-world word problems
**Competition Features:**
- [ ] Speed competitions (leaderboards)
- [ ] Accuracy challenges
- [ ] Weekly tournaments
- [ ] Regional/global rankings (optional)
**AI Tutor Assistant:**
- [ ] Smart hints during practice
- [ ] Personalized learning paths
- [ ] Concept explanations on demand
- [ ] Answer specific questions ("Why do I use friends of 5 here?")
**Deliverables:**
- Complete 4-1 Kyu curriculum
- ~110 total tutorials (cumulative)
- ~80 total skills (cumulative)
- Mental calculation training
- AI assistant
- Competition system
---
### Phase 4: Mastery Levels (Months 15-18) - "Dan Levels"
**Goal:** Championship-level speed and accuracy, mental calculation mastery
**Content Creation:**
- [ ] Dan level certification tests
- [ ] Advanced mental calculation curriculum
- [ ] Championship preparation materials
- [ ] Expert-level problem sets
**Advanced Features:**
- [ ] Customized training plans for dan levels
- [ ] Video lessons from expert abacus users
- [ ] Community forum for advanced learners
- [ ] Virtual competitions
- [ ] Certification/diploma generation (printable)
**Integration with Standards:**
- [ ] Align with League of Soroban of Americas standards
- [ ] Japan Abacus Committee certification mapping
- [ ] International competition preparation
**Deliverables:**
- 1-10 Dan curriculum
- Certification system
- Community features
- Championship training
---
### Phase 5: Ecosystem (Months 18+) - "Complete Platform"
**Content Management System:**
- [ ] Tutorial builder UI (create without code)
- [ ] Content versioning
- [ ] Community-contributed content (vetted)
- [ ] Multilingual support (Spanish, Japanese, Hindi)
**Classroom Features:**
- [ ] Teacher creates classes
- [ ] Bulk student enrollment
- [ ] Class-wide assignments
- [ ] Class leaderboards
- [ ] Live teaching mode (project for class)
**Analytics & Insights:**
- [ ] Student learning velocity
- [ ] Skill gap analysis
- [ ] Predictive success modeling
- [ ] Recommendations engine
- [ ] Export data for research
**Mobile App:**
- [ ] iOS and React Native apps
- [ ] Offline mode
- [ ] Sync across devices
**Integrations:**
- [ ] Google Classroom
- [ ] Canvas LMS
- [ ] Schoology
- [ ] Export to SIS systems
**Advanced Gamification:**
- [ ] Story mode (learning quest)
- [ ] Cooperative challenges
- [ ] Guild/team system
- [ ] Seasonal events
---
## Success Metrics
### Student Engagement
- **Daily Active Users (DAU):** Target 40% of registered students
- **Weekly Active Users (WAU):** Target 70% of registered students
- **Average session time:** 20-30 minutes
- **Completion rate per module:** >80%
- **Retention (30-day):** >60%
- **Streak length:** Average 7+ days
### Learning Outcomes
- **Certification pass rate:** >70% on first attempt per kyu level
- **Skill mastery rate:** >85% accuracy on mastered skills after 30 days
- **Time to mastery:** Track average time per kyu level
- **Progression velocity:** Students advance 1 kyu level per 4-8 weeks (varies by level)
### Content Quality
- **Tutorial completion rate:** >90%
- **Practice set completion rate:** >85%
- **Game play rate:** >60% of students play games weekly
- **Assessment completion rate:** >75%
### Platform Health
- **System uptime:** >99.5%
- **Load time:** <2 seconds
- **Error rate:** <0.1%
### Business/Growth
- **Monthly signups:** Track growth month-over-month
- **Paid conversion** (if applicable): Target 10-20%
- **Teacher/school adoption:** Track institutional users
- **Net Promoter Score (NPS):** Target >50
---
## Technical Architecture Changes
### Database Changes Priority
**Immediate (Phase 1):**
```sql
-- Skills and curriculum structure
CREATE TABLE skills (...)
CREATE TABLE modules (...)
CREATE TABLE skill_prerequisites (...)
-- Tutorial and practice content
CREATE TABLE tutorial_content (...)
CREATE TABLE practice_sets (...)
CREATE TABLE assessments (...)
-- User progress tracking
CREATE TABLE user_progress (...)
CREATE TABLE user_skill_history (...)
CREATE TABLE user_assessments (...)
CREATE TABLE user_kyu_levels (...)
-- Game-skill mapping
CREATE TABLE game_skill_mappings (...)
```
**Phase 2:**
- Add spaced repetition tables
- Achievement tracking enhancements
- Peer comparison data
**Phase 3:**
- Mental calculation tracking
- Competition results
- AI tutor interaction logs
### API Endpoints Needed
**Progress & Skills:**
- `GET /api/student/progress` - Current kyu level, skills, next steps
- `GET /api/student/skills/:skillId` - Skill details and progress
- `POST /api/student/skills/:skillId/practice` - Record practice attempt
- `GET /api/student/dashboard` - Dashboard data
**Curriculum:**
- `GET /api/curriculum/kyu/:level` - All modules for kyu level
- `GET /api/curriculum/modules/:moduleId` - Module details
- `GET /api/curriculum/tutorials/:tutorialId` - Tutorial content
- `GET /api/curriculum/next` - Next recommended activity
**Assessments:**
- `POST /api/assessments/placement` - Take placement test
- `POST /api/assessments/skill-check/:skillId` - Practice test
- `POST /api/assessments/certification/:kyuLevel` - Certification test
- `POST /api/assessments/:assessmentId/submit` - Submit answers
- `GET /api/assessments/:assessmentId/results` - Get results
**Games:**
- `GET /api/games/recommended` - Games for current skills
- `POST /api/games/:gameId/result` - Log game completion
- `GET /api/games/:gameId/skills` - Which skills this game teaches
**Teacher/Parent:**
- `GET /api/teacher/students` - List of students
- `GET /api/teacher/students/:studentId/progress` - Student progress
- `POST /api/teacher/assignments` - Create assignment
### Component Architecture
**New Components Needed:**
```
/src/components/curriculum/
- SkillCard.tsx - Display skill with progress
- ModuleCard.tsx - Module overview with skills
- CurriculumMap.tsx - Visual map of curriculum
- SkillTree.tsx - Dependency graph visualization
/src/components/practice/
- PracticeSession.tsx - Practice exercise UI
- ProblemDisplay.tsx - Show problem to solve
- AnswerInput.tsx - Accept answer (with abacus)
- FeedbackDisplay.tsx - Show correctness and hints
/src/components/assessment/
- PlacementTest.tsx - Initial assessment
- SkillCheckTest.tsx - Practice test
- CertificationTest.tsx - Formal kyu test
- TestResults.tsx - Detailed results page
/src/components/dashboard/
- StudentDashboard.tsx - Main dashboard
- ProgressOverview.tsx - Current level and progress
- NextActivity.tsx - Recommended next step
- AchievementShowcase.tsx - Badges and milestones
- ActivityFeed.tsx - Recent activity
/src/components/teacher/
- TeacherDashboard.tsx
- StudentRoster.tsx
- StudentDetail.tsx
- AssignmentCreator.tsx
```
---
## Content Creation Process
### Tutorial Creation Workflow
1. **Define Skill:** What specific skill does this teach?
2. **Outline Steps:** Break down into 5-10 learning steps
3. **Create Interactive Elements:**
- Which beads to highlight
- What movements to demonstrate
- Example problems
4. **Add Explanations:** Clear, kid-friendly language
5. **Test with Students:** Iterate based on confusion points
6. **Publish:** Add to curriculum map
### Tutorial Template
```typescript
{
id: "friends-of-5-intro",
title: "Friends of 5 - Introduction",
skillIds: ["friends-5-recognition"],
kyuLevel: 9,
estimatedDuration: 15,
steps: [
{
instruction: "Let's learn about friends of 5! These are pairs of numbers that add up to 5.",
problem: null,
highlighting: [],
explanation: "When you add friends together, they always make 5!"
},
{
instruction: "1 and 4 are friends! See how 1 + 4 = 5?",
problem: { operation: 'add', terms: [1, 4] },
highlighting: [
{ column: 0, value: 1, direction: 'activate' },
{ column: 0, value: 4, direction: 'up', step: 2 }
],
explanation: "We set 1 earth bead, then add 4 more by using the heaven bead (5) and removing 1."
},
// ... more steps
]
}
```
### Practice Set Template
```typescript
{
id: "friends-5-practice-1",
skillIds: ["friends-5-recognition", "friends-5-addition"],
problemCount: 20,
timeLimit: 300, // 5 minutes
passingAccuracy: 0.85,
problemGenerator: {
type: 'addition',
numberRange: [1, 9],
requiresFriends5: true,
maxTerms: 2
}
}
```
---
## File Structure for Curriculum
```
/apps/web/src/curriculum/
/schema/
- skills.ts (skill definitions)
- modules.ts (module definitions)
- assessments.ts (test definitions)
/content/
/10-kyu/
- module-1-intro.ts
- module-2-setting-numbers.ts
- module-3-basic-addition.ts
/9-kyu/
- module-4-friends-5-intro.ts
- module-5-friends-5-application.ts
/8-kyu/
... and so on
/tutorials/
/10-kyu/
- intro-to-abacus.ts
- place-value.ts
- setting-numbers.ts
- basic-addition.ts
- basic-subtraction.ts
/9-kyu/
- friends-5-concept.ts
- friends-5-addition.ts
- friends-5-subtraction.ts
... and so on
/practice/
/10-kyu/
- number-setting-practice.ts
- basic-addition-practice.ts
/9-kyu/
- friends-5-practice.ts
... and so on
/assessments/
- placement-test.ts
- 10-kyu-certification.ts
- 9-kyu-certification.ts
... and so on
- curriculum-map.ts (master curriculum definition)
- game-skill-mappings.ts (which games teach which skills)
```
---
## Next Immediate Steps
### Week 1: Database Schema Design
- [ ] Design complete schema for Phase 1
- [ ] Write migration scripts
- [ ] Document schema decisions
- [ ] Review with stakeholders
### Week 2-3: Content Planning
- [ ] Write detailed 10 Kyu curriculum outline
- [ ] Write detailed 9 Kyu curriculum outline
- [ ] Define all skills for 10-9 Kyu
- [ ] Map skills to existing games
### Week 4-5: Tutorial Content Creation
- [ ] Write 5 tutorials for 10 Kyu
- [ ] Write 3 tutorials for 9 Kyu
- [ ] Create interactive steps with highlighting
- [ ] Add kid-friendly explanations
### Week 6-7: Assessment System Build
- [ ] Build assessment component UI
- [ ] Implement grading engine
- [ ] Create placement test (20 problems)
- [ ] Create 10 Kyu certification test (30 problems)
- [ ] Create 9 Kyu certification test (40 problems)
### Week 8-9: Practice System
- [ ] Build practice session component
- [ ] Implement problem generator for each skill
- [ ] Add immediate feedback system
- [ ] Create hint system
### Week 10-11: Student Dashboard
- [ ] Design dashboard UI (kid-friendly)
- [ ] Build progress visualization
- [ ] Implement "next recommended activity" logic
- [ ] Add achievement display
### Week 12: Integration & Testing
- [ ] Connect all pieces: tutorials → practice → games → assessment
- [ ] Test complete user flow
- [ ] User testing with kids
- [ ] Iterate based on feedback
---
## Questions to Resolve
1. **Certification Validity:** Should kyu certifications expire? (Traditional abacus schools: no expiration)
2. **Retake Policy:** How many times can student retake certification test? (Suggest: unlimited, but must wait 24 hours)
3. **Grading Standards:** Strict adherence to Japanese standards or adjust for USA context?
4. **Physical Abacus:** Should we require physical abacus for certain levels? (Recommend: optional but encouraged)
5. **Age Restrictions:** Any minimum age? (Suggest: 6+ with parent/teacher supervision)
6. **Teacher Accounts:** Free for teachers? (Recommend: yes, free for teachers)
7. **Pricing Model:** Free tier + premium? School licensing? (TBD)
8. **Content Licensing:** Will curriculum be open source or proprietary? (Recommend: proprietary but allow teacher customization)
9. **Accessibility:** WCAG compliance level? (Recommend: AA minimum)
10. **Data Privacy:** COPPA compliance for users under 13? (Required: yes, must be compliant)
---
## Conclusion
This roadmap provides a clear path from current state (scattered features) to target state (complete educational platform). The phased approach allows incremental delivery while maintaining focus on core learning experience.
**Estimated Timeline:**
- Phase 1 (10-9 Kyu MVP): 3 months
- Phase 2 (8-5 Kyu): 5 months
- Phase 3 (4-1 Kyu): 6 months
- Phase 4 (Dan levels): 3 months
- Phase 5 (Ecosystem): Ongoing
**Total to Complete Platform:** ~17 months for core curriculum, then continuous improvement
**Priority:** Start with Phase 1 to prove the concept, get student feedback, and validate the learning loop before building the full system.

View File

@@ -0,0 +1,222 @@
# Tutorial System Documentation
## Overview
The tutorial system is a sophisticated interactive learning platform for teaching soroban abacus concepts. It features step-by-step guidance, bead highlighting, pedagogical decomposition, and progress tracking.
## Key Components
### 1. TutorialPlayer (`/src/components/tutorial/TutorialPlayer.tsx`)
The main tutorial playback component that:
- Displays tutorial steps progressively
- Highlights specific beads users should interact with
- Provides real-time feedback and tooltips
- Shows step-by-step instructions for multi-step operations
- Tracks user progress through the tutorial
- Auto-advances to next step on correct completion
**Key Features:**
- **Bead Highlighting**: Visual indicators showing which beads to manipulate
- **Step Progress**: Shows current step out of total steps
- **Error Feedback**: Provides hints when user makes mistakes
- **Multi-Step Support**: Breaks complex operations into sequential sub-steps
- **Pedagogical Decomposition**: Explains the "why" behind each operation
### 2. TutorialEditor (`/src/components/tutorial/TutorialEditor.tsx`)
A full-featured editor for creating and editing tutorials:
- Visual step editor
- Bead highlight configuration
- Multi-step instruction editor
- Live preview
- Import/export functionality
- Access control
**Editor URL:** `/tutorial-editor`
### 3. Tutorial Data Structure (`/src/types/tutorial.ts`)
```typescript
interface Tutorial {
id: string
title: string
description: string
category: string
difficulty: 'beginner' | 'intermediate' | 'advanced'
estimatedDuration: number // minutes
steps: TutorialStep[]
tags: string[]
author: string
version: string
createdAt: Date
updatedAt: Date
isPublished: boolean
}
interface TutorialStep {
id: string
title: string
problem: string // e.g. "2 + 3"
description: string // User-facing explanation
startValue: number // Initial abacus value
targetValue: number // Goal value
expectedAction: 'add' | 'remove' | 'multi-step'
actionDescription: string
// Bead highlighting
highlightBeads?: Array<{
placeValue: number // 0=ones, 1=tens, etc.
beadType: 'heaven' | 'earth'
position?: number // For earth beads: 0-3
}>
// Progressive step highlighting
stepBeadHighlights?: Array<{
placeValue: number
beadType: 'heaven' | 'earth'
position?: number
stepIndex: number // Which instruction step
direction: 'up' | 'down' | 'activate' | 'deactivate'
order?: number // Order within step
}>
totalSteps?: number // For multi-step operations
multiStepInstructions?: string[] // Sequential instructions
// Tooltips and guidance
tooltip: {
content: string // Short title
explanation: string // Detailed explanation
}
}
```
### 4. Tutorial Converter (`/src/utils/tutorialConverter.ts`)
Utility that converts the original `GuidedAdditionTutorial` data into the new tutorial format:
- `guidedAdditionSteps`: Array of tutorial steps from basic addition to complements
- `convertGuidedAdditionTutorial()`: Converts to Tutorial object
- `getTutorialForEditor()`: Main export used in the app
**Current Tutorial Steps:**
1. Basic Addition (0+1, 1+1, 2+1, 3+1)
2. Heaven Bead Introduction (0+5, 5+1)
3. Five Complements (3+4, 2+3 using 5-complement method)
4. Complex Operations (6+2, 7+4 with carrying)
### 5. Supporting Utilities
**`/src/utils/abacusInstructionGenerator.ts`**
- Automatically generates step-by-step instructions from start/target values
- Creates bead highlight data
- Determines movement directions
**`/src/utils/beadDiff.ts`**
- Calculates differences between abacus states
- Generates visual feedback tooltips
- Explains what changed and why
## Usage Examples
### Basic Usage in a Page
```typescript
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
export function MyPage() {
return (
<TutorialPlayer
tutorial={getTutorialForEditor()}
isDebugMode={false}
showDebugPanel={false}
/>
)
}
```
### Using a Subset of Steps
```typescript
import { getTutorialForEditor } from '@/utils/tutorialConverter'
const fullTutorial = getTutorialForEditor()
// Extract specific steps (e.g., just "Friends of 5")
const friendsOf5Tutorial = {
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
steps: fullTutorial.steps.filter(step =>
step.id === 'complement-2' // The 2+3=5 step
)
}
return <TutorialPlayer tutorial={friendsOf5Tutorial} />
```
### Creating a Custom Tutorial
```typescript
const customTutorial: Tutorial = {
id: 'my-tutorial',
title: 'My Custom Tutorial',
description: 'Learning something new',
category: 'Custom',
difficulty: 'beginner',
estimatedDuration: 5,
steps: [
{
id: 'step-1',
title: 'Add 2',
problem: '0 + 2',
description: 'Move two earth beads up',
startValue: 0,
targetValue: 2,
expectedAction: 'add',
actionDescription: 'Add two earth beads',
highlightBeads: [
{ placeValue: 0, beadType: 'earth', position: 0 },
{ placeValue: 0, beadType: 'earth', position: 1 }
],
tooltip: {
content: 'Adding 2',
explanation: 'Push two earth beads up to represent 2'
}
}
],
tags: ['custom'],
author: 'Me',
version: '1.0.0',
createdAt: new Date(),
updatedAt: new Date(),
isPublished: true
}
```
## Current Implementation Locations
**Live Tutorials:**
- `/guide` - Second tab "Arithmetic Operations" contains the full guided addition tutorial
**Editor:**
- `/tutorial-editor` - Full tutorial editing interface
**Storybook:**
- Multiple tutorial stories in `/src/components/tutorial/*.stories.tsx`
## Key Design Principles
1. **Progressive Disclosure**: Users see one step at a time
2. **Immediate Feedback**: Real-time validation and hints
3. **Visual Guidance**: Bead highlighting shows exactly what to do
4. **Pedagogical Decomposition**: Multi-step operations broken into atomic actions
5. **Auto-Advancement**: Successful completion automatically moves to next step
6. **Error Recovery**: Helpful hints when user makes mistakes
## Notes
- The tutorial system uses the existing `AbacusReact` component
- Tutorials can be created/edited through the TutorialEditor
- Tutorial data can be exported/imported as JSON
- The system supports both single-step and multi-step operations
- Bead highlighting uses place value indexing (0=ones, 1=tens, etc.)

View File

@@ -0,0 +1,94 @@
# UI Style Guide
## Confirmations and Dialogs
**NEVER use native browser dialogs:**
-`alert()`
-`confirm()`
-`prompt()`
**ALWAYS use inline React-based confirmations:**
- Show confirmation UI in-place using React state
- Provide Cancel and Confirm buttons
- Use descriptive warning messages with appropriate emoji (⚠️)
- Follow the Panda CSS styling system
- Match the visual style of the surrounding UI
### Pattern: Inline Confirmation
```typescript
const [confirming, setConfirming] = useState(false)
{!confirming ? (
<button onClick={() => setConfirming(true)}>
Delete Item
</button>
) : (
<div>
<div style={{ /* warning styling */ }}>
Are you sure you want to delete this item?
</div>
<div style={{ /* description styling */ }}>
This action cannot be undone.
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setConfirming(false)}>
Cancel
</button>
<button onClick={handleDelete}>
Confirm Delete
</button>
</div>
</div>
)}
```
### Real Examples
See `/src/components/nav/ModerationPanel.tsx` for production examples:
- Transfer ownership confirmation (lines 1793-1929)
- Unban user confirmation (shows inline warning with Cancel/Confirm)
### Why This Pattern?
1. **Consistency**: Native dialogs look different across browsers and platforms
2. **Control**: We can style, position, and enhance confirmations to match our design
3. **Accessibility**: We can add proper ARIA attributes and keyboard navigation
4. **UX**: Users stay in context rather than being interrupted by modal dialogs
5. **Testing**: Inline confirmations are easier to test than native browser dialogs
### Migration Checklist
When replacing native dialogs:
- [ ] Add state variable for confirmation (e.g., `const [confirming, setConfirming] = useState(false)`)
- [ ] Remove the `confirm()` or `alert()` call from the handler
- [ ] Replace the original UI with conditional rendering
- [ ] Show initial state with primary action button
- [ ] Show confirmation state with warning message + Cancel/Confirm buttons
- [ ] Ensure Cancel button resets state: `onClick={() => setConfirming(false)}`
- [ ] Ensure Confirm button performs action and resets state
- [ ] Add loading states if the action is async
- [ ] Style to match surrounding UI using Panda CSS
## Styling System
This project uses **Panda CSS**, not Tailwind CSS.
- ❌ Never use Tailwind utility classes (e.g., `className="bg-blue-500"`)
- ✅ Always use Panda CSS `css()` function
- ✅ Use Panda's token system (defined in `panda.config.ts`)
See `.claude/CLAUDE.md` for complete Panda CSS documentation.
## Emoji Usage
Emojis are used liberally throughout the UI for visual communication:
- 👑 Host/owner status
- ⏳ Waiting states
- ⚠️ Warnings and confirmations
- ✅ Success states
- ❌ Error states
- 👀 Spectating mode
- 🎮 Gaming context
Use emojis to enhance clarity, not replace text.

View File

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

View File

@@ -24,7 +24,8 @@ function getBuildInfo() {
const gitCommitShort = process.env.GIT_COMMIT_SHORT || exec('git rev-parse --short HEAD')
const gitBranch = process.env.GIT_BRANCH || exec('git rev-parse --abbrev-ref HEAD')
const gitTag = process.env.GIT_TAG || exec('git describe --tags --exact-match 2>/dev/null')
const gitDirty = process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const gitDirty =
process.env.GIT_DIRTY === 'true' || exec('git diff --quiet || echo "dirty"') === 'dirty'
const packageJson = require('../package.json')

View File

@@ -1,7 +1,9 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
@@ -23,7 +25,9 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { data: viewerId } = useViewerId()
const { mutate: setRoomGame } = useSetRoomGame()
const [permissionError, setPermissionError] = useState<string | null>(null)
// Show loading state
if (isLoading) {
@@ -74,9 +78,27 @@ export default function RoomPage() {
// Show game selection if no game is set
if (!roomData.gameName) {
// Determine if current user is the host
const currentMember = roomData.members.find((m) => m.userId === viewerId)
const isHost = currentMember?.isCreator === true
const hostMember = roomData.members.find((m) => m.isCreator)
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if user is host before allowing selection
if (!isHost) {
setPermissionError(
`Only the room host can select a game. Ask ${hostMember?.displayName || 'the host'} to choose.`
)
// Clear error after 5 seconds
setTimeout(() => setPermissionError(null), 5000)
return
}
// Clear any previous errors
setPermissionError(null)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
@@ -86,10 +108,21 @@ export default function RoomPage() {
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
setRoomGame(
{
roomId: roomData.id,
gameName: gameType,
},
{
onError: (error: any) => {
console.error('[RoomPage] Failed to set game:', error)
setPermissionError(
error.message || 'Failed to select game. Only the host can change games.'
)
setTimeout(() => setPermissionError(null), 5000)
},
}
)
return
}
@@ -119,13 +152,70 @@ export default function RoomPage() {
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
mb: '4',
textAlign: 'center',
})}
>
Choose a Game
</h1>
{/* Host info and permission messaging */}
<div
className={css({
maxWidth: '800px',
width: '100%',
mb: '6',
})}
>
{isHost ? (
<div
className={css({
background: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#86efac',
fontSize: 'sm',
textAlign: 'center',
})}
>
👑 You're the room host. Select a game to start playing.
</div>
) : (
<div
className={css({
background: 'rgba(234, 179, 8, 0.1)',
border: '1px solid rgba(234, 179, 8, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fde047',
fontSize: 'sm',
textAlign: 'center',
})}
>
⏳ Waiting for {hostMember?.displayName || 'the host'} to select a game...
</div>
)}
{/* Permission error message */}
{permissionError && (
<div
className={css({
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.3)',
borderRadius: '8px',
padding: '12px 16px',
color: '#fca5a5',
fontSize: 'sm',
textAlign: 'center',
mt: '3',
})}
>
⚠️ {permissionError}
</div>
)}
</div>
<div
className={css({
display: 'grid',
@@ -138,21 +228,22 @@ export default function RoomPage() {
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
disabled={isDisabled}
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,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
@@ -193,21 +284,24 @@ export default function RoomPage() {
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
const isDisabled = !isHost || !isAvailable
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
disabled={isDisabled}
style={{
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
}}
className={css({
border: '2px solid',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.4 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
_hover: isDisabled
? {}
: {
transform: 'translateY(-4px) scale(1.02)',

View File

@@ -1,29 +1,38 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { PageWithNav } from '@/components/PageWithNav'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
export default function HomePage() {
const [abacusValue, setAbacusValue] = useState(1234567)
const appConfig = useAbacusConfig()
// Extract just the "Friends of 5" step (2+3=5) for homepage demo
const fullTutorial = getTutorialForEditor()
const friendsOf5Tutorial = {
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Learn the "Friends of 5" technique: adding 3 to make 5',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
}
return (
<PageWithNav navTitle="Soroban Mastery Platform" navEmoji="🧮">
<PageWithNav navTitle="Soroban Learning Platform" navEmoji="🧮">
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero with Large Abacus */}
{/* Hero Section */}
<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' },
py: { base: '12', md: '16' },
position: 'relative',
overflow: 'hidden',
})}
>
{/* Background pattern */}
<div
className={css({
position: 'absolute',
@@ -34,69 +43,105 @@ export default function HomePage() {
backgroundSize: '40px 40px',
})}
/>
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
{/* Main headline */}
<h1
className={css({
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
fontWeight: 'bold',
mb: '6',
mb: '4',
lineHeight: 'tight',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Master the Soroban
A structured path to soroban fluency
</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>
{/* Subtitle */}
<p
className={css({
fontSize: { base: 'lg', md: 'xl' },
color: 'gray.300',
mb: '8',
mb: '6',
maxW: '3xl',
mx: 'auto',
lineHeight: '1.6',
})}
>
Interactive tutorials, multiplayer games, and beautiful flashcardsyour complete
soroban learning ecosystem
Designed for self-directed learning. Start where you are, practice the skills you
need, play games that reinforce concepts.
</p>
{/* Dev status badge */}
<div
className={css({
display: 'inline-block',
px: '4',
py: '2',
bg: 'rgba(139, 92, 246, 0.15)',
border: '1px solid rgba(139, 92, 246, 0.3)',
borderRadius: 'full',
fontSize: 'sm',
color: 'purple.300',
mb: '8',
})}
>
🏗 Curriculum system in active development
</div>
{/* Visual learning journey */}
<div
className={css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '2',
mb: '8',
flexWrap: 'wrap',
})}
>
{[
{ icon: '📖', label: 'Learn' },
{ icon: '→', label: '', isArrow: true },
{ icon: '✏️', label: 'Practice' },
{ icon: '→', label: '', isArrow: true },
{ icon: '🎮', label: 'Play' },
{ icon: '→', label: '', isArrow: true },
{ icon: '🎯', label: 'Master' },
].map((step, i) => (
<div
key={i}
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '1',
opacity: step.isArrow ? 0.5 : 1,
})}
>
<div
className={css({
fontSize: step.isArrow ? 'xl' : '2xl',
color: step.isArrow ? 'gray.500' : 'yellow.400',
})}
>
{step.icon}
</div>
{step.label && (
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>{step.label}</div>
)}
</div>
))}
</div>
{/* Primary CTAs */}
<div className={hstack({ gap: '4', justify: 'center', flexWrap: 'wrap' })}>
<Link
href="/games"
href="/guide"
className={css({
px: '8',
py: '4',
@@ -113,10 +158,10 @@ export default function HomePage() {
transition: 'all 0.3s ease',
})}
>
🎮 Play Games
📚 Start Learning
</Link>
<Link
href="/guide"
href="/games"
className={css({
px: '8',
py: '4',
@@ -134,28 +179,7 @@ export default function HomePage() {
transition: 'all 0.3s ease',
})}
>
📚 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
🎮 Practice Through Games
</Link>
</div>
</div>
@@ -164,173 +188,325 @@ export default function HomePage() {
{/* 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"
{/* Learn by Doing Section - with inline tutorial demo */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: 'md',
color: 'yellow.400',
fontWeight: 'semibold',
_hover: { color: 'yellow.300' },
display: { base: 'none', md: 'block' },
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
View All
</Link>
Learn by Doing
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Interactive tutorials teach you step-by-step. Try this example right now:
</p>
</div>
{/* Live demo and learning objectives */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '900px',
mx: 'auto',
})}
>
{/* What you'll learn - above tutorial */}
<div
className={css({
mb: '8',
pb: '6',
borderBottom: '1px solid',
borderColor: 'gray.700',
})}
>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'white',
mb: '4',
textAlign: 'center',
})}
>
What You'll Learn
</h3>
<div className={grid({ columns: { base: 1, sm: 2 }, gap: '3' })}>
{[
'Read and set numbers on an abacus',
'Add and subtract with "friends" techniques',
'Multiply and divide fluently',
'Calculate mentally without the abacus',
].map((skill, 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' })}>{skill}</span>
</div>
))}
</div>
</div>
<TutorialPlayer
tutorial={friendsOf5Tutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
abacusColumns={2}
theme="dark"
/>
</div>
</section>
{/* Current Offerings Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
Available Now
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>
Foundation tutorials and reinforcement games ready to use
</p>
</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']}
tags={['Memory', 'Pattern Recognition']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Turn-based card battles"
description="Match complement numbers"
players="1-4 players"
tags={['Pattern Recognition']}
tags={['Friends of 5', 'Friends of 10']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<GameCard
icon="🏁"
title="Speed Race"
description="Race AI with complements"
players="1-4 players + AI"
tags={['Practice', 'Sprint', 'Survival']}
title="Complement Race"
description="Race against time"
players="1-4 players"
tags={['Speed', 'Practice', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange cards visually"
description="Arrange numbers visually"
players="Solo challenge"
tags={['Visual Literacy']}
tags={['Visual Literacy', 'Ordering']}
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>
{/* For Kids & Families Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
For Kids & Families
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Simple enough for kids to start on their own, structured enough for parents to trust
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🧒"
title="Self-Directed for Children"
features={[
'Big, obvious buttons and clear instructions',
'Progress at your own pace',
'Works with or without a physical abacus',
]}
accentColor="purple"
/>
<FeaturePanel
icon="👨‍👩‍👧"
title="Trusted by Parents"
features={[
'Structured curriculum following Japanese methods',
'Traditional kyu/dan progression levels',
'Track progress and celebrate achievements',
]}
accentColor="blue"
/>
</div>
</section>
{/* Progression Visualization */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
Your Journey
</h2>
<p className={css({ color: 'rgba(229, 231, 235, 1)', fontSize: 'md' })}>
Progress from beginner to master
</p>
</div>
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
border: '1px solid',
borderColor: 'gray.700',
rounded: 'xl',
p: '8',
})}
>
<div
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '4',
flexWrap: 'wrap',
})}
>
{[
{ level: '10 Kyu', label: 'Beginner', color: 'green.400' },
{ level: '5 Kyu', label: 'Intermediate', color: 'blue.400' },
{ level: '1 Kyu', label: 'Advanced', color: 'purple.400' },
{ level: 'Dan', label: 'Master', color: 'yellow.400' },
].map((stage, i) => (
<div
key={i}
className={stack({
gap: '2',
textAlign: 'center',
flex: '1',
position: 'relative',
})}
>
<div
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: stage.color,
})}
>
{stage.level}
</div>
<div className={css({ fontSize: 'sm', color: 'rgba(229, 231, 235, 1)' })}>
{stage.label}
</div>
{i < 3 && (
<div
className={css({
display: { base: 'none', md: 'block' },
position: 'absolute',
right: '-50%',
fontSize: 'xl',
color: 'rgba(156, 163, 175, 1)',
})}
>
</div>
)}
</div>
))}
</div>
<div
className={css({
mt: '6',
textAlign: 'center',
fontSize: 'sm',
color: 'rgba(209, 213, 219, 1)',
fontStyle: 'italic',
})}
>
You'll progress through all these levels eventually
</div>
</div>
</section>
{/* Additional Tools Section */}
<section className={stack({ gap: '6' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
Additional Tools
</h2>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
features={[
'Multiple formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
accentColor="blue"
ctaText="Create Flashcards →"
ctaHref="/create"
/>
<FeaturePanel
icon="🧮"
title="Interactive Abacus"
features={[
'Practice anytime in your browser',
'Multiple color schemes and bead styles',
'Sound effects and animations',
]}
accentColor="purple"
ctaText="Try the Abacus →"
ctaHref="/guide"
/>
</div>
</section>
</div>
</div>
</PageWithNav>
)
}
function ColorSchemeCard({
title,
description,
colorScheme,
value,
beadShape,
}: {
title: string
description: string
colorScheme: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
value: number
beadShape: 'diamond' | 'circle' | 'square'
}) {
return (
<div
className={css({
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)',
boxShadow: '0 20px 40px rgba(139, 92, 246, 0.2)',
},
})}
>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{title}
</h3>
<p className={css({ fontSize: 'sm', color: 'gray.400', mb: '4' })}>{description}</p>
<div
className={css({
display: 'flex',
justifyContent: 'center',
p: '4',
bg: 'rgba(255, 255, 255, 0.05)',
rounded: 'lg',
})}
>
<AbacusReact
value={value}
columns={3}
beadShape={beadShape}
colorScheme={colorScheme}
hideInactiveBeads={false}
/>
</div>
</div>
)
}
function GameCard({
icon,
title,
@@ -400,19 +576,17 @@ function GameCard({
function FeaturePanel({
icon,
title,
description,
features,
accentColor,
ctaText,
ctaHref,
accentColor,
}: {
icon: string
title: string
description: string
features: string[]
ctaText: string
ctaHref: string
accentColor: 'purple' | 'blue'
ctaText?: string
ctaHref?: string
}) {
const borderColor = accentColor === 'purple' ? 'purple.500/30' : 'blue.500/30'
const bgColor = accentColor === 'purple' ? 'purple.500/10' : 'blue.500/10'
@@ -432,8 +606,7 @@ function FeaturePanel({
<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' })}>
<div className={stack({ gap: '3', mb: ctaText ? '6' : '0' })}>
{features.map((feature, i) => (
<div key={i} className={hstack({ gap: '3' })}>
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}></span>
@@ -441,25 +614,27 @@ function FeaturePanel({
</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>
{ctaText && ctaHref && (
<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

@@ -114,6 +114,9 @@ export function ModerationPanel({
null
)
// Transfer ownership confirmation state
const [confirmingTransferOwnership, setConfirmingTransferOwnership] = useState(false)
// Auto-switch to Members tab when focusedUserId is provided
useEffect(() => {
if (isOpen && focusedUserId) {
@@ -171,8 +174,6 @@ export function ModerationPanel({
}, [isOpen, roomId, members])
const handleKick = async (userId: string) => {
if (!confirm('Kick this player from the room?')) return
setActionLoading(`kick-${userId}`)
try {
const res = await fetch(`/api/arcade/rooms/${roomId}/kick`, {
@@ -414,10 +415,9 @@ export function ModerationPanel({
const newOwner = members.find((m) => m.userId === selectedNewOwner)
if (!newOwner) return
if (!confirm(`Transfer ownership to ${newOwner.displayName}? You will no longer be the host.`))
return
setConfirmingTransferOwnership(false)
setActionLoading('transfer-ownership')
try {
const res = await fetch(`/api/arcade/rooms/${roomId}/transfer-ownership`, {
method: 'POST',
@@ -436,6 +436,7 @@ export function ModerationPanel({
showError(err instanceof Error ? err.message : 'Failed to transfer ownership')
} finally {
setActionLoading(null)
setSelectedNewOwner('') // Reset selection
}
}
@@ -1789,61 +1790,143 @@ export function ModerationPanel({
Transfer host privileges to another member. You will no longer be the host.
</p>
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
style={{
width: '100%',
padding: '10px',
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(75, 85, 99, 0.5)',
borderRadius: '6px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
marginBottom: '12px',
cursor: 'pointer',
}}
>
<option value="">Select new owner...</option>
{otherMembers.map((member) => (
<option key={member.userId} value={member.userId}>
{member.displayName}
{member.isOnline ? ' (Online)' : ' (Offline)'}
</option>
))}
</select>
{!confirmingTransferOwnership ? (
<>
<select
value={selectedNewOwner}
onChange={(e) => setSelectedNewOwner(e.target.value)}
style={{
width: '100%',
padding: '10px',
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(75, 85, 99, 0.5)',
borderRadius: '6px',
color: 'rgba(209, 213, 219, 1)',
fontSize: '14px',
marginBottom: '12px',
cursor: 'pointer',
}}
>
<option value="">Select new owner...</option>
{otherMembers.map((member) => (
<option key={member.userId} value={member.userId}>
{member.displayName}
{member.isOnline ? ' (Online)' : ' (Offline)'}
</option>
))}
</select>
<button
type="button"
onClick={handleTransferOwnership}
disabled={!selectedNewOwner || actionLoading === 'transfer-ownership'}
style={{
width: '100%',
padding: '10px',
background:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
!selectedNewOwner || actionLoading === 'transfer-ownership'
? 'not-allowed'
: 'pointer',
opacity:
!selectedNewOwner || actionLoading === 'transfer-ownership' ? 0.5 : 1,
}}
>
{actionLoading === 'transfer-ownership'
? 'Transferring...'
: 'Transfer Ownership'}
</button>
<button
type="button"
onClick={() => setConfirmingTransferOwnership(true)}
disabled={!selectedNewOwner}
style={{
width: '100%',
padding: '10px',
background: !selectedNewOwner
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border: !selectedNewOwner
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor: !selectedNewOwner ? 'not-allowed' : 'pointer',
opacity: !selectedNewOwner ? 0.5 : 1,
}}
>
Transfer Ownership
</button>
</>
) : (
<div>
<div
style={{
fontSize: '13px',
fontWeight: '600',
color: 'rgba(251, 191, 36, 1)',
marginBottom: '8px',
}}
>
Confirm Transfer to{' '}
{members.find((m) => m.userId === selectedNewOwner)?.displayName}?
</div>
<div
style={{
fontSize: '12px',
color: 'rgba(209, 213, 219, 0.8)',
marginBottom: '12px',
}}
>
You will no longer be the host and will lose moderation privileges. This
cannot be undone.
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
type="button"
onClick={() => setConfirmingTransferOwnership(false)}
disabled={actionLoading === 'transfer-ownership'}
style={{
flex: 1,
padding: '10px',
background: 'rgba(75, 85, 99, 0.3)',
color: 'rgba(209, 213, 219, 1)',
border: '1px solid rgba(75, 85, 99, 0.5)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
actionLoading === 'transfer-ownership' ? 'not-allowed' : 'pointer',
opacity: actionLoading === 'transfer-ownership' ? 0.5 : 1,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (actionLoading !== 'transfer-ownership') {
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.4)'
}
}}
onMouseLeave={(e) => {
if (actionLoading !== 'transfer-ownership') {
e.currentTarget.style.background = 'rgba(75, 85, 99, 0.3)'
}
}}
>
Cancel
</button>
<button
type="button"
onClick={handleTransferOwnership}
disabled={actionLoading === 'transfer-ownership'}
style={{
flex: 1,
padding: '10px',
background:
actionLoading === 'transfer-ownership'
? 'rgba(75, 85, 99, 0.3)'
: 'linear-gradient(135deg, rgba(251, 146, 60, 0.8), rgba(249, 115, 22, 0.8))',
color: 'white',
border:
actionLoading === 'transfer-ownership'
? '1px solid rgba(75, 85, 99, 0.5)'
: '1px solid rgba(251, 146, 60, 0.6)',
borderRadius: '6px',
fontSize: '14px',
fontWeight: '600',
cursor:
actionLoading === 'transfer-ownership' ? 'not-allowed' : 'pointer',
opacity: actionLoading === 'transfer-ownership' ? 0.5 : 1,
transition: 'all 0.2s ease',
}}
>
{actionLoading === 'transfer-ownership'
? 'Transferring...'
: 'Confirm Transfer'}
</button>
</div>
</div>
)}
</div>
</div>
</div>

View File

@@ -215,6 +215,10 @@ interface TutorialPlayerProps {
initialStepIndex?: number
isDebugMode?: boolean
showDebugPanel?: boolean
hideNavigation?: boolean
hideTooltip?: boolean
abacusColumns?: number
theme?: 'light' | 'dark'
onStepChange?: (stepIndex: number, step: TutorialStep) => void
onStepComplete?: (stepIndex: number, step: TutorialStep, success: boolean) => void
onTutorialComplete?: (score: number, timeSpent: number) => void
@@ -227,6 +231,10 @@ function TutorialPlayerContent({
initialStepIndex = 0,
isDebugMode = false,
showDebugPanel = false,
hideNavigation = false,
hideTooltip = false,
abacusColumns = 5,
theme = 'light',
onStepChange,
onStepComplete,
onTutorialComplete,
@@ -379,16 +387,20 @@ function TutorialPlayerContent({
}
// Convert bead diff results to StepBeadHighlight format expected by AbacusReact
const stepBeadHighlights: StepBeadHighlight[] = beadDiff.changes.map((change, _index) => ({
placeValue: change.placeValue,
beadType: change.beadType,
position: change.position,
direction: change.direction,
stepIndex: currentMultiStep, // Use current multi-step index to match AbacusReact filtering
order: change.order,
}))
// Filter to only include beads from columns that exist
const minValidPlaceValue = Math.max(0, 5 - abacusColumns)
const stepBeadHighlights: StepBeadHighlight[] = beadDiff.changes
.filter((change) => change.placeValue < abacusColumns)
.map((change, _index) => ({
placeValue: change.placeValue,
beadType: change.beadType,
position: change.position,
direction: change.direction,
stepIndex: currentMultiStep, // Use current multi-step index to match AbacusReact filtering
order: change.order,
}))
return stepBeadHighlights
return stepBeadHighlights.length > 0 ? stepBeadHighlights : undefined
} catch (error) {
console.error('Error generating step beads with bead diff:', error)
return undefined
@@ -399,6 +411,7 @@ function TutorialPlayerContent({
expectedSteps,
currentMultiStep,
currentStep.stepBeadHighlights,
abacusColumns,
])
// Get the current step's bead diff summary for real-time user feedback
@@ -422,6 +435,14 @@ function TutorialPlayerContent({
// Get current step summary for real-time user feedback
const currentStepSummary = getCurrentStepSummary()
// Filter highlightBeads to only include valid columns
const filteredHighlightBeads = useMemo(() => {
if (!currentStep.highlightBeads) return undefined
return currentStep.highlightBeads.filter((highlight) => {
return highlight.placeValue < abacusColumns
})
}, [currentStep.highlightBeads, abacusColumns])
// Helper function to highlight the current mathematical term in the full decomposition
const renderHighlightedDecomposition = useCallback(() => {
if (!fullDecomposition || expectedSteps.length === 0) return null
@@ -519,16 +540,27 @@ function TutorialPlayerContent({
return null
}
// Validate that the bead is from a column that exists
if (topmostBead.placeValue >= abacusColumns) {
// Bead is from an invalid column, skip tooltip
return null
}
// Smart positioning logic: avoid covering active beads
const targetColumnIndex = 4 - topmostBead.placeValue // Convert placeValue to columnIndex (5 columns: 0-4)
// Convert placeValue to columnIndex based on actual number of columns
const targetColumnIndex = abacusColumns - 1 - topmostBead.placeValue
// Check if there are any active beads (against reckoning bar OR with arrows) in columns to the left
const hasActiveBeadsToLeft = (() => {
// Get current abacus state - we need to check which beads are against the reckoning bar
const abacusDigits = currentValue.toString().padStart(5, '0').split('').map(Number)
const abacusDigits = currentValue
.toString()
.padStart(abacusColumns, '0')
.split('')
.map(Number)
for (let col = 0; col < targetColumnIndex; col++) {
const _placeValue = 4 - col // Convert columnIndex back to placeValue
const placeValue = abacusColumns - 1 - col // Convert columnIndex back to placeValue
const digitValue = abacusDigits[col]
// Check if any beads are active (against reckoning bar) in this column
@@ -544,7 +576,7 @@ function TutorialPlayerContent({
// Also check if this column has beads with direction arrows (from current step)
const hasArrowsInColumn =
currentStepBeads?.some((bead) => {
const beadColumnIndex = 4 - bead.placeValue
const beadColumnIndex = abacusColumns - 1 - bead.placeValue
return beadColumnIndex === col && bead.direction && bead.direction !== 'none'
}) ?? false
if (hasArrowsInColumn) {
@@ -670,6 +702,7 @@ function TutorialPlayerContent({
currentValue,
currentStep,
isMeaningfulDecomposition,
abacusColumns,
])
// Timer for smart help detection
@@ -864,8 +897,8 @@ function TutorialPlayerContent({
// Check if this is the correct action
if (currentStep.highlightBeads && Array.isArray(currentStep.highlightBeads)) {
const isCorrectBead = currentStep.highlightBeads.some((highlight) => {
// Get place value from highlight (convert columnIndex to placeValue if needed)
const highlightPlaceValue = highlight.placeValue ?? 4 - highlight.columnIndex
// Get place value from highlight
const highlightPlaceValue = highlight.placeValue
// Get place value from bead click event
const beadPlaceValue = beadInfo.bead ? beadInfo.bead.placeValue : 4 - beadInfo.columnIndex
@@ -877,9 +910,10 @@ function TutorialPlayerContent({
})
if (!isCorrectBead) {
const errorMessage = "That's not the highlighted bead. Try clicking the highlighted bead."
dispatch({
type: 'SET_ERROR',
error: currentStep.errorMessages.wrongBead,
error: errorMessage,
})
dispatch({
@@ -887,7 +921,7 @@ function TutorialPlayerContent({
event: {
type: 'ERROR_OCCURRED',
stepId: currentStep.id,
error: currentStep.errorMessages.wrongBead,
error: errorMessage,
timestamp: new Date(),
},
})
@@ -1003,14 +1037,21 @@ function TutorialPlayerContent({
// Memoize custom styles calculation to avoid expensive recalculation on every render
const customStyles = useMemo(() => {
// Calculate valid column range based on abacusColumns
const minValidColumn = 5 - abacusColumns
// Start with static highlights from step configuration
const staticHighlights: Record<number, any> = {}
if (currentStep.highlightBeads && Array.isArray(currentStep.highlightBeads)) {
currentStep.highlightBeads.forEach((highlight) => {
// Convert placeValue to columnIndex for AbacusReact compatibility
const columnIndex =
highlight.placeValue !== undefined ? 4 - highlight.placeValue : highlight.columnIndex
const columnIndex = abacusColumns - 1 - highlight.placeValue
// Skip highlights for columns that don't exist
if (columnIndex < minValidColumn) {
return
}
// Initialize column if it doesn't exist
if (!staticHighlights[columnIndex]) {
@@ -1041,6 +1082,12 @@ function TutorialPlayerContent({
const mergedHighlights = { ...staticHighlights }
Object.keys(dynamicColumnHighlights).forEach((columnIndexStr) => {
const columnIndex = parseInt(columnIndexStr, 10)
// Skip highlights for columns that don't exist
if (columnIndex < minValidColumn) {
return
}
if (!mergedHighlights[columnIndex]) {
mergedHighlights[columnIndex] = {}
}
@@ -1048,8 +1095,32 @@ function TutorialPlayerContent({
Object.assign(mergedHighlights[columnIndex], dynamicColumnHighlights[columnIndex])
})
return Object.keys(mergedHighlights).length > 0 ? { columns: mergedHighlights } : undefined
}, [currentStep.highlightBeads, dynamicColumnHighlights])
// Build the custom styles object
const styles: any = {}
// Add column highlights if any
if (Object.keys(mergedHighlights).length > 0) {
styles.columns = mergedHighlights
}
// Add frame styling for dark mode
if (theme === 'dark') {
// Column dividers (global for all columns)
styles.columnPosts = {
fill: 'rgba(255, 255, 255, 0.3)', // High contrast fill for visibility
stroke: 'rgba(255, 255, 255, 0.2)',
strokeWidth: 2,
}
// Reckoning bar (horizontal middle bar)
styles.reckoningBar = {
fill: 'rgba(255, 255, 255, 0.4)', // High contrast fill for visibility
stroke: 'rgba(255, 255, 255, 0.25)',
strokeWidth: 3,
}
}
return Object.keys(styles).length > 0 ? styles : undefined
}, [currentStep.highlightBeads, dynamicColumnHighlights, abacusColumns, theme])
if (!currentStep) {
return <div>No steps available</div>
@@ -1061,183 +1132,187 @@ function TutorialPlayerContent({
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: '600px',
minHeight: hideNavigation ? 'auto' : '600px',
})} ${className || ''}`}
>
{/* Header */}
<div
className={css({
borderBottom: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'white',
})}
>
{!hideNavigation && (
<div
className={hstack({
justifyContent: 'space-between',
alignItems: 'center',
className={css({
borderBottom: '1px solid',
borderColor: theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
p: 4,
bg: theme === 'dark' ? 'rgba(30, 30, 40, 0.6)' : 'white',
})}
>
<div>
<h1 className={css({ fontSize: 'xl', fontWeight: 'bold' })}>{tutorial.title}</h1>
<p className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {tutorial.steps.length}: {currentStep.title}
</p>
</div>
<div className={hstack({ gap: 2 })}>
{isDebugMode && (
<>
<button
onClick={toggleDebugPanel}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'blue.300',
borderRadius: 'md',
bg: uiState.showDebugPanel ? 'blue.100' : 'white',
color: 'blue.700',
cursor: 'pointer',
_hover: { bg: 'blue.50' },
})}
>
Debug
</button>
<button
onClick={toggleStepList}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: uiState.showStepList ? 'gray.100' : 'white',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
})}
>
Steps
</button>
{/* Multi-step navigation controls */}
{currentStep.multiStepInstructions &&
currentStep.multiStepInstructions.length > 1 && (
<>
<div
className={css({
fontSize: 'xs',
color: 'gray.600',
px: 2,
borderLeft: '1px solid',
borderColor: 'gray.300',
ml: 2,
pl: 3,
})}
>
Multi-Step: {currentMultiStep + 1} /{' '}
{currentStep.multiStepInstructions.length}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
First
</button>
<button
onClick={() => previousMultiStep()}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
Prev
</button>
<button
onClick={() => advanceMultiStep()}
disabled={currentMultiStep >= currentStep.multiStepInstructions.length - 1}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.200'
: 'green.300',
borderRadius: 'md',
bg:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.100'
: 'white',
color:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.400'
: 'green.700',
cursor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'not-allowed'
: 'pointer',
_hover:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? {}
: { bg: 'green.50' },
})}
>
Next
</button>
</>
)}
<label className={hstack({ gap: 2, fontSize: 'sm' })}>
<input
type="checkbox"
checked={uiState.autoAdvance}
onChange={toggleAutoAdvance}
/>
Auto-advance
</label>
</>
)}
</div>
</div>
{/* Progress bar */}
<div className={css({ mt: 2, bg: 'gray.200', borderRadius: 'full', h: 2 })}>
<div
className={css({
bg: 'blue.500',
h: 'full',
borderRadius: 'full',
transition: 'width 0.3s ease',
className={hstack({
justifyContent: 'space-between',
alignItems: 'center',
})}
style={{ width: `${navigationState.completionPercentage}%` }}
/>
>
<div>
<h1 className={css({ fontSize: 'xl', fontWeight: 'bold' })}>{tutorial.title}</h1>
<p className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {tutorial.steps.length}: {currentStep.title}
</p>
</div>
<div className={hstack({ gap: 2 })}>
{isDebugMode && (
<>
<button
onClick={toggleDebugPanel}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'blue.300',
borderRadius: 'md',
bg: uiState.showDebugPanel ? 'blue.100' : 'white',
color: 'blue.700',
cursor: 'pointer',
_hover: { bg: 'blue.50' },
})}
>
Debug
</button>
<button
onClick={toggleStepList}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: uiState.showStepList ? 'gray.100' : 'white',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
})}
>
Steps
</button>
{/* Multi-step navigation controls */}
{currentStep.multiStepInstructions &&
currentStep.multiStepInstructions.length > 1 && (
<>
<div
className={css({
fontSize: 'xs',
color: 'gray.600',
px: 2,
borderLeft: '1px solid',
borderColor: 'gray.300',
ml: 2,
pl: 3,
})}
>
Multi-Step: {currentMultiStep + 1} /{' '}
{currentStep.multiStepInstructions.length}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
First
</button>
<button
onClick={() => previousMultiStep()}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
Prev
</button>
<button
onClick={() => advanceMultiStep()}
disabled={
currentMultiStep >= currentStep.multiStepInstructions.length - 1
}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.200'
: 'green.300',
borderRadius: 'md',
bg:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.100'
: 'white',
color:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.400'
: 'green.700',
cursor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'not-allowed'
: 'pointer',
_hover:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? {}
: { bg: 'green.50' },
})}
>
Next
</button>
</>
)}
<label className={hstack({ gap: 2, fontSize: 'sm' })}>
<input
type="checkbox"
checked={uiState.autoAdvance}
onChange={toggleAutoAdvance}
/>
Auto-advance
</label>
</>
)}
</div>
</div>
{/* Progress bar */}
<div className={css({ mt: 2, bg: 'gray.200', borderRadius: 'full', h: 2 })}>
<div
className={css({
bg: 'blue.500',
h: 'full',
borderRadius: 'full',
transition: 'width 0.3s ease',
})}
style={{ width: `${navigationState.completionPercentage}%` }}
/>
</div>
</div>
</div>
)}
<div className={hstack({ flex: 1, gap: 0 })}>
{/* Step list sidebar */}
@@ -1313,11 +1388,18 @@ function TutorialPlayerContent({
fontSize: '2xl',
fontWeight: 'bold',
mb: 2,
color: theme === 'dark' ? 'gray.200' : 'gray.900',
})}
>
{currentStep.problem}
</h2>
<p className={css({ fontSize: 'lg', color: 'gray.700', mb: 4 })}>
<p
className={css({
fontSize: 'lg',
color: theme === 'dark' ? 'gray.400' : 'gray.700',
mb: 4,
})}
>
{currentStep.description}
</p>
{/* Hide action description for multi-step problems since it duplicates pedagogical decomposition */}
@@ -1329,18 +1411,26 @@ function TutorialPlayerContent({
</div>
{/* Multi-step instructions panel */}
{currentStep.multiStepInstructions &&
{!hideTooltip &&
currentStep.multiStepInstructions &&
currentStep.multiStepInstructions.length > 0 && (
<div
className={css({
p: 5,
background:
'linear-gradient(135deg, rgba(255,248,225,0.95) 0%, rgba(254,252,232,0.95) 50%, rgba(255,245,157,0.15) 100%)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(40,40,50,0.6) 0%, rgba(50,50,60,0.6) 50%, rgba(60,50,70,0.3) 100%)'
: 'linear-gradient(135deg, rgba(255,248,225,0.95) 0%, rgba(254,252,232,0.95) 50%, rgba(255,245,157,0.15) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(251,191,36,0.3)',
border:
theme === 'dark'
? '1px solid rgba(255,255,255,0.1)'
: '1px solid rgba(251,191,36,0.3)',
borderRadius: 'xl',
boxShadow:
'0 8px 32px rgba(251,191,36,0.1), 0 2px 8px rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.6)',
theme === 'dark'
? '0 4px 16px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'
: '0 8px 32px rgba(251,191,36,0.1), 0 2px 8px rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.6)',
position: 'relative',
maxW: '600px',
w: 'full',
@@ -1350,7 +1440,9 @@ function TutorialPlayerContent({
inset: '0',
borderRadius: 'xl',
background:
'linear-gradient(135deg, rgba(251,191,36,0.1) 0%, rgba(168,85,247,0.05) 100%)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(100,100,120,0.1) 0%, rgba(80,60,100,0.05) 100%)'
: 'linear-gradient(135deg, rgba(251,191,36,0.1) 0%, rgba(168,85,247,0.05) 100%)',
zIndex: -1,
},
})}
@@ -1359,10 +1451,10 @@ function TutorialPlayerContent({
className={css({
fontSize: 'base',
fontWeight: '600',
color: 'amber.900',
color: theme === 'dark' ? 'gray.300' : 'amber.900',
mb: 4,
letterSpacing: 'wide',
textShadow: '0 1px 2px rgba(0,0,0,0.1)',
textShadow: theme === 'dark' ? 'none' : '0 1px 2px rgba(0,0,0,0.1)',
})}
>
Guidance
@@ -1375,18 +1467,25 @@ function TutorialPlayerContent({
mb: 4,
p: 3,
background:
'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.9) 100%)',
border: '1px solid rgba(203,213,225,0.4)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(50,50,60,0.4) 0%, rgba(40,40,50,0.5) 100%)'
: 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.9) 100%)',
border:
theme === 'dark'
? '1px solid rgba(255,255,255,0.1)'
: '1px solid rgba(203,213,225,0.4)',
borderRadius: 'lg',
boxShadow:
'0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.7)',
theme === 'dark'
? '0 1px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'
: '0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.7)',
backdropFilter: 'blur(4px)',
})}
>
<p
<div
className={css({
fontSize: 'base',
color: 'slate.800',
color: theme === 'dark' ? 'gray.300' : 'slate.800',
fontFamily: 'mono',
fontWeight: '500',
letterSpacing: 'tight',
@@ -1398,14 +1497,14 @@ function TutorialPlayerContent({
termPositions={termPositions}
segments={pedagogicalSegments}
/>
</p>
</div>
</div>
)}
<div
className={css({
fontSize: 'sm',
color: 'amber.800',
color: theme === 'dark' ? 'gray.400' : 'amber.800',
fontWeight: '500',
lineHeight: '1.6',
})}
@@ -1449,7 +1548,10 @@ function TutorialPlayerContent({
className={css({
mb: 1,
fontWeight: 'bold',
color: 'yellow.900',
color: theme === 'dark' ? 'yellow.200' : 'yellow.900',
textShadow:
theme === 'dark' ? '0 0 12px rgba(251, 191, 36, 0.4)' : 'none',
fontSize: theme === 'dark' ? 'lg' : 'base',
})}
>
{currentInstruction}
@@ -1483,17 +1585,17 @@ function TutorialPlayerContent({
{/* Abacus */}
<div
className={css({
bg: 'white',
bg: theme === 'dark' ? 'rgba(30, 30, 40, 0.4)' : 'white',
border: '2px solid',
borderColor: 'gray.200',
borderColor: theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
borderRadius: 'lg',
p: 6,
shadow: 'lg',
shadow: theme === 'dark' ? '0 4px 6px rgba(0, 0, 0, 0.3)' : 'lg',
})}
>
<AbacusReact
value={currentValue}
columns={5}
columns={abacusColumns}
interactive={true}
animated={true}
scaleFactor={2.5}
@@ -1502,7 +1604,7 @@ function TutorialPlayerContent({
hideInactiveBeads={abacusConfig.hideInactiveBeads}
soundEnabled={abacusConfig.soundEnabled}
soundVolume={abacusConfig.soundVolume}
highlightBeads={currentStep.highlightBeads}
highlightBeads={filteredHighlightBeads}
stepBeadHighlights={currentStepBeads}
currentStep={currentMultiStep}
showDirectionIndicators={true}
@@ -1567,7 +1669,7 @@ function TutorialPlayerContent({
</div>
{/* Tooltip */}
{currentStep.tooltip && (
{!hideTooltip && currentStep.tooltip && (
<div
className={css({
maxW: '500px',
@@ -1596,57 +1698,60 @@ function TutorialPlayerContent({
</div>
{/* Navigation controls */}
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'gray.50',
})}
>
<div className={hstack({ justifyContent: 'space-between' })}>
<button
onClick={goToPreviousStep}
disabled={!navigationState.canGoPrevious}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: 'white',
cursor: navigationState.canGoPrevious ? 'pointer' : 'not-allowed',
opacity: navigationState.canGoPrevious ? 1 : 0.5,
_hover: navigationState.canGoPrevious ? { bg: 'gray.50' } : {},
})}
>
Previous
</button>
{!hideNavigation && (
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'gray.50',
})}
>
<div className={hstack({ justifyContent: 'space-between' })}>
<button
onClick={goToPreviousStep}
disabled={!navigationState.canGoPrevious}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: 'white',
cursor: navigationState.canGoPrevious ? 'pointer' : 'not-allowed',
opacity: navigationState.canGoPrevious ? 1 : 0.5,
_hover: navigationState.canGoPrevious ? { bg: 'gray.50' } : {},
})}
>
Previous
</button>
<div className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {navigationState.totalSteps}
<div className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {navigationState.totalSteps}
</div>
<button
onClick={goToNextStep}
disabled={!navigationState.canGoNext && !isStepCompleted}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor:
navigationState.canGoNext || isStepCompleted ? 'blue.300' : 'gray.300',
borderRadius: 'md',
bg: navigationState.canGoNext || isStepCompleted ? 'blue.500' : 'gray.200',
color: navigationState.canGoNext || isStepCompleted ? 'white' : 'gray.500',
cursor:
navigationState.canGoNext || isStepCompleted ? 'pointer' : 'not-allowed',
_hover: navigationState.canGoNext || isStepCompleted ? { bg: 'blue.600' } : {},
})}
>
{navigationState.canGoNext ? 'Next →' : 'Complete Tutorial'}
</button>
</div>
<button
onClick={goToNextStep}
disabled={!navigationState.canGoNext && !isStepCompleted}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor:
navigationState.canGoNext || isStepCompleted ? 'blue.300' : 'gray.300',
borderRadius: 'md',
bg: navigationState.canGoNext || isStepCompleted ? 'blue.500' : 'gray.200',
color: navigationState.canGoNext || isStepCompleted ? 'white' : 'gray.500',
cursor: navigationState.canGoNext || isStepCompleted ? 'pointer' : 'not-allowed',
_hover: navigationState.canGoNext || isStepCompleted ? { bg: 'blue.600' } : {},
})}
>
{navigationState.canGoNext ? 'Next →' : 'Complete Tutorial'}
</button>
</div>
</div>
)}
</div>
{/* Debug panel */}

View File

@@ -464,6 +464,24 @@ export function useRoomData() {
}
}
const handleOwnershipTransferred = (data: {
roomId: string
oldOwnerId: string
newOwnerId: string
newOwnerName: string
members: RoomMember[]
}) => {
if (data.roomId === roomData?.id) {
queryClient.setQueryData<RoomData | null>(roomKeys.current(), (prev) => {
if (!prev) return null
return {
...prev,
members: data.members,
}
})
}
}
socket.on('room-joined', handleRoomJoined)
socket.on('member-joined', handleMemberJoined)
socket.on('member-left', handleMemberLeft)
@@ -474,6 +492,7 @@ export function useRoomData() {
socket.on('room-invitation-received', handleInvitationReceived)
socket.on('join-request-submitted', handleJoinRequestSubmitted)
socket.on('room-game-changed', handleRoomGameChanged)
socket.on('ownership-transferred', handleOwnershipTransferred)
return () => {
socket.off('room-joined', handleRoomJoined)
@@ -486,6 +505,7 @@ export function useRoomData() {
socket.off('room-invitation-received', handleInvitationReceived)
socket.off('join-request-submitted', handleJoinRequestSubmitted)
socket.off('room-game-changed', handleRoomGameChanged)
socket.off('ownership-transferred', handleOwnershipTransferred)
}
}, [socket, roomData?.id, queryClient])

View File

@@ -21,52 +21,52 @@ export const GAME_THEMES = {
blue: {
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
borderColor: 'blue.200',
borderColor: '#bfdbfe', // blue.200
},
purple: {
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
borderColor: 'purple.200',
borderColor: '#ddd6fe', // purple.200
},
green: {
color: 'green',
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
borderColor: 'green.200',
borderColor: '#a7f3d0', // green.200
},
teal: {
color: 'teal',
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
borderColor: 'teal.200',
borderColor: '#99f6e4', // teal.200
},
indigo: {
color: 'indigo',
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
borderColor: 'indigo.200',
borderColor: '#c7d2fe', // indigo.200
},
pink: {
color: 'pink',
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
borderColor: 'pink.200',
borderColor: '#fbcfe8', // pink.200
},
orange: {
color: 'orange',
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
borderColor: 'orange.200',
borderColor: '#fed7aa', // orange.200
},
yellow: {
color: 'yellow',
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
borderColor: 'yellow.200',
borderColor: '#fde68a', // yellow.200
},
red: {
color: 'red',
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
borderColor: 'red.200',
borderColor: '#fecaca', // red.200
},
gray: {
color: 'gray',
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
borderColor: 'gray.200',
borderColor: '#e5e7eb', // gray.200
},
} as const satisfies Record<string, GameTheme>

View File

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

View File

@@ -27,6 +27,7 @@ export interface BeadStyle {
}
export interface ColumnPostStyle {
fill?: string;
stroke?: string;
strokeWidth?: number;
opacity?: number;
@@ -34,6 +35,7 @@ export interface ColumnPostStyle {
}
export interface ReckoningBarStyle {
fill?: string;
stroke?: string;
strokeWidth?: number;
opacity?: number;
@@ -1979,7 +1981,10 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
const columnStyles = customStyles?.columns?.[colIndex];
const globalColumnPosts = customStyles?.columnPosts;
const rodStyle = {
fill: "rgb(0, 0, 0, 0.1)", // Default Typst color
fill:
columnStyles?.columnPost?.fill ||
globalColumnPosts?.fill ||
"rgb(0, 0, 0, 0.1)", // Default Typst color
stroke:
columnStyles?.columnPost?.stroke ||
globalColumnPosts?.stroke ||
@@ -2017,8 +2022,10 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
}
height={dimensions.barThickness}
fill="black" // Typst uses black
stroke="none"
fill={customStyles?.reckoningBar?.fill || "black"} // Typst default is black
stroke={customStyles?.reckoningBar?.stroke || "none"}
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
opacity={customStyles?.reckoningBar?.opacity ?? 1}
/>
{/* Beads */}