Compare commits

..

29 Commits

Author SHA1 Message Date
semantic-release-bot
9d322301ef chore(abacus-react): release v2.6.0 [skip ci]
# [2.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.5.0...abacus-react-v2.6.0) (2025-11-04)

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
2025-11-04 00:31:11 +00:00
Thomas Hallock
0641eb719e fix(abacus-react): correct column highlighting offset in AbacusStatic
Column highlights were positioned incorrectly - missing the padding offset
that beads and rods include. The highlight rectangles appeared shifted to
the left of the actual columns.

Fixed by adding padding to the X position calculation, matching how beads
and column posts are positioned.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:30:03 -06:00
Thomas Hallock
3588d5acde feat(web): add test page for AbacusStatic Server Component
Creates /test-static-abacus route to demonstrate and verify that
AbacusStatic works correctly in React Server Components.

The page:
- Has NO "use client" directive (pure Server Component)
- Renders 12 different abacus values as static preview cards
- Uses compact mode and hideInactiveBeads for clean displays
- Includes visual confirmation that RSC rendering works

This serves as both a test and reference implementation for
using AbacusStatic in Next.js App Router Server Components.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:29:16 -06:00
Thomas Hallock
74f2d97434 docs(abacus-react): export AbacusStatic and update README
Updates package exports to include:
- AbacusStatic component
- AbacusStaticConfig type

README additions:
- New "Static Display (Server Components)" section
- Comparison table: AbacusStatic vs AbacusReact
- Server Component usage examples
- Updated features list to highlight RSC support

Provides clear guidance on when to use each component based on
requirements (interactivity, animations, SSR/SSG).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:29:16 -06:00
Thomas Hallock
4f9dc4666d docs(abacus-react): add Storybook stories for AbacusStatic
Comprehensive documentation and examples demonstrating:
- Default usage and different values
- All color schemes (place-value, monochrome, heaven-earth, alternating)
- All bead shapes (circle, diamond, square)
- Compact mode for inline displays
- Hide inactive beads
- Theme presets (light, dark, trophy)
- Column highlighting and labels
- Scaling options
- Server Component usage examples
- Preview card grids

Highlights that AbacusStatic shares logic with AbacusReact (no duplication).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:29:16 -06:00
Thomas Hallock
3b8e864cfa feat(abacus-react): add AbacusStatic for React Server Components
Implements a pure, server-component-compatible static abacus renderer
that shares core business logic with AbacusReact without code duplication.

Key features:
- No "use client" directive - works in React Server Components
- No hooks, animations, or client-side JavaScript
- Shares utilities: numberToAbacusState, color scheme logic, positioning
- Supports all visual customization: color schemes, bead shapes, themes
- Full feature parity for static displays: compact mode, hide inactive,
  column highlighting & labels, custom styles

Implementation approach:
- AbacusStaticBead: Pure SVG bead component (no hooks/animations)
- AbacusStatic: Main component using shared AbacusUtils
- Dependency injection pattern: separate rendering paths for client vs server

Bundle impact: +4KB

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:29:16 -06:00
semantic-release-bot
7418adb959 chore(abacus-react): release v2.5.0 [skip ci]
# [2.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.4.0...abacus-react-v2.5.0) (2025-11-03)

### Features

* **abacus-react:** add core utility functions for state management ([e65541c](e65541c100))
* **abacus-react:** add layout and educational props ([35bbcec](35bbcecb9e))
* **abacus-react:** add pre-defined theme presets ([cf1f950](cf1f950c7c))
* **abacus-react:** add React hooks for abacus calculations ([de038d2](de038d2afc))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
2025-11-03 23:18:22 +00:00
Thomas Hallock
7228bbc2eb refactor(web): import utility functions from abacus-react
Re-export core utility functions from @soroban/abacus-react instead of
duplicating implementations:

- beadDiff.ts: Re-exports calculateBeadDiff, calculateBeadDiffFromValues,
  areStatesEqual, etc. Keeps app-specific calculateMultiStepBeadDiffs
  and validateBeadDiff.

- abacusInstructionGenerator.ts: Re-exports numberToAbacusState,
  calculateBeadChanges, and related types. Keeps app-specific
  tutorial generation logic.

- TutorialPlayer.tsx: Import calculateBeadDiffFromValues from abacus-react

- TutorialEditor.tsx: Import calculateBeadDiffFromValues from abacus-react

Eliminates ~200 lines of duplicate utility function implementations.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
ff1d60a233 refactor(web): use compact prop for inline mini-abacus
Replace manual customStyles with compact={true} in AbacusTarget.tsx.

Before: 7 props + customStyles override
After: compact={true} automatically handles frame visibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
9f7f001d74 refactor(web): use ABACUS_THEMES instead of manual style definitions
Replace manual theme style definitions with ABACUS_THEMES presets:
- MyAbacus.tsx: Use ABACUS_THEMES.light and ABACUS_THEMES.trophy
- HeroAbacus.tsx: Use ABACUS_THEMES.light
- LevelSliderDisplay.tsx: Use ABACUS_THEMES.dark

Eliminates ~60 lines of duplicate theme style code.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
35d8734a3a docs(abacus-react): update documentation for new features
Update README.md with:
- Theme presets usage examples
- Compact mode examples
- Column highlighting and labels
- New hooks (useAbacusDiff, useAbacusState)
- Utility functions documentation
- Updated TypeScript imports

Add ENHANCEMENT_PLAN.md:
- Documents the analysis of apps/web customizations
- Implementation roadmap with completion status
- Impact metrics

Add INTEGRATION_SUMMARY.md:
- Summary of integrated features
- Code deduplication metrics (~260-300 lines eliminated)
- Files modified in both packages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
6a1cec06a7 docs(abacus-react): add Storybook stories for new features
Add comprehensive Storybook stories demonstrating:
- All 6 theme presets (light, dark, trophy, translucent, solid, traditional)
- Compact mode for inline displays
- Frame visibility control
- Column highlighting and labels
- useAbacusDiff and useAbacusState hooks
- Utility functions
- Combined feature showcases

Stories are organized under "AbacusReact/Themes & Utilities".

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
ce4e44d630 feat(abacus-react): export new utilities, hooks, and themes
Update index.ts to export:
- ABACUS_THEMES and AbacusThemeName
- Utility functions (numberToAbacusState, calculateBeadDiff, etc.)
- React hooks (useAbacusDiff, useAbacusState)
- New types (BeadState, AbacusState, BeadDiffResult, etc.)

All new features are now available to consumers of the package.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
35bbcecb9e feat(abacus-react): add layout and educational props
Add new props to AbacusConfig:
- frameVisible: Show/hide column posts and reckoning bar
- compact: Compact layout for inline displays (implies frameVisible=false)
- highlightColumns: Highlight specific columns by index
- columnLabels: Optional educational labels for columns

These props simplify common use cases:
- Inline mini-abacus displays now use just compact={true}
- Column highlighting for tutorials without custom overlays
- Frame visibility control for different contexts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
cf1f950c7c feat(abacus-react): add pre-defined theme presets
Add ABACUS_THEMES with 6 pre-defined themes:
- light: Solid white frame (light backgrounds)
- dark: Translucent white with glow (dark backgrounds)
- trophy: Golden frame (achievements/rewards)
- translucent: Nearly invisible (inline/minimal UI)
- solid: Black frame (high contrast/educational)
- traditional: Brown wooden (traditional soroban aesthetic)

Eliminates ~60 lines of duplicate theme style definitions in apps/web.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
de038d2afc feat(abacus-react): add React hooks for abacus calculations
Add AbacusHooks.ts with memoized hooks:
- useAbacusDiff: Calculate bead differences for tutorials
- useAbacusState: Convert numbers to abacus state

These hooks provide performant, memoized access to core utility
functions for use in React components.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
Thomas Hallock
e65541c100 feat(abacus-react): add core utility functions for state management
Add AbacusUtils.ts with comprehensive utility functions:
- numberToAbacusState: Convert numbers to bead positions
- abacusStateToNumber: Convert bead positions to numbers
- calculateBeadChanges: Find bead differences between states
- calculateBeadDiff: Full diff with order and directions
- calculateBeadDiffFromValues: Convenience wrapper
- validateAbacusValue: Validate number ranges
- areStatesEqual: Compare abacus states

These utilities eliminate ~200 lines of duplicate code in apps/web.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:17:17 -06:00
semantic-release-bot
f4ec0689ff chore(release): 4.68.0 [skip ci]
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)

### Features

* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](79f7347d48))
* add 3D printing support for abacus models ([dafdfdd](dafdfdd233))
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add comprehensive Storybook coverage and migration guide ([7a4a37e](7a4a37ec6d))
* add game preview system with mock arcade environment ([25880cc](25880cc7e4))
* add per-player stats tracking system ([613301c](613301cd13))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* add unified trophy abacus with hero mode integration ([6620418](6620418a70))
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](3628426a56))
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](1d525c7b53))
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](2fc0a05f7f))
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](d568955d6a))
* **arcade:** auto-create room when user has none ([ff88c3a](ff88c3a1b8))
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](1461414ef4))
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](780a7161bc))
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](4d8e873358))
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](656f5a7838))
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](6527c26a81))
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](d25b888ffb))
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](fd765335ef))
* **card-sorting:** add green border to correctly positioned cards ([16fca86](16fca86b76)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](3a82099757))
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](c367e0ceec))
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](b0b93d0175))
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](ee7345d641)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](ed6f177914)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](f6ed4a27a2))
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](4ba7f24717))
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](7028cfc511))
* **card-sorting:** gentler spring animation for locked cards ([47189cb](47189cb6e7))
* **card-sorting:** implement continuous bezier curve paths ([2d93024](2d9302410f))
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](0b0503f035))
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](2e7a02c9e4))
* **card-sorting:** optimize results screen for mobile ([d188789](d188789069))
* **card-sorting:** redesign setup screen with modern UI ([73cf967](73cf967492))
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](222dc555fa))
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](8f6feec4f2))
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](c5f39d51eb))
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](e3184dd0d4))
* complete 3D enhancement integration for all three proposals ([5ac55cc](5ac55cc149))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](83d0ba26f5))
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](4d0795a9df))
* dynamically crop favicon to active beads for maximum size ([5670322](567032296a))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](37e330f26e))
* **games:** add autoplay and improve carousel layout ([9f51edf](9f51edfaa9))
* **games:** add horizontal scroll support to carousels ([a224abb](a224abb6f6))
* **games:** add rotating games hero carousel ([24231e6](24231e6b2e))
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](fe9bfeabf9))
* **i18n:** add global language selector to navigation ([0506360](0506360117))
* **i18n:** add homepage translations for all supported languages ([8c9d35a](8c9d35a3b4))
* **i18n:** add Old High German (goh) language support ([b334a15](b334a15255))
* **i18n:** complete Old High German translations for all locales ([0b06a1c](0b06a1ce00))
* **i18n:** internationalize games page and tutorial content ([4253964](4253964af1))
* **i18n:** internationalize homepage with English translations ([40cff14](40cff143c7))
* **i18n:** migrate from react-i18next to next-intl ([9016b76](9016b76024))
* **i18n:** update games page hero section copy ([6333c60](6333c60352))
* install embla-carousel-autoplay for games carousel ([946e5d1](946e5d1910))
* install embla-carousel-react for player profile carousel ([642ae95](642ae95738))
* internationalize guide page with 6 languages ([e9c320b](e9c320bb10))
* internationalize tutorial player ([26d41cf](26d41cfd05))
* optimize card sorting for mobile displays ([b443ee9](b443ee9cdc))
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](6ae4d13dc7))
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](4a78485d2e))
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](d7eb957a8d))
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](c0d6526d30))
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](f457f1a1c2))
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](cae3359587))
* **rithmomachia:** add helpful error messages for failed captures ([b172440](b172440a41))
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](d42bcff0d9))
* **rithmomachia:** Add interactive playing guide modal ([3121d82](3121d8240a))
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](82d89131f0))
* **rithmomachia:** add ratio capture example to guide ([9150b0c](9150b0c678))
* **rithmomachia:** add standalone guide page route ([3fcc79f](3fcc79fe9e))
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](27f1c989d5))
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](275f401e3c))
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](74bc3c0dcf))
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](1d5f01c966))
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](b7fac78829))
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](9fd54067ce))
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](4829e41ea1))
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](0a308016e9))
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](f5558563ea))
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](55aff829f4))
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](9fde1ef9e7))
* **rithmomachia:** guide defaults to docked right on open ([11f674d](11f674d542))
* **rithmomachia:** improve guide pieces section layout ([a270bfc](a270bfc0cc))
* **rithmomachia:** improve guide UX and add persistence ([b314740](b314740697))
* **rithmomachia:** improve roster status notice UX ([e27df45](e27df45256))
* **rithmomachia:** integrate roster warning into game nav ([8a11594](8a11594203))
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](04741971b2))
* **rithmomachia:** recreate original guide modal header layout ([2489695](24896957d0))
* **rithmomachia:** show capture error on hover instead of click ([339b678](339b6780f6))
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](b0c4523c0b))
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](5c186f3947))
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](5c2ddbef05))
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](17d2460a87))
* **rithmomachia:** simplify guide language for clarity ([85cb630](85cb630add))
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](be2a00e8b3))
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](08c97620f5))
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](0769eaaa1d))
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](976a7de949))
* **room-share:** add QR code button for easy mobile joining ([349290a](349290ac6a))
* show rithmomachia turn in nav ([7c89bfe](7c89bfef9c))
* switch to royal color theme with transparent background ([944ad65](944ad6574e)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)

### Bug Fixes

* adjust hero abacus position to avoid covering subtitle ([f03d341](f03d341314))
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](fbcde2505f))
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](7c1c2d7beb))
* **arcade:** implement optimistic locking in session manager ([71fd66d](71fd66d96a))
* board rotation now properly fills height in portrait mode ([b5a96ea](b5a96eaeb1))
* **card-sorting:** add border radius to outer card container ([a922eba](a922eba73c))
* **card-sorting:** add debug logging for spring animations ([d42947e](d42947eb8d))
* **card-sorting:** add missing gameMode support after hard reset ([a832325](a832325deb))
* **card-sorting:** add missing useMemo import ([949d76d](949d76d844))
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](84c66feec6))
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](829c741e55))
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](fc5cf1216f))
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](4dce16cca4))
* **card-sorting:** animate cards from game board to results grid ([17d45fe](17d45fe88c))
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](d02ab5922c))
* **card-sorting:** enable card scaling for spectators ([6b095c3](6b095c3383))
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](f3f6eca1db))
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](ae45298ec4))
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](dc2d94aaa5))
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](4b4fbfef32))
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](5cca279687))
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](79c94699fa))
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](170abed231))
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](275cc62a52))
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](f3687ed236))
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](51368c6ec5))
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](0d8af09517))
* **card-sorting:** preserve rotation when starting drag ([3364144](3364144fb6))
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](a0b14f87e9))
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](bd014bec4f))
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](34785f466f))
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](627b873382))
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](564a00f82b))
* **card-sorting:** prevent replaying own movements from server ([308168a](308168a7fb))
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](30953b8c4a))
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](8aff60ce3f))
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](a44aa5a4c2))
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](15c53ea4eb))
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](f5fb4d7b76))
* **card-sorting:** show only active players in team members section ([fa9f1a5](fa9f1a568f))
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](0eefc332ac))
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](b0cd194838))
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](cee399ed15))
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](f389afa831))
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](6972fdf110))
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](e1258ee041))
* configure favicon metadata and improve bead visibility ([e1369fa](e1369fa275))
* copy entire packages/core and packages/templates ([0ccada0](0ccada0ca7))
* correct hero abacus scroll direction to flow with page content ([4232746](423274657c))
* correct Typst template path in Dockerfile ([4c518de](4c518decb7))
* delete existing user sessions before creating new ones ([0cced47](0cced47a0f))
* extract pure SVG content from AbacusReact renders ([b07f1c4](b07f1c4216))
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](5a8c98fc10))
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](f80a73b35c))
* **games:** use specific transition properties for smooth carousel loop ([187271e](187271e515))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](4d4d930bd3))
* **i18n:** use useMessages() for tutorial translations ([95b0105](95b0105ca3))
* include column posts in favicon bounding box ([0b2f481](0b2f48106a))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](633ff12750))
* Integrate threshold input into Point Victory card ([b29bbee](b29bbeefca))
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](d7b35d9544))
* **nav:** show full navigation on /games page ([d3fe6ac](d3fe6acbb0))
* **qr-button:** improve layout and z-index ([646a422](646a4228d0))
* **qr-button:** increase mini QR code size to 80px ([61ac737](61ac7378bd))
* **qr-button:** increase mini QR code to 84px ([3fae5ea](3fae5ea6fa))
* **qr-button:** make button square and increase QR size ([dc2d466](dc2d46663b))
* **qr-button:** match height of stacked buttons ([81f202d](81f202d215))
* reduce padding to minimize gap below last bead ([0e529be](0e529be789))
* remove distracting parallax and wobble 3D effects ([28a2d40](28a2d40996))
* remove wobble physics and enhance wood grain visibility ([5d97673](5d97673406))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](ed9a050d64))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](26bdb11237))
* **rithmomachia:** add missing i18next dependencies ([91154d9](91154d9364))
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](dae615ee72))
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](cda1126cb0))
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](709322373a))
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](2a91748493))
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](cfac277505))
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](618e56358d))
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](aafb64f3e3))
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](1bcd99c949))
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](14259a19a9))
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](4fa20f44cb))
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](4834ece98e))
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](56f3164155))
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](d0a8fcdea6))
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](a673177bec))
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](a1a0374fac))
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](190f8cf302))
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](774c6b0ce7))
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](54bfd2fac8))
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](8f4a79c9b0))
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](bd49964186))
* **room-info:** hide Leave Room button when user is alone ([5927f61](5927f61c3c))
* separate horizontal and vertical bounding box logic ([83090df](83090df4df))
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](88993f3662))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](096104b094))
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](f8fe6e4a41))
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](f4ffc5b027))
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](440b492e85))
* various game improvements and UI enhancements ([b67cf61](b67cf610c5))

### Performance Improvements

* optimize Docker image size to reduce build failures ([9ca3106](9ca3106361))

### Code Refactoring

* **card-sorting:** remove reveal numbers feature ([ea5e3e8](ea5e3e838b))
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](e4df8432b9))
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](82c133f742))
* **games:** move page title to nav bar ([712ee58](712ee58e59))
* **games:** remove redundant subtitle below nav ([ad5bb87](ad5bb87325))
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](876513c9cc))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* restructure /create page into hub with sub-pages ([b91b23d](b91b23d95f))
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](a0a867b271))
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](f0a066d8f0))
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](eace0ed529))
* **rithmomachia:** extract guide sections into separate files ([765525d](765525dc45))
* **rithmomachia:** extract hooks (phase 5) ([324a659](324a65992f))
* **rithmomachia:** extract phase components (phase 4) ([11364f6](11364f6394))
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](3abc325ea2))
* **rithmomachia:** make setup phase UI more compact ([e55f848](e55f848a26))
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](dfeeb0e0db)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](82a5eb2e4b))
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](0471da598d))
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](2ab6ab5799))
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](0ab7a1df32))
* use AbacusReact for dynamic Open Graph image ([9c20f12](9c20f12bac))

### Documentation

* add 3D enhancement documentation to README ([cc96802](cc96802df8))
* add database migration guide and playing guide modal spec ([5a29af7](5a29af78e2))
* add deployment verification guidelines to prevent false positives ([3d8da23](3d8da2348b))
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](008ccead0f))
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](e3c1f10233))
* update workflow to require manual testing before commits ([0991796](0991796f1e))

### Styles

* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](88ca35e044)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](94e5e6a268)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](7bf2d730d3))

### Tests

* trigger compose-updater deployment test ([2b06aae](2b06aae394))
* verify compose-updater automatic deployment cycle ([af0552c](af0552ccd9))
2025-11-03 23:06:59 +00:00
Thomas Hallock
af0552ccd9 test: verify compose-updater automatic deployment cycle
Add test comment to trigger automatic deployment and verify that
compose-updater successfully updates the container without crashing
with the new separate project names configuration.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 17:04:05 -06:00
semantic-release-bot
90421cfc38 chore(release): 4.68.0 [skip ci]
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)

### Features

* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](79f7347d48))
* add 3D printing support for abacus models ([dafdfdd](dafdfdd233))
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add comprehensive Storybook coverage and migration guide ([7a4a37e](7a4a37ec6d))
* add game preview system with mock arcade environment ([25880cc](25880cc7e4))
* add per-player stats tracking system ([613301c](613301cd13))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* add unified trophy abacus with hero mode integration ([6620418](6620418a70))
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](3628426a56))
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](1d525c7b53))
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](2fc0a05f7f))
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](d568955d6a))
* **arcade:** auto-create room when user has none ([ff88c3a](ff88c3a1b8))
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](1461414ef4))
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](780a7161bc))
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](4d8e873358))
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](656f5a7838))
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](6527c26a81))
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](d25b888ffb))
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](fd765335ef))
* **card-sorting:** add green border to correctly positioned cards ([16fca86](16fca86b76)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](3a82099757))
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](c367e0ceec))
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](b0b93d0175))
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](ee7345d641)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](ed6f177914)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](f6ed4a27a2))
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](4ba7f24717))
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](7028cfc511))
* **card-sorting:** gentler spring animation for locked cards ([47189cb](47189cb6e7))
* **card-sorting:** implement continuous bezier curve paths ([2d93024](2d9302410f))
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](0b0503f035))
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](2e7a02c9e4))
* **card-sorting:** optimize results screen for mobile ([d188789](d188789069))
* **card-sorting:** redesign setup screen with modern UI ([73cf967](73cf967492))
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](222dc555fa))
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](8f6feec4f2))
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](c5f39d51eb))
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](e3184dd0d4))
* complete 3D enhancement integration for all three proposals ([5ac55cc](5ac55cc149))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](83d0ba26f5))
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](4d0795a9df))
* dynamically crop favicon to active beads for maximum size ([5670322](567032296a))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](37e330f26e))
* **games:** add autoplay and improve carousel layout ([9f51edf](9f51edfaa9))
* **games:** add horizontal scroll support to carousels ([a224abb](a224abb6f6))
* **games:** add rotating games hero carousel ([24231e6](24231e6b2e))
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](fe9bfeabf9))
* **i18n:** add global language selector to navigation ([0506360](0506360117))
* **i18n:** add homepage translations for all supported languages ([8c9d35a](8c9d35a3b4))
* **i18n:** add Old High German (goh) language support ([b334a15](b334a15255))
* **i18n:** complete Old High German translations for all locales ([0b06a1c](0b06a1ce00))
* **i18n:** internationalize games page and tutorial content ([4253964](4253964af1))
* **i18n:** internationalize homepage with English translations ([40cff14](40cff143c7))
* **i18n:** migrate from react-i18next to next-intl ([9016b76](9016b76024))
* **i18n:** update games page hero section copy ([6333c60](6333c60352))
* install embla-carousel-autoplay for games carousel ([946e5d1](946e5d1910))
* install embla-carousel-react for player profile carousel ([642ae95](642ae95738))
* internationalize guide page with 6 languages ([e9c320b](e9c320bb10))
* internationalize tutorial player ([26d41cf](26d41cfd05))
* optimize card sorting for mobile displays ([b443ee9](b443ee9cdc))
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](6ae4d13dc7))
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](4a78485d2e))
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](d7eb957a8d))
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](c0d6526d30))
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](f457f1a1c2))
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](cae3359587))
* **rithmomachia:** add helpful error messages for failed captures ([b172440](b172440a41))
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](d42bcff0d9))
* **rithmomachia:** Add interactive playing guide modal ([3121d82](3121d8240a))
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](82d89131f0))
* **rithmomachia:** add ratio capture example to guide ([9150b0c](9150b0c678))
* **rithmomachia:** add standalone guide page route ([3fcc79f](3fcc79fe9e))
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](27f1c989d5))
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](275f401e3c))
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](74bc3c0dcf))
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](1d5f01c966))
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](b7fac78829))
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](9fd54067ce))
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](4829e41ea1))
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](0a308016e9))
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](f5558563ea))
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](55aff829f4))
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](9fde1ef9e7))
* **rithmomachia:** guide defaults to docked right on open ([11f674d](11f674d542))
* **rithmomachia:** improve guide pieces section layout ([a270bfc](a270bfc0cc))
* **rithmomachia:** improve guide UX and add persistence ([b314740](b314740697))
* **rithmomachia:** improve roster status notice UX ([e27df45](e27df45256))
* **rithmomachia:** integrate roster warning into game nav ([8a11594](8a11594203))
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](04741971b2))
* **rithmomachia:** recreate original guide modal header layout ([2489695](24896957d0))
* **rithmomachia:** show capture error on hover instead of click ([339b678](339b6780f6))
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](b0c4523c0b))
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](5c186f3947))
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](5c2ddbef05))
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](17d2460a87))
* **rithmomachia:** simplify guide language for clarity ([85cb630](85cb630add))
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](be2a00e8b3))
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](08c97620f5))
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](0769eaaa1d))
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](976a7de949))
* **room-share:** add QR code button for easy mobile joining ([349290a](349290ac6a))
* show rithmomachia turn in nav ([7c89bfe](7c89bfef9c))
* switch to royal color theme with transparent background ([944ad65](944ad6574e)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)

### Bug Fixes

* adjust hero abacus position to avoid covering subtitle ([f03d341](f03d341314))
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](fbcde2505f))
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](7c1c2d7beb))
* **arcade:** implement optimistic locking in session manager ([71fd66d](71fd66d96a))
* board rotation now properly fills height in portrait mode ([b5a96ea](b5a96eaeb1))
* **card-sorting:** add border radius to outer card container ([a922eba](a922eba73c))
* **card-sorting:** add debug logging for spring animations ([d42947e](d42947eb8d))
* **card-sorting:** add missing gameMode support after hard reset ([a832325](a832325deb))
* **card-sorting:** add missing useMemo import ([949d76d](949d76d844))
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](84c66feec6))
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](829c741e55))
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](fc5cf1216f))
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](4dce16cca4))
* **card-sorting:** animate cards from game board to results grid ([17d45fe](17d45fe88c))
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](d02ab5922c))
* **card-sorting:** enable card scaling for spectators ([6b095c3](6b095c3383))
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](f3f6eca1db))
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](ae45298ec4))
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](dc2d94aaa5))
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](4b4fbfef32))
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](5cca279687))
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](79c94699fa))
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](170abed231))
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](275cc62a52))
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](f3687ed236))
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](51368c6ec5))
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](0d8af09517))
* **card-sorting:** preserve rotation when starting drag ([3364144](3364144fb6))
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](a0b14f87e9))
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](bd014bec4f))
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](34785f466f))
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](627b873382))
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](564a00f82b))
* **card-sorting:** prevent replaying own movements from server ([308168a](308168a7fb))
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](30953b8c4a))
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](8aff60ce3f))
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](a44aa5a4c2))
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](15c53ea4eb))
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](f5fb4d7b76))
* **card-sorting:** show only active players in team members section ([fa9f1a5](fa9f1a568f))
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](0eefc332ac))
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](b0cd194838))
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](cee399ed15))
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](f389afa831))
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](6972fdf110))
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](e1258ee041))
* configure favicon metadata and improve bead visibility ([e1369fa](e1369fa275))
* copy entire packages/core and packages/templates ([0ccada0](0ccada0ca7))
* correct hero abacus scroll direction to flow with page content ([4232746](423274657c))
* correct Typst template path in Dockerfile ([4c518de](4c518decb7))
* delete existing user sessions before creating new ones ([0cced47](0cced47a0f))
* extract pure SVG content from AbacusReact renders ([b07f1c4](b07f1c4216))
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](5a8c98fc10))
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](f80a73b35c))
* **games:** use specific transition properties for smooth carousel loop ([187271e](187271e515))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](4d4d930bd3))
* **i18n:** use useMessages() for tutorial translations ([95b0105](95b0105ca3))
* include column posts in favicon bounding box ([0b2f481](0b2f48106a))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](633ff12750))
* Integrate threshold input into Point Victory card ([b29bbee](b29bbeefca))
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](d7b35d9544))
* **nav:** show full navigation on /games page ([d3fe6ac](d3fe6acbb0))
* **qr-button:** improve layout and z-index ([646a422](646a4228d0))
* **qr-button:** increase mini QR code size to 80px ([61ac737](61ac7378bd))
* **qr-button:** increase mini QR code to 84px ([3fae5ea](3fae5ea6fa))
* **qr-button:** make button square and increase QR size ([dc2d466](dc2d46663b))
* **qr-button:** match height of stacked buttons ([81f202d](81f202d215))
* reduce padding to minimize gap below last bead ([0e529be](0e529be789))
* remove distracting parallax and wobble 3D effects ([28a2d40](28a2d40996))
* remove wobble physics and enhance wood grain visibility ([5d97673](5d97673406))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](ed9a050d64))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](26bdb11237))
* **rithmomachia:** add missing i18next dependencies ([91154d9](91154d9364))
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](dae615ee72))
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](cda1126cb0))
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](709322373a))
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](2a91748493))
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](cfac277505))
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](618e56358d))
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](aafb64f3e3))
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](1bcd99c949))
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](14259a19a9))
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](4fa20f44cb))
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](4834ece98e))
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](56f3164155))
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](d0a8fcdea6))
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](a673177bec))
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](a1a0374fac))
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](190f8cf302))
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](774c6b0ce7))
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](54bfd2fac8))
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](8f4a79c9b0))
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](bd49964186))
* **room-info:** hide Leave Room button when user is alone ([5927f61](5927f61c3c))
* separate horizontal and vertical bounding box logic ([83090df](83090df4df))
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](88993f3662))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](096104b094))
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](f8fe6e4a41))
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](f4ffc5b027))
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](440b492e85))
* various game improvements and UI enhancements ([b67cf61](b67cf610c5))

### Performance Improvements

* optimize Docker image size to reduce build failures ([9ca3106](9ca3106361))

### Code Refactoring

* **card-sorting:** remove reveal numbers feature ([ea5e3e8](ea5e3e838b))
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](e4df8432b9))
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](82c133f742))
* **games:** move page title to nav bar ([712ee58](712ee58e59))
* **games:** remove redundant subtitle below nav ([ad5bb87](ad5bb87325))
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](876513c9cc))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* restructure /create page into hub with sub-pages ([b91b23d](b91b23d95f))
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](a0a867b271))
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](f0a066d8f0))
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](eace0ed529))
* **rithmomachia:** extract guide sections into separate files ([765525d](765525dc45))
* **rithmomachia:** extract hooks (phase 5) ([324a659](324a65992f))
* **rithmomachia:** extract phase components (phase 4) ([11364f6](11364f6394))
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](3abc325ea2))
* **rithmomachia:** make setup phase UI more compact ([e55f848](e55f848a26))
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](dfeeb0e0db)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](82a5eb2e4b))
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](0471da598d))
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](2ab6ab5799))
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](0ab7a1df32))
* use AbacusReact for dynamic Open Graph image ([9c20f12](9c20f12bac))

### Documentation

* add 3D enhancement documentation to README ([cc96802](cc96802df8))
* add database migration guide and playing guide modal spec ([5a29af7](5a29af78e2))
* add deployment verification guidelines to prevent false positives ([3d8da23](3d8da2348b))
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](008ccead0f))
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](e3c1f10233))
* update workflow to require manual testing before commits ([0991796](0991796f1e))

### Styles

* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](88ca35e044)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](94e5e6a268)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](7bf2d730d3))

### Tests

* trigger compose-updater deployment test ([2b06aae](2b06aae394))
2025-11-03 22:34:58 +00:00
Thomas Hallock
2b06aae394 test: trigger compose-updater deployment test
Testing that compose-updater successfully updates without crashing itself
2025-11-03 16:32:13 -06:00
semantic-release-bot
ee53bb9a9d chore(abacus-react): release v2.4.0 [skip ci]
# [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.3.0...abacus-react-v2.4.0) (2025-11-03)

### Bug Fixes

* remove distracting parallax and wobble 3D effects ([28a2d40](28a2d40996))
* remove wobble physics and enhance wood grain visibility ([5d97673](5d97673406))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](26bdb11237))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](096104b094))

### Features

* complete 3D enhancement integration for all three proposals ([5ac55cc](5ac55cc149))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](37e330f26e))
2025-11-03 21:42:52 +00:00
Thomas Hallock
28a2d40996 fix: remove distracting parallax and wobble 3D effects
Remove all parallax hover effects and wobble physics from 3D enhancement system
as they were distracting and worsened the user experience.

Changes:
- Remove Abacus3DPhysics interface completely
- Remove physics3d prop from AbacusConfig and component
- Remove calculateParallaxOffset utility function
- Remove mouse tracking infrastructure (containerRef, mousePos, handleMouseMove)
- Update enhanced3d type to only support 'subtle' | 'realistic' (removed 'delightful')
- Update all 3D utilities to remove delightful mode support
- Remove all Delightful stories from Storybook
- Update Interactive Playground to remove parallax controls
- Change MyAbacus from delightful to realistic mode

The 3D enhancement system now focuses purely on visual improvements
(materials, lighting, textures) without any motion-based effects.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
37e330f26e feat: enable 3D enhancement on hero/open MyAbacus modes
Added delightful 3D mode to the hero and open states of MyAbacus:
- Glossy heaven beads + satin earth beads for premium feel
- Dramatic lighting for impact
- Wood grain texture on golden frame
- Hover parallax enabled for interactive depth
- Only applies to hero/open modes (not button mode)

The giant hero abacus on the home page now has satisfying
material rendering and interactive parallax effects.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
cc96802df8 docs: add 3D enhancement documentation to README
Added comprehensive 3D enhancement section covering:
- Subtle mode (CSS perspective + shadows)
- Realistic mode (materials, lighting, wood grain)
- Delightful mode (physics + parallax)

Includes code examples and explanations of all options.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
5d97673406 fix: remove wobble physics and enhance wood grain visibility
**Changes:**
- Removed wobble physics feature (was janky and distracting)
- Increased wood grain opacity from 0.15 → 0.4 (realistic) and 0.45 (delightful)
- Enhanced wood grain pattern with bolder strokes and more visible knots
- Removed getWobbleRotation utility function
- Simplified Abacus3DPhysics interface to only hoverParallax
- Updated all stories to remove wobble references
- Removed velocity tracking code from Bead component

Wood grain is now much more visible on frame elements without
affecting bead spacing or layout.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
26bdb11237 fix: rewrite 3D stories to use props instead of CSS wrappers
The original stories were only applying CSS classes externally,
which meant none of the actual 3D features were working:
- No material gradients (glossy/satin/matte)
- No wood grain textures
- No wobble physics
- No hover parallax

Now properly passing enhanced3d, material3d, and physics3d props
to showcase all features:

**Stories added:**
- Compare All Levels (side-by-side)
- Material showcases (glossy, satin, matte, mixed)
- Wood grain frame demo
- Lighting comparison (top-down, ambient, dramatic)
- Wobble physics demo
- Hover parallax demo
- Interactive Playground with live controls

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
5ac55cc149 feat: complete 3D enhancement integration for all three proposals
Fully integrated the 3D enhancement system into AbacusReact component:

**Proposal 1: Subtle (CSS Perspective + Shadows)**
- Container classes with perspective transforms
- CSS-based depth shadows on beads and frame

**Proposal 2: Realistic (Lighting + Materials)**
- Material gradients (glossy/satin/matte) for beads via SVG radial gradients
- Wood grain texture overlays for frame elements (rods & reckoning bar)
- Lighting filter effects (top-down/ambient/dramatic)
- Enhanced physics config for realistic motion

**Proposal 3: Delightful (Physics + Micro-interactions)**
- Advanced physics with overshoot and bounce
- Wobble rotation on bead movement based on velocity tracking
- Hover parallax tracking with Z-depth lift
- Mouse position tracking for interactive parallax effects

Implementation details:
- Pass enhanced3d, material3d, physics3d props down to Bead component
- Generate SVG gradient defs for material rendering (realistic/delightful)
- Apply gradients to bead shapes based on material type
- Calculate parallax offsets using Abacus3DUtils
- Track velocity for wobble rotation effects
- Add wood grain texture pattern to frame elements
- Enhanced React Spring physics config per enhancement level
- Container ref and mouse tracking for parallax

All three proposals work end-to-end with existing configurations.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
Thomas Hallock
096104b094 fix: use absolute positioning for hero abacus to eliminate scroll lag
Replace laggy JavaScript scroll tracking with proper CSS positioning:

Before:
- Used position: fixed with JavaScript scroll listener
- Calculated top position dynamically: calc(60vh - ${scrollY}px)
- Caused noticeable lag on mobile and slower browsers

After:
- Hero mode: position: absolute (scrolls naturally with document)
- Button mode: position: fixed (stays in viewport at bottom-right)
- Open mode: position: fixed (stays in viewport at center)
- Zero JavaScript scroll tracking needed

Result: Buttery smooth scrolling on all devices with zero lag.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 15:41:49 -06:00
31 changed files with 4185 additions and 1155 deletions

View File

@@ -1,6 +1,543 @@
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
### Features
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))
* add 3D printing support for abacus models ([dafdfdd](https://github.com/antialias/soroban-abacus-flashcards/commit/dafdfdd233b53464b9825a8a9b5f2e6206fc54cb))
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](https://github.com/antialias/soroban-abacus-flashcards/commit/0922ea10b77e7d16b8c414c596d23cb11e20c1cc))
* add comprehensive Storybook coverage and migration guide ([7a4a37e](https://github.com/antialias/soroban-abacus-flashcards/commit/7a4a37ec6d0171782778e18122da782f069e0556))
* add game preview system with mock arcade environment ([25880cc](https://github.com/antialias/soroban-abacus-flashcards/commit/25880cc7e463f98a5a23c812c1ffd43734d3fe1f))
* add per-player stats tracking system ([613301c](https://github.com/antialias/soroban-abacus-flashcards/commit/613301cd137ad6f712571a0be45c708ce391fc8f))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](https://github.com/antialias/soroban-abacus-flashcards/commit/81ead65680892efa4d0ab07e7f0ef77eb1bc1405))
* add unified trophy abacus with hero mode integration ([6620418](https://github.com/antialias/soroban-abacus-flashcards/commit/6620418a704dcca810b511a5f394084521104e6b))
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](https://github.com/antialias/soroban-abacus-flashcards/commit/3628426a567d7e0273be75cce64632ae04b7d5eb))
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](https://github.com/antialias/soroban-abacus-flashcards/commit/1d525c7b5320984a1582b8ab7eae57895c728428))
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](https://github.com/antialias/soroban-abacus-flashcards/commit/2fc0a05f7f557cee55f7d31b585499dd04e68ff9))
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](https://github.com/antialias/soroban-abacus-flashcards/commit/d568955d6abf389e6ab7c6979e33122a65917a46))
* **arcade:** auto-create room when user has none ([ff88c3a](https://github.com/antialias/soroban-abacus-flashcards/commit/ff88c3a1b81703a87a1d57eeb5cc139da7d9df04))
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](https://github.com/antialias/soroban-abacus-flashcards/commit/1461414ef4d0b213af241213447c91eed1abe5fb))
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](https://github.com/antialias/soroban-abacus-flashcards/commit/780a7161bc05c2ca6597d7d8d89f01afd33d9f4d))
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](https://github.com/antialias/soroban-abacus-flashcards/commit/4d8e873358271fe3fd50b228aea8277e20aa5966))
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](https://github.com/antialias/soroban-abacus-flashcards/commit/656f5a7838ed6003c214ec484d4c37072270fa8d))
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](https://github.com/antialias/soroban-abacus-flashcards/commit/6527c26a8166b23f074e85eb335a15800c1947a2))
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](https://github.com/antialias/soroban-abacus-flashcards/commit/d25b888ffb3915d2d482442ab708ba3e159af512))
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](https://github.com/antialias/soroban-abacus-flashcards/commit/fd765335efbc91366c596c7789b92882cd3379d9))
* **card-sorting:** add green border to correctly positioned cards ([16fca86](https://github.com/antialias/soroban-abacus-flashcards/commit/16fca86b7687115f1cf565c533a512e92946e3a8)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](https://github.com/antialias/soroban-abacus-flashcards/commit/3a8209975728cdcf914c43ba08339454a9e2457f))
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](https://github.com/antialias/soroban-abacus-flashcards/commit/c367e0ceece41d8e7c2bc8aebe3239ff6053a115))
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](https://github.com/antialias/soroban-abacus-flashcards/commit/b0b93d0175c8a1c8958d6ba346d969c234fdd6ff))
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](https://github.com/antialias/soroban-abacus-flashcards/commit/ee7345d641e0ee72915afb9cdbd6d284b7e238bd)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](https://github.com/antialias/soroban-abacus-flashcards/commit/ed6f1779141d0bc9dff2d532a3dfc638015936b5)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](https://github.com/antialias/soroban-abacus-flashcards/commit/f6ed4a27a26d8bfa495ba5f580a446286b9674a0))
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](https://github.com/antialias/soroban-abacus-flashcards/commit/4ba7f247175d93e4d339e2be7bbdb2e009992232))
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](https://github.com/antialias/soroban-abacus-flashcards/commit/7028cfc51164e9219479e6040b03c29239aa7edb))
* **card-sorting:** gentler spring animation for locked cards ([47189cb](https://github.com/antialias/soroban-abacus-flashcards/commit/47189cb6e79ed2915f5ddcc9cb3626540dfb07f3))
* **card-sorting:** implement continuous bezier curve paths ([2d93024](https://github.com/antialias/soroban-abacus-flashcards/commit/2d9302410f5e98145a435b00df3ae5fcf3f4c0b5))
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](https://github.com/antialias/soroban-abacus-flashcards/commit/0b0503f0354a4a82fe6b9bfe827729e8e5a9e329))
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](https://github.com/antialias/soroban-abacus-flashcards/commit/2e7a02c9e4ab84e821d58661d6e7a326f7882afb))
* **card-sorting:** optimize results screen for mobile ([d188789](https://github.com/antialias/soroban-abacus-flashcards/commit/d188789069b4c350ce3cc0d221bd4a43dab528e0))
* **card-sorting:** redesign setup screen with modern UI ([73cf967](https://github.com/antialias/soroban-abacus-flashcards/commit/73cf96749234c480482f62392245b38c1fd5f0a0))
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](https://github.com/antialias/soroban-abacus-flashcards/commit/222dc555fa5068e2594dcc074e33f70320f5742c))
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](https://github.com/antialias/soroban-abacus-flashcards/commit/8f6feec4f21d0af0d1c98daf5017eddd91d3d578))
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](https://github.com/antialias/soroban-abacus-flashcards/commit/c5f39d51eb45ec816f32151dc7f9d7c06360474b))
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](https://github.com/antialias/soroban-abacus-flashcards/commit/e3184dd0d444e5dc204731f5b396d5c553cf7d11))
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](https://github.com/antialias/soroban-abacus-flashcards/commit/83d0ba26f5eeec3e189d279710d5bbcf13e82f29))
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](https://github.com/antialias/soroban-abacus-flashcards/commit/4d0795a9df74fcb085af821eafb923bdcb5f0b0c))
* dynamically crop favicon to active beads for maximum size ([5670322](https://github.com/antialias/soroban-abacus-flashcards/commit/567032296aecaad13408bdc17d108ec7c57fb4a8))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
* **games:** add autoplay and improve carousel layout ([9f51edf](https://github.com/antialias/soroban-abacus-flashcards/commit/9f51edfaa95c14f55a30a6eceafb9099eeed437f))
* **games:** add horizontal scroll support to carousels ([a224abb](https://github.com/antialias/soroban-abacus-flashcards/commit/a224abb6f660e1aa31ab04f5590b003fae072af9))
* **games:** add rotating games hero carousel ([24231e6](https://github.com/antialias/soroban-abacus-flashcards/commit/24231e6b2ebbdcae066344df54e7e80e7d221128))
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](https://github.com/antialias/soroban-abacus-flashcards/commit/fe9bfeabf9ee66923501b18e1b69f2d666d0817d))
* **i18n:** add global language selector to navigation ([0506360](https://github.com/antialias/soroban-abacus-flashcards/commit/0506360117807665e8f5a6fcd8f1178339f6e65c))
* **i18n:** add homepage translations for all supported languages ([8c9d35a](https://github.com/antialias/soroban-abacus-flashcards/commit/8c9d35a3b43dd29664f5afb1bd96c4e584d9ec75))
* **i18n:** add Old High German (goh) language support ([b334a15](https://github.com/antialias/soroban-abacus-flashcards/commit/b334a15255ed9fa29beb43de66da0288691390c6))
* **i18n:** complete Old High German translations for all locales ([0b06a1c](https://github.com/antialias/soroban-abacus-flashcards/commit/0b06a1ce005d92e7ae9c225aba40d240e965753d))
* **i18n:** internationalize games page and tutorial content ([4253964](https://github.com/antialias/soroban-abacus-flashcards/commit/4253964af19f9aaa16f2394f41819223542fb519))
* **i18n:** internationalize homepage with English translations ([40cff14](https://github.com/antialias/soroban-abacus-flashcards/commit/40cff143c72e9228d7cce607cab64c4a6d067017))
* **i18n:** migrate from react-i18next to next-intl ([9016b76](https://github.com/antialias/soroban-abacus-flashcards/commit/9016b760247a20271255839e4dd7e5b9a8353b9f))
* **i18n:** update games page hero section copy ([6333c60](https://github.com/antialias/soroban-abacus-flashcards/commit/6333c60352b920916afd81cc3b0229706a1519fa))
* install embla-carousel-autoplay for games carousel ([946e5d1](https://github.com/antialias/soroban-abacus-flashcards/commit/946e5d19107020992be8945f8fe7c41e4bc2a0e2))
* install embla-carousel-react for player profile carousel ([642ae95](https://github.com/antialias/soroban-abacus-flashcards/commit/642ae957383cfe1d6045f645bbe426fd80c56f35))
* internationalize guide page with 6 languages ([e9c320b](https://github.com/antialias/soroban-abacus-flashcards/commit/e9c320bb1032e94c3852b9459236409da4669c09))
* internationalize tutorial player ([26d41cf](https://github.com/antialias/soroban-abacus-flashcards/commit/26d41cfd058bfdf5b61ee6e20cfc61cbecb32f45))
* optimize card sorting for mobile displays ([b443ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/b443ee9cdcd9fcb7674845d8c92f7c338ad98dea))
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](https://github.com/antialias/soroban-abacus-flashcards/commit/6ae4d13dc784a87f85206c6ff6d005e5b23b678c))
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](https://github.com/antialias/soroban-abacus-flashcards/commit/4a78485d2e20f2cbf36cc898a1beafa8eb48bfbf))
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](https://github.com/antialias/soroban-abacus-flashcards/commit/d7eb957a8dabbcac35e166a83dd679a628e19baa))
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](https://github.com/antialias/soroban-abacus-flashcards/commit/c0d6526d30aca8deaeda2b7c2e27eb37af8b577c))
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](https://github.com/antialias/soroban-abacus-flashcards/commit/f457f1a1c22b6cb7fff23a7701474322cf423dd9))
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](https://github.com/antialias/soroban-abacus-flashcards/commit/cae335958751c27684bfb10c8e2e526b460954ed))
* **rithmomachia:** add helpful error messages for failed captures ([b172440](https://github.com/antialias/soroban-abacus-flashcards/commit/b172440a41e958ced98903bb8f4c2e4b423e1356))
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](https://github.com/antialias/soroban-abacus-flashcards/commit/d42bcff0d922895549c1c12f8e02a3ae6d53425a))
* **rithmomachia:** Add interactive playing guide modal ([3121d82](https://github.com/antialias/soroban-abacus-flashcards/commit/3121d8240a567817f5f205a4ef4a788fcf451f71))
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](https://github.com/antialias/soroban-abacus-flashcards/commit/82d89131f00517f162ec496397cb390f9ecfc52e))
* **rithmomachia:** add ratio capture example to guide ([9150b0c](https://github.com/antialias/soroban-abacus-flashcards/commit/9150b0c678ce7104fe984ee0fc93748b43a245f4))
* **rithmomachia:** add standalone guide page route ([3fcc79f](https://github.com/antialias/soroban-abacus-flashcards/commit/3fcc79fe9eae11d4bd3a724c1b1f7d086e7cae81))
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](https://github.com/antialias/soroban-abacus-flashcards/commit/27f1c989d59a19844b90a5148ae27fb97161da2d))
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](https://github.com/antialias/soroban-abacus-flashcards/commit/275f401e3c25b75fec4700a8c2d4be6e33f0afe9))
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](https://github.com/antialias/soroban-abacus-flashcards/commit/74bc3c0dcf8d1ee7084e88a04861a85f9b623809))
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](https://github.com/antialias/soroban-abacus-flashcards/commit/1d5f01c966cf1eec9a9c19ee37f1cad93c89df40))
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](https://github.com/antialias/soroban-abacus-flashcards/commit/b7fac788292e00c6060a47fdbcca89a7e7fee35c))
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](https://github.com/antialias/soroban-abacus-flashcards/commit/9fd54067ce257e028b02f4784568ff3f2bbb32ca))
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](https://github.com/antialias/soroban-abacus-flashcards/commit/4829e41ea13fae2edec10837e65e505929445782))
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](https://github.com/antialias/soroban-abacus-flashcards/commit/0a308016e9d6a926c52dbfc5623b60b169d16d03))
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](https://github.com/antialias/soroban-abacus-flashcards/commit/f5558563ea93ef7428aa220c2e15e3f02711420f))
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](https://github.com/antialias/soroban-abacus-flashcards/commit/55aff829f4c284e8cfe6d471c0821575928b93bc))
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](https://github.com/antialias/soroban-abacus-flashcards/commit/9fde1ef9e703e26b2450128155b53fdf2d2e1fe5))
* **rithmomachia:** guide defaults to docked right on open ([11f674d](https://github.com/antialias/soroban-abacus-flashcards/commit/11f674d542ea5e4e88bd60ff1068451805d9766e))
* **rithmomachia:** improve guide pieces section layout ([a270bfc](https://github.com/antialias/soroban-abacus-flashcards/commit/a270bfc0cc4a3b6b54ba43a5af14a227cc7d29f9))
* **rithmomachia:** improve guide UX and add persistence ([b314740](https://github.com/antialias/soroban-abacus-flashcards/commit/b31474069734350a7059cd7c73255a7e11b78eb9))
* **rithmomachia:** improve roster status notice UX ([e27df45](https://github.com/antialias/soroban-abacus-flashcards/commit/e27df45256147f958ca215f9dd1f4e133e8cf06c))
* **rithmomachia:** integrate roster warning into game nav ([8a11594](https://github.com/antialias/soroban-abacus-flashcards/commit/8a11594203fb91faee6cbc4cb74367164ecd6d85))
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](https://github.com/antialias/soroban-abacus-flashcards/commit/04741971b296976f4476ecd949e84066fc549010))
* **rithmomachia:** recreate original guide modal header layout ([2489695](https://github.com/antialias/soroban-abacus-flashcards/commit/24896957d0817758c5f64c0e3473e6a0a343af67))
* **rithmomachia:** show capture error on hover instead of click ([339b678](https://github.com/antialias/soroban-abacus-flashcards/commit/339b6780f657ace5bfe1611c4ef64bb0c2c31587))
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](https://github.com/antialias/soroban-abacus-flashcards/commit/b0c4523c0b4669c96a50b2812ba6cb2faa3f9a22))
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](https://github.com/antialias/soroban-abacus-flashcards/commit/5c186f3947cc38f1f5db5de3e68e590b90c2d092))
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](https://github.com/antialias/soroban-abacus-flashcards/commit/5c2ddbef05d7f4195d21b084cb1c0c4193ee3c9c))
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](https://github.com/antialias/soroban-abacus-flashcards/commit/17d2460a8769a21d33fabc5f909cf5b939712d36))
* **rithmomachia:** simplify guide language for clarity ([85cb630](https://github.com/antialias/soroban-abacus-flashcards/commit/85cb630add395a6693ecbbe9c8fc6aaf8c47be29))
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](https://github.com/antialias/soroban-abacus-flashcards/commit/be2a00e8b366b5606525309b4c7813f5c35c7f7c))
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](https://github.com/antialias/soroban-abacus-flashcards/commit/08c97620f5e694b8526c448c44d265e6dd1fe1eb))
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](https://github.com/antialias/soroban-abacus-flashcards/commit/0769eaaa1dc238b901e3a7cfe0486e6122d5eda9))
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](https://github.com/antialias/soroban-abacus-flashcards/commit/976a7de949c22842f4b6da3ced990f502a1c2733))
* **room-share:** add QR code button for easy mobile joining ([349290a](https://github.com/antialias/soroban-abacus-flashcards/commit/349290ac6a411651686b64d2e6b540083d2df1d9))
* show rithmomachia turn in nav ([7c89bfe](https://github.com/antialias/soroban-abacus-flashcards/commit/7c89bfef9c60db0e2c46e920500dcc1fbe90d3df))
* switch to royal color theme with transparent background ([944ad65](https://github.com/antialias/soroban-abacus-flashcards/commit/944ad6574e01a67ce1fdbb1f2452fe632c78ce43)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)
### Bug Fixes
* adjust hero abacus position to avoid covering subtitle ([f03d341](https://github.com/antialias/soroban-abacus-flashcards/commit/f03d3413145cc7ddfba93728ecdec7eabea9ada6))
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](https://github.com/antialias/soroban-abacus-flashcards/commit/fbcde2505f7ff2bf3426f3458e480c4548314ba4))
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](https://github.com/antialias/soroban-abacus-flashcards/commit/7c1c2d7bebbb9a1acb274d17dd43b6ee5d196f44))
* **arcade:** implement optimistic locking in session manager ([71fd66d](https://github.com/antialias/soroban-abacus-flashcards/commit/71fd66d96a3b03650c90f59f6e516aae7dddc345))
* board rotation now properly fills height in portrait mode ([b5a96ea](https://github.com/antialias/soroban-abacus-flashcards/commit/b5a96eaeb1e29c20304142a7a0adf62f1cef570f))
* **card-sorting:** add border radius to outer card container ([a922eba](https://github.com/antialias/soroban-abacus-flashcards/commit/a922eba73c4656ee941ce4dfb1dc57a62f076570))
* **card-sorting:** add debug logging for spring animations ([d42947e](https://github.com/antialias/soroban-abacus-flashcards/commit/d42947eb8d5d3d8298f5d3b3d1644891c268dbb6))
* **card-sorting:** add missing gameMode support after hard reset ([a832325](https://github.com/antialias/soroban-abacus-flashcards/commit/a832325debde289d6928c5e6f9c24311c5e079ad))
* **card-sorting:** add missing useMemo import ([949d76d](https://github.com/antialias/soroban-abacus-flashcards/commit/949d76d844c786ada8a6373e4abb7f498f6befb9))
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](https://github.com/antialias/soroban-abacus-flashcards/commit/84c66feec6b4112b015e1afd95bf33b24b5f6a4f))
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](https://github.com/antialias/soroban-abacus-flashcards/commit/829c741e554d1490dd7a5bbc17f2a32f7195dc07))
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](https://github.com/antialias/soroban-abacus-flashcards/commit/fc5cf1216fe03edfb7e44afda01192f4b97b4f4e))
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](https://github.com/antialias/soroban-abacus-flashcards/commit/4dce16cca46c965199b7e09f8b34bfa221efac33))
* **card-sorting:** animate cards from game board to results grid ([17d45fe](https://github.com/antialias/soroban-abacus-flashcards/commit/17d45fe88cd9773f5e550f6ee5a7f0c82cca2023))
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](https://github.com/antialias/soroban-abacus-flashcards/commit/d02ab5922c416042d525f54097a6975ae1541586))
* **card-sorting:** enable card scaling for spectators ([6b095c3](https://github.com/antialias/soroban-abacus-flashcards/commit/6b095c33830341c46139bc847ddaab3db632265e))
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](https://github.com/antialias/soroban-abacus-flashcards/commit/f3f6eca1db30df9e1e34cc4e77a069a6a3954f3d))
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](https://github.com/antialias/soroban-abacus-flashcards/commit/ae45298ec48efb29587c0a1c1a7986a72821f3ef))
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d94aaa58531ed4f9047e2ca92724d9264643d))
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](https://github.com/antialias/soroban-abacus-flashcards/commit/4b4fbfef322ecda06020ad52d4b1788267112460))
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](https://github.com/antialias/soroban-abacus-flashcards/commit/5cca279687d8973d25bd9a411a55b632d1c82f63))
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](https://github.com/antialias/soroban-abacus-flashcards/commit/79c94699fa1cc2a2886e3ab1addc5fcd975602f5))
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](https://github.com/antialias/soroban-abacus-flashcards/commit/170abed2318432f309de40692f6092bb4c4a1a45))
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](https://github.com/antialias/soroban-abacus-flashcards/commit/275cc62a523d9e849f2162001141b6d75ae0925e))
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](https://github.com/antialias/soroban-abacus-flashcards/commit/f3687ed236eff4ebe61699ec02909024c7086fb5))
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](https://github.com/antialias/soroban-abacus-flashcards/commit/51368c6ec59d5447ce2875c5e1181dec97fd509d))
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](https://github.com/antialias/soroban-abacus-flashcards/commit/0d8af09517534f1e1cf1f57160391d465a279d76))
* **card-sorting:** preserve rotation when starting drag ([3364144](https://github.com/antialias/soroban-abacus-flashcards/commit/3364144fb6212934b6ad6d63ac6e7b78b436b258))
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](https://github.com/antialias/soroban-abacus-flashcards/commit/a0b14f87e9c5b32fcbb685da4e70c563f70ed91a))
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](https://github.com/antialias/soroban-abacus-flashcards/commit/bd014bec4ffa12bcd8f4a4e84ff51203c90c1f1d))
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](https://github.com/antialias/soroban-abacus-flashcards/commit/34785f466faaa6b9f2958df786af88561fa80b06))
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](https://github.com/antialias/soroban-abacus-flashcards/commit/627b873382eaa76ad16477280d10451cf2951e1a))
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](https://github.com/antialias/soroban-abacus-flashcards/commit/564a00f82b6ca6aa8a2c0586ca49fc42d44991a8))
* **card-sorting:** prevent replaying own movements from server ([308168a](https://github.com/antialias/soroban-abacus-flashcards/commit/308168a7fb51013b0851e98b161ba1a1a3e39fbb))
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](https://github.com/antialias/soroban-abacus-flashcards/commit/30953b8c4a3cf147f980455818f9ce8eea07837c))
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](https://github.com/antialias/soroban-abacus-flashcards/commit/8aff60ce3f8d302ce5c1bde7cb773e63064c36b7))
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](https://github.com/antialias/soroban-abacus-flashcards/commit/a44aa5a4c2d84cab7cf0bbf87485bb61548fdeb2))
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](https://github.com/antialias/soroban-abacus-flashcards/commit/15c53ea4eb4abb824eb0360fb645b1f3e455578e))
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](https://github.com/antialias/soroban-abacus-flashcards/commit/f5fb4d7b76e25286bcdecd017894ff2d78b31963))
* **card-sorting:** show only active players in team members section ([fa9f1a5](https://github.com/antialias/soroban-abacus-flashcards/commit/fa9f1a568f3dff2f4e5e7d3e8841b951ef1b7d04))
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](https://github.com/antialias/soroban-abacus-flashcards/commit/0eefc332ac2724c54b477301a269915e895db94f))
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](https://github.com/antialias/soroban-abacus-flashcards/commit/b0cd194838705bb7bbf21ac9e318eaba491097b2))
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](https://github.com/antialias/soroban-abacus-flashcards/commit/cee399ed1513d32d0fff51a6f63898aa861605e1))
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](https://github.com/antialias/soroban-abacus-flashcards/commit/f389afa831935e896a626f526cfee378e340a64b))
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](https://github.com/antialias/soroban-abacus-flashcards/commit/6972fdf1105b6e854494efe1c4c587e6b6ff32a9))
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](https://github.com/antialias/soroban-abacus-flashcards/commit/e1258ee0416010909774694c0b25306b6f30329c))
* configure favicon metadata and improve bead visibility ([e1369fa](https://github.com/antialias/soroban-abacus-flashcards/commit/e1369fa2754cd61745a2950e6cb767d6b08db38f))
* copy entire packages/core and packages/templates ([0ccada0](https://github.com/antialias/soroban-abacus-flashcards/commit/0ccada0ca783e635f9ae08f33a69c392018ee342))
* correct hero abacus scroll direction to flow with page content ([4232746](https://github.com/antialias/soroban-abacus-flashcards/commit/423274657c9698bba28f7246fbf48d8508d97ef9))
* correct Typst template path in Dockerfile ([4c518de](https://github.com/antialias/soroban-abacus-flashcards/commit/4c518decb7fcc0b519d07680cbfd01c94c23dd41))
* delete existing user sessions before creating new ones ([0cced47](https://github.com/antialias/soroban-abacus-flashcards/commit/0cced47a0f414a04371bdb253fc5a43e4d9557be))
* extract pure SVG content from AbacusReact renders ([b07f1c4](https://github.com/antialias/soroban-abacus-flashcards/commit/b07f1c421616bcfd1f949f9a42ce1b03df418945))
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](https://github.com/antialias/soroban-abacus-flashcards/commit/5a8c98fc10704e459690308a84dc7ee2bfa0ef6c))
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](https://github.com/antialias/soroban-abacus-flashcards/commit/f80a73b35c324959bfd7141ebf086cb47d3c0ebc))
* **games:** use specific transition properties for smooth carousel loop ([187271e](https://github.com/antialias/soroban-abacus-flashcards/commit/187271e51527ee0129f71d77be1bd24072b963c4))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](https://github.com/antialias/soroban-abacus-flashcards/commit/4d4d930bd307ce5a405fc5751af6682a9f221f1f))
* **i18n:** use useMessages() for tutorial translations ([95b0105](https://github.com/antialias/soroban-abacus-flashcards/commit/95b0105ca3b28c5adfa843e8d77a8b27d9e7ade4))
* include column posts in favicon bounding box ([0b2f481](https://github.com/antialias/soroban-abacus-flashcards/commit/0b2f48106a939307b728c86fe2ea1be1e0247ea8))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](https://github.com/antialias/soroban-abacus-flashcards/commit/633ff127500c893a215491afa0e6ff814ad553bf))
* Integrate threshold input into Point Victory card ([b29bbee](https://github.com/antialias/soroban-abacus-flashcards/commit/b29bbeefcad92be42f7a3ca27ac126db4232ab26))
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](https://github.com/antialias/soroban-abacus-flashcards/commit/d7b35d954421fd7577cd2c26247666e5953b647d))
* **nav:** show full navigation on /games page ([d3fe6ac](https://github.com/antialias/soroban-abacus-flashcards/commit/d3fe6acbb0390e1df71869a4095e5ee6021e06b1))
* **qr-button:** improve layout and z-index ([646a422](https://github.com/antialias/soroban-abacus-flashcards/commit/646a4228d0573796b1a429e31bc037411024c0ff))
* **qr-button:** increase mini QR code size to 80px ([61ac737](https://github.com/antialias/soroban-abacus-flashcards/commit/61ac7378bdb01132b26bfc265a057c095ea41606))
* **qr-button:** increase mini QR code to 84px ([3fae5ea](https://github.com/antialias/soroban-abacus-flashcards/commit/3fae5ea6fa9ebd0f8fe8c9140a027be7f6a041aa))
* **qr-button:** make button square and increase QR size ([dc2d466](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d46663b8e0ec94a1508a57c4f8c2d8ba03506))
* **qr-button:** match height of stacked buttons ([81f202d](https://github.com/antialias/soroban-abacus-flashcards/commit/81f202d21556aa430402fda814519adbc8883831))
* reduce padding to minimize gap below last bead ([0e529be](https://github.com/antialias/soroban-abacus-flashcards/commit/0e529be789caf16e73f3e2ee77f52e243841aef4))
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](https://github.com/antialias/soroban-abacus-flashcards/commit/ed9a050d64db905e1328008f25dc0014e9a81999))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
* **rithmomachia:** add missing i18next dependencies ([91154d9](https://github.com/antialias/soroban-abacus-flashcards/commit/91154d93647e59f7e5f96d1db5624a7ec9b1b9ff))
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](https://github.com/antialias/soroban-abacus-flashcards/commit/dae615ee72a7ec7d0b235a22c61ebc4af0d8eadb))
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](https://github.com/antialias/soroban-abacus-flashcards/commit/cda1126cb0eab6840df89f3a8778d72410298093))
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](https://github.com/antialias/soroban-abacus-flashcards/commit/709322373a91c8174d21052d184fa84dd8bda326))
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](https://github.com/antialias/soroban-abacus-flashcards/commit/2a917484938bc269cf16acb501d4d26584405e0f))
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](https://github.com/antialias/soroban-abacus-flashcards/commit/cfac27750526fb1f6a7e4314a96aab3b92e08e44))
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](https://github.com/antialias/soroban-abacus-flashcards/commit/618e56358deb66cba968472f39b8d4e28b4dd211))
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](https://github.com/antialias/soroban-abacus-flashcards/commit/aafb64f3e337c6cf925766fe179b91f66c4a040b))
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](https://github.com/antialias/soroban-abacus-flashcards/commit/1bcd99c949e4d2b4fb1c0813debd50176fa58cb9))
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](https://github.com/antialias/soroban-abacus-flashcards/commit/14259a19a9817d0947467faa004d5f43118f8d8d))
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](https://github.com/antialias/soroban-abacus-flashcards/commit/4fa20f44cb9758f29d1f1512232be0fdc0b53b3d))
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](https://github.com/antialias/soroban-abacus-flashcards/commit/4834ece98e86f2fb00511bb876a5c32c289df0e0))
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](https://github.com/antialias/soroban-abacus-flashcards/commit/56f3164155beb94ceec2838bed9fc74fd75524db))
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](https://github.com/antialias/soroban-abacus-flashcards/commit/d0a8fcdea6aa4fdacfee33e183c92923634ee2b7))
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](https://github.com/antialias/soroban-abacus-flashcards/commit/a673177bec1c709463ce0f266848f473a79f4ef0))
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a0374fac5dce676df5890663b75531589ed93a))
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](https://github.com/antialias/soroban-abacus-flashcards/commit/190f8cf302aa966f029d05931811e217c67bfe39))
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](https://github.com/antialias/soroban-abacus-flashcards/commit/774c6b0ce712b1a77bb684457da9831e6ec91138))
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](https://github.com/antialias/soroban-abacus-flashcards/commit/54bfd2fac86be3597d40c67a1235e4c4ed8e2709))
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](https://github.com/antialias/soroban-abacus-flashcards/commit/8f4a79c9b0cad55336584fdc8e67409015d3a8ae))
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](https://github.com/antialias/soroban-abacus-flashcards/commit/bd49964186a0daa1639ae849b128a76081643daf))
* **room-info:** hide Leave Room button when user is alone ([5927f61](https://github.com/antialias/soroban-abacus-flashcards/commit/5927f61c3c34ba583ee45c8cee48a116c1c03071))
* separate horizontal and vertical bounding box logic ([83090df](https://github.com/antialias/soroban-abacus-flashcards/commit/83090df4dfad1d1d5cfa6c278c241526cacc7972))
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](https://github.com/antialias/soroban-abacus-flashcards/commit/88993f36629206a7bdcf9aa9d5641f1580b64de5))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](https://github.com/antialias/soroban-abacus-flashcards/commit/f8fe6e4a415f8655626af567129d0cda61b82e15))
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](https://github.com/antialias/soroban-abacus-flashcards/commit/f4ffc5b0277535358bea7588309a1a4afd1983a1))
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](https://github.com/antialias/soroban-abacus-flashcards/commit/440b492e85beff1612697346b6c5cfc8461e83da))
* various game improvements and UI enhancements ([b67cf61](https://github.com/antialias/soroban-abacus-flashcards/commit/b67cf610c570d54744553cd8f6694243fa50bee1))
### Performance Improvements
* optimize Docker image size to reduce build failures ([9ca3106](https://github.com/antialias/soroban-abacus-flashcards/commit/9ca310636183f4970db925ce8fa368e23645eb02))
### Code Refactoring
* **card-sorting:** remove reveal numbers feature ([ea5e3e8](https://github.com/antialias/soroban-abacus-flashcards/commit/ea5e3e838bd6a5b8b38469a70aa92a0e9baba769))
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](https://github.com/antialias/soroban-abacus-flashcards/commit/e4df8432b9c4a2055d47833d56b6e9fcf325ca94))
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](https://github.com/antialias/soroban-abacus-flashcards/commit/82c133f742f3f5c40b723c18d1997b518f25b320))
* **games:** move page title to nav bar ([712ee58](https://github.com/antialias/soroban-abacus-flashcards/commit/712ee58e5956e5bbdb13d5a5fb367020c87c8c9a))
* **games:** remove redundant subtitle below nav ([ad5bb87](https://github.com/antialias/soroban-abacus-flashcards/commit/ad5bb87325a44825f0cd85b38eb0e5f0eea7a695))
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](https://github.com/antialias/soroban-abacus-flashcards/commit/876513c9cc6323c20845ae8f1a3a5478d449f9e4))
* reorganize Harmony and Victory guide sections ([fb629c4](https://github.com/antialias/soroban-abacus-flashcards/commit/fb629c44ea37a7b296561919a4980c10d14efed8))
* restructure /create page into hub with sub-pages ([b91b23d](https://github.com/antialias/soroban-abacus-flashcards/commit/b91b23d95ffaeeaa30dbc8579f4c30bae8829ee7))
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](https://github.com/antialias/soroban-abacus-flashcards/commit/a0a867b27166a838ca7e0dcbd8f89fe1be812a80))
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](https://github.com/antialias/soroban-abacus-flashcards/commit/f0a066d8f0a51d35e18f87a8436c0d05153c03b5))
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](https://github.com/antialias/soroban-abacus-flashcards/commit/eace0ed52979b71870f77ee68f8568558f2aaecb))
* **rithmomachia:** extract guide sections into separate files ([765525d](https://github.com/antialias/soroban-abacus-flashcards/commit/765525dc451897f561f017e444aae892dc27177f))
* **rithmomachia:** extract hooks (phase 5) ([324a659](https://github.com/antialias/soroban-abacus-flashcards/commit/324a65992f97c295ea3968aaf54d266373f4c035))
* **rithmomachia:** extract phase components (phase 4) ([11364f6](https://github.com/antialias/soroban-abacus-flashcards/commit/11364f6394c15e49850e5cad2cbd32e1ea08a178))
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](https://github.com/antialias/soroban-abacus-flashcards/commit/3abc325ea27feee5c4cc59f02296ff218f342a81))
* **rithmomachia:** make setup phase UI more compact ([e55f848](https://github.com/antialias/soroban-abacus-flashcards/commit/e55f848a26092a2b4a5b09c3c255544ea9666f1b))
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](https://github.com/antialias/soroban-abacus-flashcards/commit/dfeeb0e0db8b2c4a38198cf71cd918439d6c211b)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](https://github.com/antialias/soroban-abacus-flashcards/commit/82a5eb2e4bf74f42a183a15f1129e5ec84cc5231))
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](https://github.com/antialias/soroban-abacus-flashcards/commit/0471da598d8d591b3f9d63f467cb35f999924c13))
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/2ab6ab57995a6d7d9c66b9fba8de945507209661))
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab7a1df327d7258228af9851762555583a20d61))
* use AbacusReact for dynamic Open Graph image ([9c20f12](https://github.com/antialias/soroban-abacus-flashcards/commit/9c20f12bacff4fe7f8bd7a87032afbed9711e94b))
### Documentation
* add 3D enhancement documentation to README ([cc96802](https://github.com/antialias/soroban-abacus-flashcards/commit/cc96802df87c805c946ee59af509663ba570e75b))
* add database migration guide and playing guide modal spec ([5a29af7](https://github.com/antialias/soroban-abacus-flashcards/commit/5a29af78e27e897ab35273611b79c4b669304f71))
* add deployment verification guidelines to prevent false positives ([3d8da23](https://github.com/antialias/soroban-abacus-flashcards/commit/3d8da2348b4e8a227e963791d15dc6718eac5af1))
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](https://github.com/antialias/soroban-abacus-flashcards/commit/008ccead0f9c634fe52fd156e6f9a04d6cdd7744))
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](https://github.com/antialias/soroban-abacus-flashcards/commit/e3c1f10233cc0924ff96a643c7c4c1f1278de3e3))
* update workflow to require manual testing before commits ([0991796](https://github.com/antialias/soroban-abacus-flashcards/commit/0991796f1eccef345f10205e675e4c33d1a62b17))
### Styles
* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](https://github.com/antialias/soroban-abacus-flashcards/commit/88ca35e0440157ff9349e8d3d2d3cc844f18ffea)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](https://github.com/antialias/soroban-abacus-flashcards/commit/94e5e6a268b387380b88b192737bd55578b98bc7)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](https://github.com/antialias/soroban-abacus-flashcards/commit/7bf2d730d370e562486b229f4d209099ff8c4463))
### Tests
* trigger compose-updater deployment test ([2b06aae](https://github.com/antialias/soroban-abacus-flashcards/commit/2b06aae39474cc80d501c47c9685fa99e7120c48))
* verify compose-updater automatic deployment cycle ([af0552c](https://github.com/antialias/soroban-abacus-flashcards/commit/af0552ccd98f7b5a62d6e4074b7d87b3716af698))
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
### Features
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))
* add 3D printing support for abacus models ([dafdfdd](https://github.com/antialias/soroban-abacus-flashcards/commit/dafdfdd233b53464b9825a8a9b5f2e6206fc54cb))
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](https://github.com/antialias/soroban-abacus-flashcards/commit/0922ea10b77e7d16b8c414c596d23cb11e20c1cc))
* add comprehensive Storybook coverage and migration guide ([7a4a37e](https://github.com/antialias/soroban-abacus-flashcards/commit/7a4a37ec6d0171782778e18122da782f069e0556))
* add game preview system with mock arcade environment ([25880cc](https://github.com/antialias/soroban-abacus-flashcards/commit/25880cc7e463f98a5a23c812c1ffd43734d3fe1f))
* add per-player stats tracking system ([613301c](https://github.com/antialias/soroban-abacus-flashcards/commit/613301cd137ad6f712571a0be45c708ce391fc8f))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](https://github.com/antialias/soroban-abacus-flashcards/commit/81ead65680892efa4d0ab07e7f0ef77eb1bc1405))
* add unified trophy abacus with hero mode integration ([6620418](https://github.com/antialias/soroban-abacus-flashcards/commit/6620418a704dcca810b511a5f394084521104e6b))
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](https://github.com/antialias/soroban-abacus-flashcards/commit/3628426a567d7e0273be75cce64632ae04b7d5eb))
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](https://github.com/antialias/soroban-abacus-flashcards/commit/1d525c7b5320984a1582b8ab7eae57895c728428))
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](https://github.com/antialias/soroban-abacus-flashcards/commit/2fc0a05f7f557cee55f7d31b585499dd04e68ff9))
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](https://github.com/antialias/soroban-abacus-flashcards/commit/d568955d6abf389e6ab7c6979e33122a65917a46))
* **arcade:** auto-create room when user has none ([ff88c3a](https://github.com/antialias/soroban-abacus-flashcards/commit/ff88c3a1b81703a87a1d57eeb5cc139da7d9df04))
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](https://github.com/antialias/soroban-abacus-flashcards/commit/1461414ef4d0b213af241213447c91eed1abe5fb))
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](https://github.com/antialias/soroban-abacus-flashcards/commit/780a7161bc05c2ca6597d7d8d89f01afd33d9f4d))
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](https://github.com/antialias/soroban-abacus-flashcards/commit/4d8e873358271fe3fd50b228aea8277e20aa5966))
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](https://github.com/antialias/soroban-abacus-flashcards/commit/656f5a7838ed6003c214ec484d4c37072270fa8d))
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](https://github.com/antialias/soroban-abacus-flashcards/commit/6527c26a8166b23f074e85eb335a15800c1947a2))
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](https://github.com/antialias/soroban-abacus-flashcards/commit/d25b888ffb3915d2d482442ab708ba3e159af512))
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](https://github.com/antialias/soroban-abacus-flashcards/commit/fd765335efbc91366c596c7789b92882cd3379d9))
* **card-sorting:** add green border to correctly positioned cards ([16fca86](https://github.com/antialias/soroban-abacus-flashcards/commit/16fca86b7687115f1cf565c533a512e92946e3a8)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](https://github.com/antialias/soroban-abacus-flashcards/commit/3a8209975728cdcf914c43ba08339454a9e2457f))
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](https://github.com/antialias/soroban-abacus-flashcards/commit/c367e0ceece41d8e7c2bc8aebe3239ff6053a115))
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](https://github.com/antialias/soroban-abacus-flashcards/commit/b0b93d0175c8a1c8958d6ba346d969c234fdd6ff))
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](https://github.com/antialias/soroban-abacus-flashcards/commit/ee7345d641e0ee72915afb9cdbd6d284b7e238bd)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](https://github.com/antialias/soroban-abacus-flashcards/commit/ed6f1779141d0bc9dff2d532a3dfc638015936b5)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](https://github.com/antialias/soroban-abacus-flashcards/commit/f6ed4a27a26d8bfa495ba5f580a446286b9674a0))
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](https://github.com/antialias/soroban-abacus-flashcards/commit/4ba7f247175d93e4d339e2be7bbdb2e009992232))
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](https://github.com/antialias/soroban-abacus-flashcards/commit/7028cfc51164e9219479e6040b03c29239aa7edb))
* **card-sorting:** gentler spring animation for locked cards ([47189cb](https://github.com/antialias/soroban-abacus-flashcards/commit/47189cb6e79ed2915f5ddcc9cb3626540dfb07f3))
* **card-sorting:** implement continuous bezier curve paths ([2d93024](https://github.com/antialias/soroban-abacus-flashcards/commit/2d9302410f5e98145a435b00df3ae5fcf3f4c0b5))
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](https://github.com/antialias/soroban-abacus-flashcards/commit/0b0503f0354a4a82fe6b9bfe827729e8e5a9e329))
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](https://github.com/antialias/soroban-abacus-flashcards/commit/2e7a02c9e4ab84e821d58661d6e7a326f7882afb))
* **card-sorting:** optimize results screen for mobile ([d188789](https://github.com/antialias/soroban-abacus-flashcards/commit/d188789069b4c350ce3cc0d221bd4a43dab528e0))
* **card-sorting:** redesign setup screen with modern UI ([73cf967](https://github.com/antialias/soroban-abacus-flashcards/commit/73cf96749234c480482f62392245b38c1fd5f0a0))
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](https://github.com/antialias/soroban-abacus-flashcards/commit/222dc555fa5068e2594dcc074e33f70320f5742c))
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](https://github.com/antialias/soroban-abacus-flashcards/commit/8f6feec4f21d0af0d1c98daf5017eddd91d3d578))
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](https://github.com/antialias/soroban-abacus-flashcards/commit/c5f39d51eb45ec816f32151dc7f9d7c06360474b))
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](https://github.com/antialias/soroban-abacus-flashcards/commit/e3184dd0d444e5dc204731f5b396d5c553cf7d11))
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](https://github.com/antialias/soroban-abacus-flashcards/commit/83d0ba26f5eeec3e189d279710d5bbcf13e82f29))
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](https://github.com/antialias/soroban-abacus-flashcards/commit/4d0795a9df74fcb085af821eafb923bdcb5f0b0c))
* dynamically crop favicon to active beads for maximum size ([5670322](https://github.com/antialias/soroban-abacus-flashcards/commit/567032296aecaad13408bdc17d108ec7c57fb4a8))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
* **games:** add autoplay and improve carousel layout ([9f51edf](https://github.com/antialias/soroban-abacus-flashcards/commit/9f51edfaa95c14f55a30a6eceafb9099eeed437f))
* **games:** add horizontal scroll support to carousels ([a224abb](https://github.com/antialias/soroban-abacus-flashcards/commit/a224abb6f660e1aa31ab04f5590b003fae072af9))
* **games:** add rotating games hero carousel ([24231e6](https://github.com/antialias/soroban-abacus-flashcards/commit/24231e6b2ebbdcae066344df54e7e80e7d221128))
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](https://github.com/antialias/soroban-abacus-flashcards/commit/fe9bfeabf9ee66923501b18e1b69f2d666d0817d))
* **i18n:** add global language selector to navigation ([0506360](https://github.com/antialias/soroban-abacus-flashcards/commit/0506360117807665e8f5a6fcd8f1178339f6e65c))
* **i18n:** add homepage translations for all supported languages ([8c9d35a](https://github.com/antialias/soroban-abacus-flashcards/commit/8c9d35a3b43dd29664f5afb1bd96c4e584d9ec75))
* **i18n:** add Old High German (goh) language support ([b334a15](https://github.com/antialias/soroban-abacus-flashcards/commit/b334a15255ed9fa29beb43de66da0288691390c6))
* **i18n:** complete Old High German translations for all locales ([0b06a1c](https://github.com/antialias/soroban-abacus-flashcards/commit/0b06a1ce005d92e7ae9c225aba40d240e965753d))
* **i18n:** internationalize games page and tutorial content ([4253964](https://github.com/antialias/soroban-abacus-flashcards/commit/4253964af19f9aaa16f2394f41819223542fb519))
* **i18n:** internationalize homepage with English translations ([40cff14](https://github.com/antialias/soroban-abacus-flashcards/commit/40cff143c72e9228d7cce607cab64c4a6d067017))
* **i18n:** migrate from react-i18next to next-intl ([9016b76](https://github.com/antialias/soroban-abacus-flashcards/commit/9016b760247a20271255839e4dd7e5b9a8353b9f))
* **i18n:** update games page hero section copy ([6333c60](https://github.com/antialias/soroban-abacus-flashcards/commit/6333c60352b920916afd81cc3b0229706a1519fa))
* install embla-carousel-autoplay for games carousel ([946e5d1](https://github.com/antialias/soroban-abacus-flashcards/commit/946e5d19107020992be8945f8fe7c41e4bc2a0e2))
* install embla-carousel-react for player profile carousel ([642ae95](https://github.com/antialias/soroban-abacus-flashcards/commit/642ae957383cfe1d6045f645bbe426fd80c56f35))
* internationalize guide page with 6 languages ([e9c320b](https://github.com/antialias/soroban-abacus-flashcards/commit/e9c320bb1032e94c3852b9459236409da4669c09))
* internationalize tutorial player ([26d41cf](https://github.com/antialias/soroban-abacus-flashcards/commit/26d41cfd058bfdf5b61ee6e20cfc61cbecb32f45))
* optimize card sorting for mobile displays ([b443ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/b443ee9cdcd9fcb7674845d8c92f7c338ad98dea))
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](https://github.com/antialias/soroban-abacus-flashcards/commit/6ae4d13dc784a87f85206c6ff6d005e5b23b678c))
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](https://github.com/antialias/soroban-abacus-flashcards/commit/4a78485d2e20f2cbf36cc898a1beafa8eb48bfbf))
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](https://github.com/antialias/soroban-abacus-flashcards/commit/d7eb957a8dabbcac35e166a83dd679a628e19baa))
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](https://github.com/antialias/soroban-abacus-flashcards/commit/c0d6526d30aca8deaeda2b7c2e27eb37af8b577c))
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](https://github.com/antialias/soroban-abacus-flashcards/commit/f457f1a1c22b6cb7fff23a7701474322cf423dd9))
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](https://github.com/antialias/soroban-abacus-flashcards/commit/cae335958751c27684bfb10c8e2e526b460954ed))
* **rithmomachia:** add helpful error messages for failed captures ([b172440](https://github.com/antialias/soroban-abacus-flashcards/commit/b172440a41e958ced98903bb8f4c2e4b423e1356))
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](https://github.com/antialias/soroban-abacus-flashcards/commit/d42bcff0d922895549c1c12f8e02a3ae6d53425a))
* **rithmomachia:** Add interactive playing guide modal ([3121d82](https://github.com/antialias/soroban-abacus-flashcards/commit/3121d8240a567817f5f205a4ef4a788fcf451f71))
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](https://github.com/antialias/soroban-abacus-flashcards/commit/82d89131f00517f162ec496397cb390f9ecfc52e))
* **rithmomachia:** add ratio capture example to guide ([9150b0c](https://github.com/antialias/soroban-abacus-flashcards/commit/9150b0c678ce7104fe984ee0fc93748b43a245f4))
* **rithmomachia:** add standalone guide page route ([3fcc79f](https://github.com/antialias/soroban-abacus-flashcards/commit/3fcc79fe9eae11d4bd3a724c1b1f7d086e7cae81))
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](https://github.com/antialias/soroban-abacus-flashcards/commit/27f1c989d59a19844b90a5148ae27fb97161da2d))
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](https://github.com/antialias/soroban-abacus-flashcards/commit/275f401e3c25b75fec4700a8c2d4be6e33f0afe9))
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](https://github.com/antialias/soroban-abacus-flashcards/commit/74bc3c0dcf8d1ee7084e88a04861a85f9b623809))
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](https://github.com/antialias/soroban-abacus-flashcards/commit/1d5f01c966cf1eec9a9c19ee37f1cad93c89df40))
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](https://github.com/antialias/soroban-abacus-flashcards/commit/b7fac788292e00c6060a47fdbcca89a7e7fee35c))
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](https://github.com/antialias/soroban-abacus-flashcards/commit/9fd54067ce257e028b02f4784568ff3f2bbb32ca))
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](https://github.com/antialias/soroban-abacus-flashcards/commit/4829e41ea13fae2edec10837e65e505929445782))
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](https://github.com/antialias/soroban-abacus-flashcards/commit/0a308016e9d6a926c52dbfc5623b60b169d16d03))
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](https://github.com/antialias/soroban-abacus-flashcards/commit/f5558563ea93ef7428aa220c2e15e3f02711420f))
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](https://github.com/antialias/soroban-abacus-flashcards/commit/55aff829f4c284e8cfe6d471c0821575928b93bc))
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](https://github.com/antialias/soroban-abacus-flashcards/commit/9fde1ef9e703e26b2450128155b53fdf2d2e1fe5))
* **rithmomachia:** guide defaults to docked right on open ([11f674d](https://github.com/antialias/soroban-abacus-flashcards/commit/11f674d542ea5e4e88bd60ff1068451805d9766e))
* **rithmomachia:** improve guide pieces section layout ([a270bfc](https://github.com/antialias/soroban-abacus-flashcards/commit/a270bfc0cc4a3b6b54ba43a5af14a227cc7d29f9))
* **rithmomachia:** improve guide UX and add persistence ([b314740](https://github.com/antialias/soroban-abacus-flashcards/commit/b31474069734350a7059cd7c73255a7e11b78eb9))
* **rithmomachia:** improve roster status notice UX ([e27df45](https://github.com/antialias/soroban-abacus-flashcards/commit/e27df45256147f958ca215f9dd1f4e133e8cf06c))
* **rithmomachia:** integrate roster warning into game nav ([8a11594](https://github.com/antialias/soroban-abacus-flashcards/commit/8a11594203fb91faee6cbc4cb74367164ecd6d85))
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](https://github.com/antialias/soroban-abacus-flashcards/commit/04741971b296976f4476ecd949e84066fc549010))
* **rithmomachia:** recreate original guide modal header layout ([2489695](https://github.com/antialias/soroban-abacus-flashcards/commit/24896957d0817758c5f64c0e3473e6a0a343af67))
* **rithmomachia:** show capture error on hover instead of click ([339b678](https://github.com/antialias/soroban-abacus-flashcards/commit/339b6780f657ace5bfe1611c4ef64bb0c2c31587))
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](https://github.com/antialias/soroban-abacus-flashcards/commit/b0c4523c0b4669c96a50b2812ba6cb2faa3f9a22))
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](https://github.com/antialias/soroban-abacus-flashcards/commit/5c186f3947cc38f1f5db5de3e68e590b90c2d092))
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](https://github.com/antialias/soroban-abacus-flashcards/commit/5c2ddbef05d7f4195d21b084cb1c0c4193ee3c9c))
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](https://github.com/antialias/soroban-abacus-flashcards/commit/17d2460a8769a21d33fabc5f909cf5b939712d36))
* **rithmomachia:** simplify guide language for clarity ([85cb630](https://github.com/antialias/soroban-abacus-flashcards/commit/85cb630add395a6693ecbbe9c8fc6aaf8c47be29))
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](https://github.com/antialias/soroban-abacus-flashcards/commit/be2a00e8b366b5606525309b4c7813f5c35c7f7c))
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](https://github.com/antialias/soroban-abacus-flashcards/commit/08c97620f5e694b8526c448c44d265e6dd1fe1eb))
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](https://github.com/antialias/soroban-abacus-flashcards/commit/0769eaaa1dc238b901e3a7cfe0486e6122d5eda9))
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](https://github.com/antialias/soroban-abacus-flashcards/commit/976a7de949c22842f4b6da3ced990f502a1c2733))
* **room-share:** add QR code button for easy mobile joining ([349290a](https://github.com/antialias/soroban-abacus-flashcards/commit/349290ac6a411651686b64d2e6b540083d2df1d9))
* show rithmomachia turn in nav ([7c89bfe](https://github.com/antialias/soroban-abacus-flashcards/commit/7c89bfef9c60db0e2c46e920500dcc1fbe90d3df))
* switch to royal color theme with transparent background ([944ad65](https://github.com/antialias/soroban-abacus-flashcards/commit/944ad6574e01a67ce1fdbb1f2452fe632c78ce43)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)
### Bug Fixes
* adjust hero abacus position to avoid covering subtitle ([f03d341](https://github.com/antialias/soroban-abacus-flashcards/commit/f03d3413145cc7ddfba93728ecdec7eabea9ada6))
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](https://github.com/antialias/soroban-abacus-flashcards/commit/fbcde2505f7ff2bf3426f3458e480c4548314ba4))
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](https://github.com/antialias/soroban-abacus-flashcards/commit/7c1c2d7bebbb9a1acb274d17dd43b6ee5d196f44))
* **arcade:** implement optimistic locking in session manager ([71fd66d](https://github.com/antialias/soroban-abacus-flashcards/commit/71fd66d96a3b03650c90f59f6e516aae7dddc345))
* board rotation now properly fills height in portrait mode ([b5a96ea](https://github.com/antialias/soroban-abacus-flashcards/commit/b5a96eaeb1e29c20304142a7a0adf62f1cef570f))
* **card-sorting:** add border radius to outer card container ([a922eba](https://github.com/antialias/soroban-abacus-flashcards/commit/a922eba73c4656ee941ce4dfb1dc57a62f076570))
* **card-sorting:** add debug logging for spring animations ([d42947e](https://github.com/antialias/soroban-abacus-flashcards/commit/d42947eb8d5d3d8298f5d3b3d1644891c268dbb6))
* **card-sorting:** add missing gameMode support after hard reset ([a832325](https://github.com/antialias/soroban-abacus-flashcards/commit/a832325debde289d6928c5e6f9c24311c5e079ad))
* **card-sorting:** add missing useMemo import ([949d76d](https://github.com/antialias/soroban-abacus-flashcards/commit/949d76d844c786ada8a6373e4abb7f498f6befb9))
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](https://github.com/antialias/soroban-abacus-flashcards/commit/84c66feec6b4112b015e1afd95bf33b24b5f6a4f))
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](https://github.com/antialias/soroban-abacus-flashcards/commit/829c741e554d1490dd7a5bbc17f2a32f7195dc07))
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](https://github.com/antialias/soroban-abacus-flashcards/commit/fc5cf1216fe03edfb7e44afda01192f4b97b4f4e))
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](https://github.com/antialias/soroban-abacus-flashcards/commit/4dce16cca46c965199b7e09f8b34bfa221efac33))
* **card-sorting:** animate cards from game board to results grid ([17d45fe](https://github.com/antialias/soroban-abacus-flashcards/commit/17d45fe88cd9773f5e550f6ee5a7f0c82cca2023))
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](https://github.com/antialias/soroban-abacus-flashcards/commit/d02ab5922c416042d525f54097a6975ae1541586))
* **card-sorting:** enable card scaling for spectators ([6b095c3](https://github.com/antialias/soroban-abacus-flashcards/commit/6b095c33830341c46139bc847ddaab3db632265e))
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](https://github.com/antialias/soroban-abacus-flashcards/commit/f3f6eca1db30df9e1e34cc4e77a069a6a3954f3d))
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](https://github.com/antialias/soroban-abacus-flashcards/commit/ae45298ec48efb29587c0a1c1a7986a72821f3ef))
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d94aaa58531ed4f9047e2ca92724d9264643d))
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](https://github.com/antialias/soroban-abacus-flashcards/commit/4b4fbfef322ecda06020ad52d4b1788267112460))
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](https://github.com/antialias/soroban-abacus-flashcards/commit/5cca279687d8973d25bd9a411a55b632d1c82f63))
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](https://github.com/antialias/soroban-abacus-flashcards/commit/79c94699fa1cc2a2886e3ab1addc5fcd975602f5))
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](https://github.com/antialias/soroban-abacus-flashcards/commit/170abed2318432f309de40692f6092bb4c4a1a45))
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](https://github.com/antialias/soroban-abacus-flashcards/commit/275cc62a523d9e849f2162001141b6d75ae0925e))
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](https://github.com/antialias/soroban-abacus-flashcards/commit/f3687ed236eff4ebe61699ec02909024c7086fb5))
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](https://github.com/antialias/soroban-abacus-flashcards/commit/51368c6ec59d5447ce2875c5e1181dec97fd509d))
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](https://github.com/antialias/soroban-abacus-flashcards/commit/0d8af09517534f1e1cf1f57160391d465a279d76))
* **card-sorting:** preserve rotation when starting drag ([3364144](https://github.com/antialias/soroban-abacus-flashcards/commit/3364144fb6212934b6ad6d63ac6e7b78b436b258))
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](https://github.com/antialias/soroban-abacus-flashcards/commit/a0b14f87e9c5b32fcbb685da4e70c563f70ed91a))
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](https://github.com/antialias/soroban-abacus-flashcards/commit/bd014bec4ffa12bcd8f4a4e84ff51203c90c1f1d))
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](https://github.com/antialias/soroban-abacus-flashcards/commit/34785f466faaa6b9f2958df786af88561fa80b06))
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](https://github.com/antialias/soroban-abacus-flashcards/commit/627b873382eaa76ad16477280d10451cf2951e1a))
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](https://github.com/antialias/soroban-abacus-flashcards/commit/564a00f82b6ca6aa8a2c0586ca49fc42d44991a8))
* **card-sorting:** prevent replaying own movements from server ([308168a](https://github.com/antialias/soroban-abacus-flashcards/commit/308168a7fb51013b0851e98b161ba1a1a3e39fbb))
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](https://github.com/antialias/soroban-abacus-flashcards/commit/30953b8c4a3cf147f980455818f9ce8eea07837c))
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](https://github.com/antialias/soroban-abacus-flashcards/commit/8aff60ce3f8d302ce5c1bde7cb773e63064c36b7))
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](https://github.com/antialias/soroban-abacus-flashcards/commit/a44aa5a4c2d84cab7cf0bbf87485bb61548fdeb2))
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](https://github.com/antialias/soroban-abacus-flashcards/commit/15c53ea4eb4abb824eb0360fb645b1f3e455578e))
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](https://github.com/antialias/soroban-abacus-flashcards/commit/f5fb4d7b76e25286bcdecd017894ff2d78b31963))
* **card-sorting:** show only active players in team members section ([fa9f1a5](https://github.com/antialias/soroban-abacus-flashcards/commit/fa9f1a568f3dff2f4e5e7d3e8841b951ef1b7d04))
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](https://github.com/antialias/soroban-abacus-flashcards/commit/0eefc332ac2724c54b477301a269915e895db94f))
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](https://github.com/antialias/soroban-abacus-flashcards/commit/b0cd194838705bb7bbf21ac9e318eaba491097b2))
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](https://github.com/antialias/soroban-abacus-flashcards/commit/cee399ed1513d32d0fff51a6f63898aa861605e1))
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](https://github.com/antialias/soroban-abacus-flashcards/commit/f389afa831935e896a626f526cfee378e340a64b))
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](https://github.com/antialias/soroban-abacus-flashcards/commit/6972fdf1105b6e854494efe1c4c587e6b6ff32a9))
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](https://github.com/antialias/soroban-abacus-flashcards/commit/e1258ee0416010909774694c0b25306b6f30329c))
* configure favicon metadata and improve bead visibility ([e1369fa](https://github.com/antialias/soroban-abacus-flashcards/commit/e1369fa2754cd61745a2950e6cb767d6b08db38f))
* copy entire packages/core and packages/templates ([0ccada0](https://github.com/antialias/soroban-abacus-flashcards/commit/0ccada0ca783e635f9ae08f33a69c392018ee342))
* correct hero abacus scroll direction to flow with page content ([4232746](https://github.com/antialias/soroban-abacus-flashcards/commit/423274657c9698bba28f7246fbf48d8508d97ef9))
* correct Typst template path in Dockerfile ([4c518de](https://github.com/antialias/soroban-abacus-flashcards/commit/4c518decb7fcc0b519d07680cbfd01c94c23dd41))
* delete existing user sessions before creating new ones ([0cced47](https://github.com/antialias/soroban-abacus-flashcards/commit/0cced47a0f414a04371bdb253fc5a43e4d9557be))
* extract pure SVG content from AbacusReact renders ([b07f1c4](https://github.com/antialias/soroban-abacus-flashcards/commit/b07f1c421616bcfd1f949f9a42ce1b03df418945))
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](https://github.com/antialias/soroban-abacus-flashcards/commit/5a8c98fc10704e459690308a84dc7ee2bfa0ef6c))
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](https://github.com/antialias/soroban-abacus-flashcards/commit/f80a73b35c324959bfd7141ebf086cb47d3c0ebc))
* **games:** use specific transition properties for smooth carousel loop ([187271e](https://github.com/antialias/soroban-abacus-flashcards/commit/187271e51527ee0129f71d77be1bd24072b963c4))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](https://github.com/antialias/soroban-abacus-flashcards/commit/4d4d930bd307ce5a405fc5751af6682a9f221f1f))
* **i18n:** use useMessages() for tutorial translations ([95b0105](https://github.com/antialias/soroban-abacus-flashcards/commit/95b0105ca3b28c5adfa843e8d77a8b27d9e7ade4))
* include column posts in favicon bounding box ([0b2f481](https://github.com/antialias/soroban-abacus-flashcards/commit/0b2f48106a939307b728c86fe2ea1be1e0247ea8))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](https://github.com/antialias/soroban-abacus-flashcards/commit/633ff127500c893a215491afa0e6ff814ad553bf))
* Integrate threshold input into Point Victory card ([b29bbee](https://github.com/antialias/soroban-abacus-flashcards/commit/b29bbeefcad92be42f7a3ca27ac126db4232ab26))
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](https://github.com/antialias/soroban-abacus-flashcards/commit/d7b35d954421fd7577cd2c26247666e5953b647d))
* **nav:** show full navigation on /games page ([d3fe6ac](https://github.com/antialias/soroban-abacus-flashcards/commit/d3fe6acbb0390e1df71869a4095e5ee6021e06b1))
* **qr-button:** improve layout and z-index ([646a422](https://github.com/antialias/soroban-abacus-flashcards/commit/646a4228d0573796b1a429e31bc037411024c0ff))
* **qr-button:** increase mini QR code size to 80px ([61ac737](https://github.com/antialias/soroban-abacus-flashcards/commit/61ac7378bdb01132b26bfc265a057c095ea41606))
* **qr-button:** increase mini QR code to 84px ([3fae5ea](https://github.com/antialias/soroban-abacus-flashcards/commit/3fae5ea6fa9ebd0f8fe8c9140a027be7f6a041aa))
* **qr-button:** make button square and increase QR size ([dc2d466](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d46663b8e0ec94a1508a57c4f8c2d8ba03506))
* **qr-button:** match height of stacked buttons ([81f202d](https://github.com/antialias/soroban-abacus-flashcards/commit/81f202d21556aa430402fda814519adbc8883831))
* reduce padding to minimize gap below last bead ([0e529be](https://github.com/antialias/soroban-abacus-flashcards/commit/0e529be789caf16e73f3e2ee77f52e243841aef4))
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](https://github.com/antialias/soroban-abacus-flashcards/commit/ed9a050d64db905e1328008f25dc0014e9a81999))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
* **rithmomachia:** add missing i18next dependencies ([91154d9](https://github.com/antialias/soroban-abacus-flashcards/commit/91154d93647e59f7e5f96d1db5624a7ec9b1b9ff))
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](https://github.com/antialias/soroban-abacus-flashcards/commit/dae615ee72a7ec7d0b235a22c61ebc4af0d8eadb))
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](https://github.com/antialias/soroban-abacus-flashcards/commit/cda1126cb0eab6840df89f3a8778d72410298093))
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](https://github.com/antialias/soroban-abacus-flashcards/commit/709322373a91c8174d21052d184fa84dd8bda326))
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](https://github.com/antialias/soroban-abacus-flashcards/commit/2a917484938bc269cf16acb501d4d26584405e0f))
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](https://github.com/antialias/soroban-abacus-flashcards/commit/cfac27750526fb1f6a7e4314a96aab3b92e08e44))
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](https://github.com/antialias/soroban-abacus-flashcards/commit/618e56358deb66cba968472f39b8d4e28b4dd211))
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](https://github.com/antialias/soroban-abacus-flashcards/commit/aafb64f3e337c6cf925766fe179b91f66c4a040b))
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](https://github.com/antialias/soroban-abacus-flashcards/commit/1bcd99c949e4d2b4fb1c0813debd50176fa58cb9))
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](https://github.com/antialias/soroban-abacus-flashcards/commit/14259a19a9817d0947467faa004d5f43118f8d8d))
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](https://github.com/antialias/soroban-abacus-flashcards/commit/4fa20f44cb9758f29d1f1512232be0fdc0b53b3d))
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](https://github.com/antialias/soroban-abacus-flashcards/commit/4834ece98e86f2fb00511bb876a5c32c289df0e0))
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](https://github.com/antialias/soroban-abacus-flashcards/commit/56f3164155beb94ceec2838bed9fc74fd75524db))
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](https://github.com/antialias/soroban-abacus-flashcards/commit/d0a8fcdea6aa4fdacfee33e183c92923634ee2b7))
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](https://github.com/antialias/soroban-abacus-flashcards/commit/a673177bec1c709463ce0f266848f473a79f4ef0))
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a0374fac5dce676df5890663b75531589ed93a))
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](https://github.com/antialias/soroban-abacus-flashcards/commit/190f8cf302aa966f029d05931811e217c67bfe39))
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](https://github.com/antialias/soroban-abacus-flashcards/commit/774c6b0ce712b1a77bb684457da9831e6ec91138))
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](https://github.com/antialias/soroban-abacus-flashcards/commit/54bfd2fac86be3597d40c67a1235e4c4ed8e2709))
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](https://github.com/antialias/soroban-abacus-flashcards/commit/8f4a79c9b0cad55336584fdc8e67409015d3a8ae))
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](https://github.com/antialias/soroban-abacus-flashcards/commit/bd49964186a0daa1639ae849b128a76081643daf))
* **room-info:** hide Leave Room button when user is alone ([5927f61](https://github.com/antialias/soroban-abacus-flashcards/commit/5927f61c3c34ba583ee45c8cee48a116c1c03071))
* separate horizontal and vertical bounding box logic ([83090df](https://github.com/antialias/soroban-abacus-flashcards/commit/83090df4dfad1d1d5cfa6c278c241526cacc7972))
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](https://github.com/antialias/soroban-abacus-flashcards/commit/88993f36629206a7bdcf9aa9d5641f1580b64de5))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](https://github.com/antialias/soroban-abacus-flashcards/commit/f8fe6e4a415f8655626af567129d0cda61b82e15))
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](https://github.com/antialias/soroban-abacus-flashcards/commit/f4ffc5b0277535358bea7588309a1a4afd1983a1))
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](https://github.com/antialias/soroban-abacus-flashcards/commit/440b492e85beff1612697346b6c5cfc8461e83da))
* various game improvements and UI enhancements ([b67cf61](https://github.com/antialias/soroban-abacus-flashcards/commit/b67cf610c570d54744553cd8f6694243fa50bee1))
### Performance Improvements
* optimize Docker image size to reduce build failures ([9ca3106](https://github.com/antialias/soroban-abacus-flashcards/commit/9ca310636183f4970db925ce8fa368e23645eb02))
### Code Refactoring
* **card-sorting:** remove reveal numbers feature ([ea5e3e8](https://github.com/antialias/soroban-abacus-flashcards/commit/ea5e3e838bd6a5b8b38469a70aa92a0e9baba769))
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](https://github.com/antialias/soroban-abacus-flashcards/commit/e4df8432b9c4a2055d47833d56b6e9fcf325ca94))
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](https://github.com/antialias/soroban-abacus-flashcards/commit/82c133f742f3f5c40b723c18d1997b518f25b320))
* **games:** move page title to nav bar ([712ee58](https://github.com/antialias/soroban-abacus-flashcards/commit/712ee58e5956e5bbdb13d5a5fb367020c87c8c9a))
* **games:** remove redundant subtitle below nav ([ad5bb87](https://github.com/antialias/soroban-abacus-flashcards/commit/ad5bb87325a44825f0cd85b38eb0e5f0eea7a695))
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](https://github.com/antialias/soroban-abacus-flashcards/commit/876513c9cc6323c20845ae8f1a3a5478d449f9e4))
* reorganize Harmony and Victory guide sections ([fb629c4](https://github.com/antialias/soroban-abacus-flashcards/commit/fb629c44ea37a7b296561919a4980c10d14efed8))
* restructure /create page into hub with sub-pages ([b91b23d](https://github.com/antialias/soroban-abacus-flashcards/commit/b91b23d95ffaeeaa30dbc8579f4c30bae8829ee7))
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](https://github.com/antialias/soroban-abacus-flashcards/commit/a0a867b27166a838ca7e0dcbd8f89fe1be812a80))
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](https://github.com/antialias/soroban-abacus-flashcards/commit/f0a066d8f0a51d35e18f87a8436c0d05153c03b5))
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](https://github.com/antialias/soroban-abacus-flashcards/commit/eace0ed52979b71870f77ee68f8568558f2aaecb))
* **rithmomachia:** extract guide sections into separate files ([765525d](https://github.com/antialias/soroban-abacus-flashcards/commit/765525dc451897f561f017e444aae892dc27177f))
* **rithmomachia:** extract hooks (phase 5) ([324a659](https://github.com/antialias/soroban-abacus-flashcards/commit/324a65992f97c295ea3968aaf54d266373f4c035))
* **rithmomachia:** extract phase components (phase 4) ([11364f6](https://github.com/antialias/soroban-abacus-flashcards/commit/11364f6394c15e49850e5cad2cbd32e1ea08a178))
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](https://github.com/antialias/soroban-abacus-flashcards/commit/3abc325ea27feee5c4cc59f02296ff218f342a81))
* **rithmomachia:** make setup phase UI more compact ([e55f848](https://github.com/antialias/soroban-abacus-flashcards/commit/e55f848a26092a2b4a5b09c3c255544ea9666f1b))
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](https://github.com/antialias/soroban-abacus-flashcards/commit/dfeeb0e0db8b2c4a38198cf71cd918439d6c211b)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](https://github.com/antialias/soroban-abacus-flashcards/commit/82a5eb2e4bf74f42a183a15f1129e5ec84cc5231))
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](https://github.com/antialias/soroban-abacus-flashcards/commit/0471da598d8d591b3f9d63f467cb35f999924c13))
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/2ab6ab57995a6d7d9c66b9fba8de945507209661))
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab7a1df327d7258228af9851762555583a20d61))
* use AbacusReact for dynamic Open Graph image ([9c20f12](https://github.com/antialias/soroban-abacus-flashcards/commit/9c20f12bacff4fe7f8bd7a87032afbed9711e94b))
### Documentation
* add 3D enhancement documentation to README ([cc96802](https://github.com/antialias/soroban-abacus-flashcards/commit/cc96802df87c805c946ee59af509663ba570e75b))
* add database migration guide and playing guide modal spec ([5a29af7](https://github.com/antialias/soroban-abacus-flashcards/commit/5a29af78e27e897ab35273611b79c4b669304f71))
* add deployment verification guidelines to prevent false positives ([3d8da23](https://github.com/antialias/soroban-abacus-flashcards/commit/3d8da2348b4e8a227e963791d15dc6718eac5af1))
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](https://github.com/antialias/soroban-abacus-flashcards/commit/008ccead0f9c634fe52fd156e6f9a04d6cdd7744))
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](https://github.com/antialias/soroban-abacus-flashcards/commit/e3c1f10233cc0924ff96a643c7c4c1f1278de3e3))
* update workflow to require manual testing before commits ([0991796](https://github.com/antialias/soroban-abacus-flashcards/commit/0991796f1eccef345f10205e675e4c33d1a62b17))
### Styles
* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](https://github.com/antialias/soroban-abacus-flashcards/commit/88ca35e0440157ff9349e8d3d2d3cc844f18ffea)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](https://github.com/antialias/soroban-abacus-flashcards/commit/94e5e6a268b387380b88b192737bd55578b98bc7)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](https://github.com/antialias/soroban-abacus-flashcards/commit/7bf2d730d370e562486b229f4d209099ff8c4463))
### Tests
* trigger compose-updater deployment test ([2b06aae](https://github.com/antialias/soroban-abacus-flashcards/commit/2b06aae39474cc80d501c47c9685fa99e7120c48))
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
### Features
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))

View File

@@ -161,11 +161,14 @@
"Bash(printenv:*)",
"Bash(typst:*)",
"Bash(npx tsx:*)",
"Bash(sort:*)"
"Bash(sort:*)",
"Bash(scp:*)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
"enabledMcpjsonServers": [
"sqlite"
]
}

1
apps/web/README.md Normal file
View File

@@ -0,0 +1 @@
# Test deployment - Mon Nov 3 16:31:57 CST 2025

View File

@@ -1,4 +1,4 @@
import { NextRequest, NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, readFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'

View File

@@ -23,13 +23,11 @@ export function AbacusTarget({ number }: AbacusTargetProps) {
<AbacusReact
value={number}
columns={1}
compact={true}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.72}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
</div>
)

View File

@@ -22,6 +22,8 @@ import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*
* Test: Verifying compose-updater automatic deployment cycle
*/
export default function RoomPage() {
const router = useRouter()

View File

@@ -0,0 +1,64 @@
/**
* Test page for AbacusStatic - Server Component
* This demonstrates that AbacusStatic works without "use client"
*/
import { AbacusStatic } from '@soroban/abacus-react'
export default function TestStaticAbacusPage() {
const numbers = [1, 2, 3, 4, 5, 10, 25, 50, 100, 123, 456, 789]
return (
<div style={{ padding: '40px', maxWidth: '1200px', margin: '0 auto' }}>
<h1 style={{ marginBottom: '10px' }}>AbacusStatic Test (Server Component)</h1>
<p style={{ color: '#64748b', marginBottom: '30px' }}>
This page is a React Server Component - no "use client" directive!
All abacus displays below are rendered on the server with zero client-side JavaScript.
</p>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
gap: '20px',
}}
>
{numbers.map((num) => (
<div
key={num}
style={{
padding: '20px',
background: 'white',
border: '2px solid #e2e8f0',
borderRadius: '12px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
}}
>
<AbacusStatic
value={num}
columns="auto"
hideInactiveBeads
compact
scaleFactor={0.9}
/>
<span style={{ fontSize: '20px', fontWeight: 'bold', color: '#475569' }}>
{num}
</span>
</div>
))}
</div>
<div style={{ marginTop: '40px', padding: '20px', background: '#f0fdf4', borderRadius: '8px' }}>
<h2 style={{ marginTop: 0, color: '#166534' }}> Success!</h2>
<p style={{ color: '#15803d' }}>
If you can see the abacus displays above, then AbacusStatic is working correctly
in React Server Components. Check the page source - you'll see pure HTML/SVG with
no client-side hydration markers!
</p>
</div>
</div>
)
}

View File

@@ -1,7 +1,7 @@
'use client'
import { useEffect, useRef } from 'react'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { AbacusReact, useAbacusConfig, ABACUS_THEMES } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { useHomeHero } from '../contexts/HomeHeroContext'
@@ -17,19 +17,8 @@ export function HeroAbacus() {
const appConfig = useAbacusConfig()
const heroRef = useRef<HTMLDivElement>(null)
// Styling for structural elements (solid, no translucency)
const structuralStyles = {
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
}
// Use theme preset from abacus-react instead of manual definition
const structuralStyles = ABACUS_THEMES.light
// Detect when hero scrolls out of view
useEffect(() => {

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import { useSpring, useTransition, animated } from '@react-spring/web'
import * as Slider from '@radix-ui/react-slider'
import { AbacusReact, StandaloneBead } from '@soroban/abacus-react'
import { AbacusReact, StandaloneBead, ABACUS_THEMES } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { stack } from '../../styled-system/patterns'
import { kyuLevelDetails } from '@/data/kyuLevelDetails'
@@ -260,19 +260,8 @@ function parseKyuDetails(rawText: string) {
return sections
}
// Dark theme styles matching the homepage
const darkStyles = {
columnPosts: {
fill: 'rgba(255, 255, 255, 0.3)',
stroke: 'rgba(255, 255, 255, 0.2)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgba(255, 255, 255, 0.4)',
stroke: 'rgba(255, 255, 255, 0.25)',
strokeWidth: 3,
},
}
// Use dark theme preset from abacus-react instead of manual definition
const darkStyles = ABACUS_THEMES.dark
interface LevelSliderDisplayProps {
initialIndex?: number

View File

@@ -2,11 +2,10 @@
import { useContext, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { AbacusReact, useAbacusConfig, ABACUS_THEMES } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { useMyAbacus } from '@/contexts/MyAbacusContext'
import { HomeHeroContext } from '@/contexts/HomeHeroContext'
import { Z_INDEX } from '@/constants/zIndex'
export function MyAbacus() {
const { isOpen, close, toggle } = useMyAbacus()
@@ -31,21 +30,6 @@ export function MyAbacus() {
const isHeroVisible = homeHeroContext?.isHeroVisible ?? false
const isHeroMode = isOnHomePage && isHeroVisible && !isOpen
// Track scroll position for hero mode
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
if (!isHeroMode) return
const handleScroll = () => {
setScrollY(window.scrollY)
}
handleScroll() // Initial position
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [isHeroMode])
// Close on Escape key
useEffect(() => {
if (!isOpen) return
@@ -72,33 +56,9 @@ export function MyAbacus() {
}
}, [isOpen])
// Hero mode styles - white structural (from original HeroAbacus)
const structuralStyles = {
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
}
// Trophy abacus styles - golden/premium look
const trophyStyles = {
columnPosts: {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3,
},
reckoningBar: {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 4,
},
}
// Use theme presets from abacus-react instead of manual definitions
const structuralStyles = ABACUS_THEMES.light
const trophyStyles = ABACUS_THEMES.trophy
return (
<>
@@ -114,7 +74,7 @@ export function MyAbacus() {
inset: 0,
bg: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(12px)',
zIndex: Z_INDEX.MY_ABACUS_BACKDROP,
zIndex: 101,
animation: 'backdropFadeIn 0.4s ease-out',
})}
onClick={close}
@@ -144,7 +104,7 @@ export function MyAbacus() {
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.2s',
zIndex: Z_INDEX.MY_ABACUS + 1,
zIndex: 103,
animation: 'fadeIn 0.3s ease-out 0.2s both',
_hover: {
bg: 'rgba(255, 255, 255, 0.2)',
@@ -162,36 +122,28 @@ export function MyAbacus() {
data-component="my-abacus"
data-mode={isOpen ? 'open' : isHeroMode ? 'hero' : 'button'}
onClick={isOpen || isHeroMode ? undefined : toggle}
style={
isHeroMode
? {
// Hero mode: position accounts for scroll to flow with page (subtract scroll to move up with content)
// Positioned lower (60vh instead of 50vh) to avoid covering subtitle
top: `calc(60vh - ${scrollY}px)`,
}
: undefined
}
className={css({
position: 'fixed',
zIndex: Z_INDEX.MY_ABACUS,
position: isHeroMode ? 'absolute' : 'fixed',
zIndex: 102,
cursor: isOpen || isHeroMode ? 'default' : 'pointer',
transition: isHeroMode ? 'none' : 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
// Three modes: hero (inline with content), button (bottom-right), open (center)
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
// Three modes: hero (absolute - scrolls with document), button (fixed), open (fixed)
...(isOpen
? {
// Open mode: center of screen
// Open mode: fixed to center of viewport
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: isHeroMode
? {
// Hero mode: centered horizontally, top handled by inline style
// Hero mode: absolute positioning - scrolls naturally with document
top: '60vh',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: {
// Button mode: bottom-right corner
// Button mode: fixed to bottom-right corner
bottom: { base: '4', md: '6' },
right: { base: '4', md: '6' },
transform: 'translate(0, 0)',
@@ -259,58 +211,22 @@ export function MyAbacus() {
animated={isOpen || isHeroMode}
customStyles={isHeroMode ? structuralStyles : trophyStyles}
onValueChange={setAbacusValue}
// 3D Enhancement - realistic mode for hero and open states
enhanced3d={isOpen || isHeroMode ? 'realistic' : undefined}
material3d={
isOpen || isHeroMode
? {
heavenBeads: 'glossy',
earthBeads: 'satin',
lighting: 'dramatic',
woodGrain: true,
}
: undefined
}
/>
</div>
</div>
{/* Title and achievement info - only visible when open */}
{isOpen && (
<div
className={css({
position: 'absolute',
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
mt: { base: '16', md: '20', lg: '24' },
textAlign: 'center',
animation: 'fadeIn 0.5s ease-out 0.3s both',
maxW: '600px',
px: '8',
})}
>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl', lg: '4xl' },
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
mb: '3',
})}
>
My Abacus
</h2>
<p
className={css({
fontSize: { base: 'md', md: 'lg' },
color: 'gray.300',
mb: '4',
fontWeight: 'medium',
})}
>
Your personal abacus grows with you
</p>
<p
className={css({
fontSize: { base: 'sm', md: 'md' },
color: 'gray.400',
lineHeight: '1.6',
})}
>
Complete tutorials, play games, and earn achievements to unlock higher place values
</p>
</div>
)}
</div>
{/* Keyframes for animations */}

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import Resizable from 'react-resizable-layout'
import { calculateBeadDiffFromValues } from '@soroban/abacus-react'
import { css } from '../../../styled-system/css'
import { hstack, stack, vstack } from '../../../styled-system/patterns'
import {
@@ -13,7 +14,6 @@ import {
type TutorialValidation,
} from '../../types/tutorial'
import { generateAbacusInstructions } from '../../utils/abacusInstructionGenerator'
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
import { generateSingleProblem } from '../../utils/problemGenerator'
import {
createBasicAllowedConfiguration,

View File

@@ -6,6 +6,7 @@ import {
AbacusReact,
type StepBeadHighlight,
useAbacusDisplay,
calculateBeadDiffFromValues,
} from '@soroban/abacus-react'
import { useTranslations } from 'next-intl'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -18,7 +19,6 @@ import type {
TutorialStep,
UIState,
} from '../../types/tutorial'
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
import { generateUnifiedInstructionSequence } from '../../utils/unifiedStepGenerator'
import { CoachBar } from './CoachBar/CoachBar'
import { DecompositionWithReasons } from './DecompositionWithReasons'

View File

@@ -1,29 +1,30 @@
// Automatic instruction generator for abacus tutorial steps
import type { ValidPlaceValues } from '@soroban/abacus-react'
// Re-exports core types and functions from abacus-react
export interface BeadState {
heavenActive: boolean
earthActive: number // 0-4
}
export type { ValidPlaceValues } from '@soroban/abacus-react'
export {
type BeadState,
type AbacusState,
type PlaceValueBasedBead as BeadHighlight,
numberToAbacusState,
calculateBeadChanges,
} from '@soroban/abacus-react'
export interface AbacusState {
[placeValue: number]: BeadState
}
import type { ValidPlaceValues, PlaceValueBasedBead } from '@soroban/abacus-react'
import { numberToAbacusState, calculateBeadChanges } from '@soroban/abacus-react'
export interface BeadHighlight {
placeValue: ValidPlaceValues
beadType: 'heaven' | 'earth'
position?: number
}
// Type alias for internal use
type BeadHighlight = PlaceValueBasedBead
export interface StepBeadHighlight extends BeadHighlight {
// App-specific extension for step-based tutorial highlighting
export interface StepBeadHighlight extends PlaceValueBasedBead {
stepIndex: number // Which instruction step this bead belongs to
direction: 'up' | 'down' | 'activate' | 'deactivate' // Movement direction
order?: number // Order within the step (for multiple beads per step)
}
export interface GeneratedInstruction {
highlightBeads: BeadHighlight[]
highlightBeads: PlaceValueBasedBead[]
expectedAction: 'add' | 'remove' | 'multi-step'
actionDescription: string
multiStepInstructions?: string[]
@@ -40,68 +41,7 @@ export interface GeneratedInstruction {
}
}
// Convert a number to abacus state representation
export function numberToAbacusState(value: number, maxPlaces: number = 5): AbacusState {
const state: AbacusState = {}
for (let place = 0; place < maxPlaces; place++) {
const placeValueNum = 10 ** place
const digit = Math.floor(value / placeValueNum) % 10
state[place] = {
heavenActive: digit >= 5,
earthActive: digit >= 5 ? digit - 5 : digit,
}
}
return state
}
// Calculate the difference between two abacus states
export function calculateBeadChanges(
startState: AbacusState,
targetState: AbacusState
): {
additions: BeadHighlight[]
removals: BeadHighlight[]
placeValue: number
} {
const additions: BeadHighlight[] = []
const removals: BeadHighlight[] = []
let mainPlaceValue = 0
for (const placeStr in targetState) {
const place = parseInt(placeStr, 10) as ValidPlaceValues
const start = startState[place] || { heavenActive: false, earthActive: 0 }
const target = targetState[place]
// Check heaven bead changes
if (!start.heavenActive && target.heavenActive) {
additions.push({ placeValue: place, beadType: 'heaven' })
mainPlaceValue = place
} else if (start.heavenActive && !target.heavenActive) {
removals.push({ placeValue: place, beadType: 'heaven' })
mainPlaceValue = place
}
// Check earth bead changes
if (target.earthActive > start.earthActive) {
// Adding earth beads
for (let pos = start.earthActive; pos < target.earthActive; pos++) {
additions.push({ placeValue: place, beadType: 'earth', position: pos })
mainPlaceValue = place
}
} else if (target.earthActive < start.earthActive) {
// Removing earth beads
for (let pos = start.earthActive - 1; pos >= target.earthActive; pos--) {
removals.push({ placeValue: place, beadType: 'earth', position: pos })
mainPlaceValue = place
}
}
}
return { additions, removals, placeValue: mainPlaceValue }
}
// Note: numberToAbacusState and calculateBeadChanges are now re-exported from @soroban/abacus-react above
// Generate proper complement breakdown using simple bead movements
function generateProperComplementDescription(

View File

@@ -1,107 +1,25 @@
// Dynamic bead diff algorithm for calculating transitions between abacus states
// Provides arrows, highlights, and movement directions for tutorial UI
// Re-export core bead diff functionality from abacus-react
// App-specific extensions for multi-step tutorials and validation
import type { ValidPlaceValues } from '@soroban/abacus-react'
import {
export {
type BeadDiffResult,
type BeadDiffOutput,
calculateBeadDiff,
calculateBeadDiffFromValues,
areStatesEqual,
type AbacusState,
type BeadHighlight,
calculateBeadChanges,
numberToAbacusState,
} from './abacusInstructionGenerator'
type BeadState,
} from '@soroban/abacus-react'
export interface BeadDiffResult {
placeValue: ValidPlaceValues
beadType: 'heaven' | 'earth'
position?: number
direction: 'activate' | 'deactivate'
order: number // Order of operations for animations
}
export interface BeadDiffOutput {
changes: BeadDiffResult[]
highlights: BeadHighlight[]
hasChanges: boolean
summary: string
}
/**
* THE BEAD DIFF ALGORITHM
*
* Takes current and desired abacus states and returns exactly which beads
* need to move with arrows and highlights for the tutorial UI.
*
* This is the core "diff" function that keeps tutorial highlights in sync.
*/
export function calculateBeadDiff(fromState: AbacusState, toState: AbacusState): BeadDiffOutput {
const { additions, removals } = calculateBeadChanges(fromState, toState)
const changes: BeadDiffResult[] = []
const highlights: BeadHighlight[] = []
let order = 0
// Process removals first (pedagogical order: clear before adding)
removals.forEach((removal) => {
changes.push({
placeValue: removal.placeValue,
beadType: removal.beadType,
position: removal.position,
direction: 'deactivate',
order: order++,
})
highlights.push({
placeValue: removal.placeValue,
beadType: removal.beadType,
position: removal.position,
})
})
// Process additions second (pedagogical order: add after clearing)
additions.forEach((addition) => {
changes.push({
placeValue: addition.placeValue,
beadType: addition.beadType,
position: addition.position,
direction: 'activate',
order: order++,
})
highlights.push({
placeValue: addition.placeValue,
beadType: addition.beadType,
position: addition.position,
})
})
// Generate summary
const summary = generateDiffSummary(changes)
return {
changes,
highlights,
hasChanges: changes.length > 0,
summary,
}
}
/**
* Calculate bead diff from numeric values
* Convenience function for when you have numbers instead of states
*/
export function calculateBeadDiffFromValues(
fromValue: number,
toValue: number,
maxPlaces: number = 5
): BeadDiffOutput {
const fromState = numberToAbacusState(fromValue, maxPlaces)
const toState = numberToAbacusState(toValue, maxPlaces)
return calculateBeadDiff(fromState, toState)
}
import type { BeadDiffOutput, BeadDiffResult, AbacusState } from '@soroban/abacus-react'
import { calculateBeadDiffFromValues } from '@soroban/abacus-react'
/**
* Calculate step-by-step bead diffs for multi-step operations
* This is used for tutorial multi-step instructions where we want to show
* the progression through intermediate states
*
* APP-SPECIFIC FUNCTION - not in core abacus-react
*/
export function calculateMultiStepBeadDiffs(
startValue: number,
@@ -133,126 +51,10 @@ export function calculateMultiStepBeadDiffs(
return stepDiffs
}
/**
* Generate a human-readable summary of what the diff does
* Respects pedagogical order: removals first, then additions
*/
function generateDiffSummary(changes: BeadDiffResult[]): string {
if (changes.length === 0) {
return 'No changes needed'
}
// Sort by order to respect pedagogical sequence
const sortedChanges = [...changes].sort((a, b) => a.order - b.order)
const deactivations = sortedChanges.filter((c) => c.direction === 'deactivate')
const activations = sortedChanges.filter((c) => c.direction === 'activate')
const parts: string[] = []
// Process deactivations first (pedagogical order)
if (deactivations.length > 0) {
const deactivationsByPlace = groupByPlace(deactivations)
Object.entries(deactivationsByPlace).forEach(([place, beads]) => {
const placeName = getPlaceName(parseInt(place, 10))
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
const earthBeads = beads.filter((b) => b.beadType === 'earth')
if (heavenBeads.length > 0) {
parts.push(`remove heaven bead in ${placeName}`)
}
if (earthBeads.length > 0) {
const count = earthBeads.length
parts.push(`remove ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
}
})
}
// Process activations second (pedagogical order)
if (activations.length > 0) {
const activationsByPlace = groupByPlace(activations)
Object.entries(activationsByPlace).forEach(([place, beads]) => {
const placeName = getPlaceName(parseInt(place, 10))
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
const earthBeads = beads.filter((b) => b.beadType === 'earth')
if (heavenBeads.length > 0) {
parts.push(`add heaven bead in ${placeName}`)
}
if (earthBeads.length > 0) {
const count = earthBeads.length
parts.push(`add ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
}
})
}
return parts.join(', then ')
}
/**
* Group bead changes by place value
*/
function groupByPlace(changes: BeadDiffResult[]): {
[place: string]: BeadDiffResult[]
} {
return changes.reduce(
(groups, change) => {
const place = change.placeValue.toString()
if (!groups[place]) {
groups[place] = []
}
groups[place].push(change)
return groups
},
{} as { [place: string]: BeadDiffResult[] }
)
}
/**
* Get human-readable place name
*/
function getPlaceName(place: number): string {
switch (place) {
case 0:
return 'ones column'
case 1:
return 'tens column'
case 2:
return 'hundreds column'
case 3:
return 'thousands column'
default:
return `place ${place} column`
}
}
/**
* Check if two abacus states are equal
*/
export function areStatesEqual(state1: AbacusState, state2: AbacusState): boolean {
const places1 = Object.keys(state1)
.map((k) => parseInt(k, 10))
.sort()
const places2 = Object.keys(state2)
.map((k) => parseInt(k, 10))
.sort()
if (places1.length !== places2.length) return false
for (const place of places1) {
const bead1 = state1[place]
const bead2 = state2[place]
if (!bead2) return false
if (bead1.heavenActive !== bead2.heavenActive) return false
if (bead1.earthActive !== bead2.earthActive) return false
}
return true
}
/**
* Validate that a bead diff is feasible (no impossible bead states)
*
* APP-SPECIFIC FUNCTION - not in core abacus-react
*/
export function validateBeadDiff(diff: BeadDiffOutput): {
isValid: boolean
@@ -282,3 +84,20 @@ export function validateBeadDiff(diff: BeadDiffOutput): {
errors,
}
}
// Helper function for validation
function groupByPlace(changes: BeadDiffResult[]): {
[place: string]: BeadDiffResult[]
} {
return changes.reduce(
(groups, change) => {
const place = change.placeValue.toString()
if (!groups[place]) {
groups[place] = []
}
groups[place].push(change)
return groups
},
{} as { [place: string]: BeadDiffResult[] }
)
}

View File

@@ -9,7 +9,9 @@
"Bash(git add:*)",
"Bash(git rm:*)",
"Bash(git commit:*)",
"Bash(npm run build:*)"
"Bash(npm run build:*)",
"Bash(git reset:*)",
"Bash(cat:*)"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -1,3 +1,43 @@
# [2.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.5.0...abacus-react-v2.6.0) (2025-11-04)
### Bug Fixes
* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](https://github.com/antialias/soroban-abacus-flashcards/commit/0641eb719ef56c67de965296006df666f83e5b08))
### Features
* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](https://github.com/antialias/soroban-abacus-flashcards/commit/3b8e864cfa3af50b1912ce7ff55003d7f6b9c229))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](https://github.com/antialias/soroban-abacus-flashcards/commit/3588d5acde25588ce4db3ee32adb04ace0e394d4))
# [2.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.4.0...abacus-react-v2.5.0) (2025-11-03)
### Features
* **abacus-react:** add core utility functions for state management ([e65541c](https://github.com/antialias/soroban-abacus-flashcards/commit/e65541c100e590a51448750c6d5178ed4f3e8eeb))
* **abacus-react:** add layout and educational props ([35bbcec](https://github.com/antialias/soroban-abacus-flashcards/commit/35bbcecb9e36f1ef5917a5a629f5e78f1f490e9c))
* **abacus-react:** add pre-defined theme presets ([cf1f950](https://github.com/antialias/soroban-abacus-flashcards/commit/cf1f950c7c5fb9ee1f0de673235d6f037be3b9d6))
* **abacus-react:** add React hooks for abacus calculations ([de038d2](https://github.com/antialias/soroban-abacus-flashcards/commit/de038d2afc26c36c1490d5ea45dace0ab812c5cc))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](https://github.com/antialias/soroban-abacus-flashcards/commit/ce4e44d6302746053ad40dc61bab57ef3a0a9f31))
# [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.3.0...abacus-react-v2.4.0) (2025-11-03)
### Bug Fixes
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
### Features
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
# [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.2.0...abacus-react-v2.3.0) (2025-11-03)

View File

@@ -0,0 +1,402 @@
# Abacus-React Feature Enhancement Plan
## Executive Summary
The web application has developed numerous custom patterns and workarounds for styling, layout, and interactions with the abacus component. These patterns reveal gaps in the abacus-react API that, if addressed, would significantly improve developer experience and reduce code duplication across the application.
## Priority 1: Critical Features (High Impact, High Frequency)
### 1. **Inline "Mini Abacus" Component**
**Location**: `apps/web/src/app/arcade/complement-race/components/AbacusTarget.tsx`
**Current Implementation**:
```tsx
<AbacusReact
value={number}
columns={1}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.72}
customStyles={{
columnPosts: { opacity: 0 },
}}
/>
```
**Problem**: Creating an inline mini-abacus for displaying single digits requires multiple props and style overrides. This pattern appears throughout game UIs.
**Proposed Solution**: Add a `variant` prop with preset configurations:
```tsx
// Native API proposal
<AbacusReact
value={7}
variant="inline-digit"
// Automatically sets: columns=1, hideInactiveBeads, transparent frame, optimal scaleFactor
/>
// Or more granular:
<AbacusReact
value={7}
compact={true} // Removes frame, optimizes spacing
frameVisible={false} // Hide posts and bar
/>
```
**Benefits**:
- Single prop instead of 5+
- Consistent inline abacus appearance across the app
- Better semantic intent
---
### 2. **Theme-Aware Styling Presets**
**Locations**:
- `MyAbacus.tsx` (lines 60-85) - structural & trophy styles
- `HeroAbacus.tsx` (lines 20-32) - structural styles
- `LevelSliderDisplay.tsx` (lines 263-275) - dark theme styles
**Current Pattern**: Every component defines custom style objects for structural elements:
```tsx
const structuralStyles = {
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
}
```
**Problem**: Manual style object creation for common themes is repetitive and error-prone.
**Proposed Solution**: Add theme presets to abacus-react:
```tsx
// Native API proposal
<AbacusReact
value={123}
theme="dark" // or "light", "translucent", "solid", "trophy"
/>
// Or expose theme constants
import { ABACUS_THEMES } from '@soroban/abacus-react'
<AbacusReact customStyles={ABACUS_THEMES.dark} />
```
**Benefits**:
- Eliminates ~30 lines of style definitions per component
- Ensures visual consistency
- Makes theme switching trivial
---
### 3. **Scaling Containers & Responsive Layouts**
**Locations**:
- `HeroAbacus.tsx` (lines 133-138) - manual scale transforms
- `MyAbacus.tsx` (lines 214-218) - responsive scale values
- `LevelSliderDisplay.tsx` (lines 370-379) - dynamic scale calculation
**Current Pattern**: Components manually wrap abacus in transform containers:
```tsx
<div style={{
transform: 'scale(3.5)',
transformOrigin: 'center center'
}}>
<AbacusReact value={1234} columns={4} />
</div>
```
**Problem**: Manual transform handling requires extra DOM nesting, breaks click boundaries, and makes centering complex.
**Proposed Solution**: Enhanced `scaleFactor` with responsive breakpoints:
```tsx
// Native API proposal
<AbacusReact
value={1234}
scaleFactor={{ base: 2.5, md: 3.5, lg: 4.5 }} // Responsive
scaleOrigin="center" // Handle transform origin
scaleContainer={true} // Apply correct boundaries for interaction
/>
```
**Benefits**:
- Eliminates wrapper divs
- Proper click/hover boundaries
- Built-in responsive scaling
---
## Priority 2: Developer Experience Improvements
### 4. **Bead Diff Calculation System**
**Location**: `apps/web/src/utils/beadDiff.ts` (285 lines) + `abacusInstructionGenerator.ts` (400+ lines)
**Current Implementation**: Complex utilities to calculate which beads need to move between states:
```tsx
// Current external pattern
import { calculateBeadDiffFromValues } from '@/utils/beadDiff'
const diff = calculateBeadDiffFromValues(fromValue, toValue)
const stepBeadHighlights = diff.changes.map(change => ({
placeValue: change.placeValue,
beadType: change.beadType,
direction: change.direction,
// ...
}))
```
**Problem**: Tutorial/game developers need to calculate bead movements manually. This core logic belongs in abacus-react.
**Proposed Solution**: Add a diff calculation hook:
```tsx
// Native API proposal
import { useAbacusDiff } from '@soroban/abacus-react'
function Tutorial() {
const diff = useAbacusDiff(startValue, targetValue)
return (
<AbacusReact
value={currentValue}
stepBeadHighlights={diff.highlights} // Generated by hook
// diff also includes: instructions, order, validation
/>
)
}
```
**Benefits**:
- Centralizes "diff" algorithm
- Eliminates ~500 lines of application code
- Better tested and maintained
---
### 5. **Tutorial/Step Context Provider**
**Location**: `apps/web/src/components/tutorial/TutorialContext.tsx`
**Current Pattern**: Apps need to implement complex state management for multi-step tutorial flows with reducer patterns, event tracking, and error handling.
**Problem**: Tutorial infrastructure is duplicated across components. The logic for tracking progress through abacus instruction steps is tightly coupled to application code.
**Proposed Solution**: Add optional tutorial/stepper context to abacus-react:
```tsx
// Native API proposal
import { AbacusReact, AbacusTutorial } from '@soroban/abacus-react'
<AbacusTutorial
steps={[
{ from: 0, to: 5, instruction: "Add 5" },
{ from: 5, to: 15, instruction: "Add 10" },
]}
onStepComplete={(step) => { /* analytics */ }}
onComplete={() => { /* celebration */ }}
>
<AbacusReact />
</AbacusTutorial>
```
**Benefits**:
- Reusable tutorial infrastructure
- Built-in progress tracking and validation
- Could power educational features across projects
---
## Priority 3: Nice-to-Have Enhancements
### 6. **Animation Speed Configuration**
**Location**: `LevelSliderDisplay.tsx` (lines 306-345)
**Current Pattern**: Applications control animation speed by rapidly changing the value prop:
```tsx
const intervalMs = 500 - danProgress * 490 // 500ms down to 10ms
setInterval(() => {
setAnimatedDigits(prev => {
// Rapidly change digits to simulate calculation
})
}, intervalMs)
```
**Problem**: "Rapid calculation" animation requires external interval management.
**Proposed Solution**: Add animation speed prop:
```tsx
// Native API proposal
<AbacusReact
value={calculatingValue}
animationSpeed="fast" // or "normal", "slow", or ms number
autoAnimate={true} // Animate value prop changes automatically
/>
```
**Benefits**:
- Smoother animations with internal management
- Consistent timing across the app
---
### 7. **Draggable/Positionable Abacus Cards**
**Location**: `InteractiveFlashcards.tsx`
**Current Pattern**: Complex drag-and-drop implementation wrapped around each AbacusReact instance with pointer capture, offset tracking, and rotation.
**Problem**: Making abacus instances draggable requires significant boilerplate.
**Proposed Solution**: This is probably too specific to remain external. However, a ref-based API to get bounding boxes would help:
```tsx
// Possible improvement
const abacusRef = useAbacusRef()
<AbacusReact ref={abacusRef} />
// abacusRef.current.getBoundingBox() for drag calculations
```
---
### 8. **Column Highlighting for Multi-Step Problems**
**Location**: Tutorial system extensively
**Current Pattern**: Manual column highlighting based on place values with custom overlay positioning logic.
**Problem**: Highlighting specific columns (e.g., "the tens column") requires external overlay management.
**Proposed Solution**: Add native column highlighting:
```tsx
// Native API proposal
<AbacusReact
value={123}
highlightColumns={[1]} // Highlight tens column
columnLabels={["ones", "tens", "hundreds"]} // Optional labels
/>
```
---
## Priority 4: Documentation & Exports
### 9. **Utility Functions & Types**
**Current State**: Apps re-implement utilities for working with abacus states:
- `numberToAbacusState()` - convert numbers to bead states
- `calculateBeadChanges()` - diff algorithm
- `ValidPlaceValues` type - imported but limited
**Proposed Solution**: Export more utilities from abacus-react:
```tsx
// Expanded exports
export {
// Utilities
numberToAbacusState,
abacusStateToNumber,
calculateBeadDiff,
validateAbacusValue,
// Types
AbacusState,
BeadState,
PlaceValue,
// Hooks
useAbacusDiff,
useAbacusValidation,
useAbacusState,
}
```
---
## Implementation Roadmap
### Phase 1 (Immediate) ✅ COMPLETED
1. ✅ Add `frameVisible={false}` prop
2. ✅ Add `compact` prop/variant
3. ✅ Export theme presets (ABACUS_THEMES constant)
### Phase 2 (Short-term) ✅ COMPLETED
4. ⏸️ Enhanced `scaleFactor` with responsive object support (DEFERRED - too complex, low priority)
5. ✅ Export utility functions (numberToAbacusState, calculateBeadDiff, etc.)
### Phase 3 (Medium-term) ✅ COMPLETED
6. ✅ Add `useAbacusDiff` hook
7. ✅ Add native column highlighting with `highlightColumns` and `columnLabels` props
### Phase 4 (Long-term - Future)
8. 📋 Consider tutorial context provider (needs more research)
9. 📋 Animation speed controls
## Completed Features Summary
### New Props
- `frameVisible?: boolean` - Show/hide column posts and reckoning bar
- `compact?: boolean` - Compact layout for inline display (implies frameVisible=false)
- `highlightColumns?: number[]` - Highlight specific columns by index
- `columnLabels?: string[]` - Optional labels for columns
### New Exports
- `ABACUS_THEMES` - Pre-defined theme presets (light, dark, trophy, translucent, solid, traditional)
- `AbacusThemeName` type - TypeScript type for theme names
### New Utility Functions
- `numberToAbacusState(value, maxPlaces)` - Convert number to bead positions
- `abacusStateToNumber(state)` - Convert bead positions to number
- `calculateBeadChanges(startState, targetState)` - Calculate bead differences
- `calculateBeadDiff(fromState, toState)` - Full diff with order and directions
- `calculateBeadDiffFromValues(from, to, maxPlaces)` - Convenience wrapper
- `validateAbacusValue(value, maxPlaces)` - Validate number ranges
- `areStatesEqual(state1, state2)` - Compare states
### New Hooks
- `useAbacusDiff(fromValue, toValue, maxPlaces)` - Calculate bead differences for tutorials
- `useAbacusState(value, maxPlaces)` - Convert number to abacus state (memoized)
### New Types
- `BeadState` - Bead state in a single column
- `AbacusState` - Complete abacus state
- `BeadDiffResult` - Single bead movement result
- `BeadDiffOutput` - Complete diff output
- `PlaceValueBasedBead` - Internal place-value based bead type
---
## Metrics & Impact
**Code Reduction Estimate**:
- Eliminates ~800-1000 lines of repetitive application code
- Reduces component complexity by ~40% for tutorial/game components
**Developer Experience**:
- Faster onboarding for new features using abacus
- More consistent UX across application
- Better TypeScript support and autocomplete
**Maintenance**:
- Centralized logic easier to test and debug
- Single source of truth for abacus behavior
- Easier to add new features (e.g., sound effects for different themes)
---
## Questions for Discussion
1. Should we split these into separate packages (e.g., `@soroban/abacus-tutorial`)?
2. Which theme presets should be included by default?
3. Should responsive scaling use CSS media queries or JS breakpoints?
4. How much tutorial logic belongs in the core library vs. app code?

View File

@@ -0,0 +1,162 @@
# Integration Summary
## ✅ Completed: Apps/Web Integration with Abacus-React Enhancements
### Features Implemented & Integrated
#### 1. **Theme Presets (ABACUS_THEMES)**
**Status:** ✅ Fully integrated
**Files Updated:**
- `apps/web/src/components/MyAbacus.tsx` - Now uses `ABACUS_THEMES.light` and `ABACUS_THEMES.trophy`
- `apps/web/src/components/HeroAbacus.tsx` - Now uses `ABACUS_THEMES.light`
- `apps/web/src/components/LevelSliderDisplay.tsx` - Now uses `ABACUS_THEMES.dark`
**Code Eliminated:** ~60 lines of duplicate theme style definitions
---
#### 2. **Compact Prop**
**Status:** ✅ Fully integrated
**Files Updated:**
- `apps/web/src/app/arcade/complement-race/components/AbacusTarget.tsx` - Now uses `compact={true}`
**Before:**
```tsx
<AbacusReact
value={number}
columns={1}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.72}
customStyles={{ columnPosts: { opacity: 0 } }}
/>
```
**After:**
```tsx
<AbacusReact
value={number}
columns={1}
compact={true}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
scaleFactor={0.72}
/>
```
---
#### 3. **Utility Functions**
**Status:** ✅ Fully integrated
**Files Updated:**
- `apps/web/src/utils/beadDiff.ts` - Now re-exports from abacus-react
- `apps/web/src/utils/abacusInstructionGenerator.ts` - Now re-exports from abacus-react
- `apps/web/src/components/tutorial/TutorialPlayer.tsx` - Imports `calculateBeadDiffFromValues` from abacus-react
- `apps/web/src/components/tutorial/TutorialEditor.tsx` - Imports `calculateBeadDiffFromValues` from abacus-react
**Exports from abacus-react:**
- `numberToAbacusState()`
- `abacusStateToNumber()`
- `calculateBeadChanges()`
- `calculateBeadDiff()`
- `calculateBeadDiffFromValues()`
- `validateAbacusValue()`
- `areStatesEqual()`
**Code Eliminated:** ~200+ lines of duplicate utility implementations
---
#### 4. **React Hooks**
**Status:** ✅ Exported and ready to use
**Available Hooks:**
- `useAbacusDiff(fromValue, toValue, maxPlaces)` - Memoized bead diff calculation
- `useAbacusState(value, maxPlaces)` - Memoized state conversion
**Not yet used in app** (available for future tutorials)
---
#### 5. **Column Highlighting**
**Status:** ✅ Implemented, not yet used
**New Props:**
- `highlightColumns?: number[]` - Highlight specific columns
- `columnLabels?: string[]` - Add educational labels above columns
**Usage Example:**
```tsx
<AbacusReact
value={123}
highlightColumns={[1]}
columnLabels={['ones', 'tens', 'hundreds']}
/>
```
---
### Code Deduplication Summary
**Total Lines Eliminated:** ~260-300 lines
**Breakdown:**
- Theme style definitions: ~60 lines
- Utility function implementations: ~200 lines
- Custom styles for inline abacus: ~5-10 lines per usage
---
### Remaining Work (Optional Future Enhancements)
1. Use `highlightColumns` and `columnLabels` in tutorial components
2. Replace manual bead diff calculations with `useAbacusDiff` hook in interactive tutorials
3. Use `useAbacusState` for state inspection in debugging/development tools
4. Consider implementing `frameVisible` toggles in settings pages
---
### Files Modified
**packages/abacus-react:**
- `src/AbacusReact.tsx` - Added new props (frameVisible, compact, highlightColumns, columnLabels)
- `src/AbacusThemes.ts` - **NEW FILE** - 6 theme presets
- `src/AbacusUtils.ts` - **NEW FILE** - Core utility functions
- `src/AbacusHooks.ts` - **NEW FILE** - React hooks
- `src/index.ts` - Updated exports
- `src/AbacusReact.themes-and-utilities.stories.tsx` - **NEW FILE** - Storybook demos
- `README.md` - Updated with new features documentation
- `ENHANCEMENT_PLAN.md` - Updated with completion status
**apps/web:**
- `src/components/MyAbacus.tsx` - Using ABACUS_THEMES
- `src/components/HeroAbacus.tsx` - Using ABACUS_THEMES
- `src/components/LevelSliderDisplay.tsx` - Using ABACUS_THEMES
- `src/app/arcade/complement-race/components/AbacusTarget.tsx` - Using compact prop
- `src/components/tutorial/TutorialPlayer.tsx` - Importing from abacus-react
- `src/components/tutorial/TutorialEditor.tsx` - Importing from abacus-react
- `src/utils/beadDiff.ts` - Re-exports from abacus-react
- `src/utils/abacusInstructionGenerator.ts` - Re-exports from abacus-react
---
### Testing
✅ Build successful for packages/abacus-react
✅ TypeScript compilation passes for integrated files
✅ Runtime tests confirm functions work correctly
✅ Storybook stories demonstrate all new features
---
### Next Steps
1. Monitor app for any runtime issues with the new integrations
2. Consider using hooks in future tutorial implementations
3. Explore using column highlighting in educational content
4. Document best practices for theme usage in the app

View File

@@ -13,6 +13,8 @@ A comprehensive React component for rendering interactive Soroban (Japanese abac
- 🔧 **Developer-friendly** - Comprehensive hooks and callback system
- 🎓 **Tutorial system** - Built-in overlay and guidance capabilities
- 🧩 **Framework-free SVG** - Complete control over rendering
-**3D Enhancement** - Three levels of progressive 3D effects for immersive visuals
- 🚀 **Server Component support** - AbacusStatic works in React Server Components (Next.js App Router)
## Installation
@@ -85,34 +87,227 @@ Personalized colors and highlights
/>
```
### Theme Presets
Use pre-defined themes for quick styling:
```tsx
import { AbacusReact, ABACUS_THEMES } from '@soroban/abacus-react';
// Available themes: 'light', 'dark', 'trophy', 'translucent', 'solid', 'traditional'
<AbacusReact
value={123}
columns={3}
customStyles={ABACUS_THEMES.dark}
/>
<AbacusReact
value={456}
columns={3}
customStyles={ABACUS_THEMES.trophy} // Golden frame for achievements
/>
<AbacusReact
value={789}
columns={3}
customStyles={ABACUS_THEMES.traditional} // Brown wooden appearance
/>
```
**Available Themes:**
- `light` - Solid white frame with subtle gray accents (best for light backgrounds)
- `dark` - Translucent white with subtle glow (best for dark backgrounds)
- `trophy` - Golden frame with warm tones (best for achievements/rewards)
- `translucent` - Nearly invisible frame (best for inline/minimal UI)
- `solid` - Black frame (best for high contrast/educational contexts)
- `traditional` - Brown wooden appearance (best for traditional soroban aesthetic)
### Static Display (Server Components)
For static, non-interactive displays that work in React Server Components:
```tsx
import { AbacusStatic } from '@soroban/abacus-react';
// ✅ Works in React Server Components - no "use client" needed!
// ✅ No JavaScript sent to client
// ✅ Perfect for SSG, SSR, and static previews
<AbacusStatic
value={123}
columns="auto"
hideInactiveBeads
compact
/>
```
**When to use `AbacusStatic` vs `AbacusReact`:**
| Feature | AbacusStatic | AbacusReact |
|---------|--------------|-------------|
| React Server Components | ✅ Yes | ❌ No (requires "use client") |
| Client-side JavaScript | ❌ None | ✅ Yes |
| User interaction | ❌ No | ✅ Click/drag beads |
| Animations | ❌ No | ✅ Smooth transitions |
| Sound effects | ❌ No | ✅ Optional sounds |
| 3D effects | ❌ No | ✅ Yes |
| Bundle size | 📦 Minimal | 📦 Full-featured |
| Use cases | Preview cards, thumbnails, static pages | Interactive tutorials, games, tools |
```tsx
// Example: Server Component with static abacus cards
// app/flashcards/page.tsx
import { AbacusStatic } from '@soroban/abacus-react'
export default function FlashcardsPage() {
const numbers = [1, 5, 10, 25, 50, 100]
return (
<div className="grid grid-cols-3 gap-4">
{numbers.map(num => (
<div key={num} className="card">
<AbacusStatic value={num} columns="auto" compact />
<p>{num}</p>
</div>
))}
</div>
)
}
```
### Compact/Inline Display
Create mini abacus displays for inline use:
```tsx
// Compact mode - automatically hides frame and optimizes spacing
<AbacusReact
value={7}
columns={1}
compact={true}
hideInactiveBeads={true}
scaleFactor={0.7}
/>
// Or manually control frame visibility
<AbacusReact
value={42}
columns={2}
frameVisible={false} // Hide column posts and reckoning bar
/>
```
### Tutorial System
Educational guidance with tooltips
Educational guidance with tooltips and column highlighting
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/tutorial-mode.svg" alt="Tutorial System">
```tsx
<AbacusReact
value={42}
columns={2}
columns={3}
interactive={true}
// Highlight the tens column with a label
highlightColumns={[1]} // Highlight column index 1 (tens)
columnLabels={['ones', 'tens', 'hundreds']} // Add labels to columns
overlays={[{
id: 'tip',
type: 'tooltip',
target: { type: 'bead', columnIndex: 0, beadType: 'earth', beadPosition: 1 },
content: <div>Click this bead!</div>,
target: { type: 'bead', columnIndex: 1, beadType: 'earth', beadPosition: 1 },
content: <div>Click this bead in the tens column!</div>,
offset: { x: 0, y: -30 }
}]}
callbacks={{
onBeadClick: (event) => {
if (event.columnIndex === 0 && event.beadType === 'earth' && event.position === 1) {
console.log('Correct!');
if (event.columnIndex === 1 && event.beadType === 'earth' && event.position === 1) {
console.log('Correct! You clicked the tens column.');
}
}
}}
/>
```
**Column Highlighting:**
- `highlightColumns` - Array of column indices to highlight (e.g., `[0, 2]` highlights first and third columns)
- `columnLabels` - Optional labels displayed above each column (indexed left to right)
## 3D Enhancement
Make the abacus feel tangible and satisfying with three progressive levels of 3D effects.
### Subtle Mode
Light depth shadows and perspective for subtle dimensionality.
```tsx
<AbacusReact
value={12345}
columns={5}
enhanced3d="subtle"
interactive
animated
/>
```
### Realistic Mode
Material-based rendering with lighting effects and textures.
```tsx
<AbacusReact
value={7890}
columns={4}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy', // 'glossy' | 'satin' | 'matte'
earthBeads: 'satin',
lighting: 'top-down', // 'top-down' | 'ambient' | 'dramatic'
woodGrain: true // Add wood texture to frame
}}
interactive
animated
/>
```
**Materials:**
- `glossy` - High shine with strong highlights
- `satin` - Balanced shine (default)
- `matte` - Subtle shading, no shine
**Lighting:**
- `top-down` - Balanced directional light from above
- `ambient` - Soft light from all directions
- `dramatic` - Strong directional light for high contrast
### Delightful Mode
Maximum satisfaction with enhanced physics and interactive effects.
```tsx
<AbacusReact
value={8642}
columns={4}
enhanced3d="delightful"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'satin',
lighting: 'dramatic',
woodGrain: true
}}
physics3d={{
hoverParallax: true // Beads lift on hover with Z-depth
}}
interactive
animated
soundEnabled
/>
```
**Physics Options:**
- `hoverParallax` - Beads near mouse cursor lift up with depth perception
All 3D modes work with existing configurations and preserve exact geometry.
## Core API
@@ -132,10 +327,18 @@ interface AbacusConfig {
colorPalette?: 'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature';
hideInactiveBeads?: boolean; // Hide/show inactive beads
// Layout & Frame
frameVisible?: boolean; // Show/hide column posts and reckoning bar
compact?: boolean; // Compact layout (implies frameVisible=false)
// Interaction
interactive?: boolean; // Enable user interactions
animated?: boolean; // Enable animations
gestures?: boolean; // Enable drag gestures
// Tutorial Features
highlightColumns?: number[]; // Highlight specific columns by index
columnLabels?: string[]; // Optional labels for columns
}
```
@@ -282,6 +485,60 @@ function AdvancedExample() {
## Hooks
### useAbacusDiff
Calculate bead differences between values for tutorials and animations:
```tsx
import { useAbacusDiff } from '@soroban/abacus-react';
function Tutorial() {
const [currentValue, setCurrentValue] = useState(5);
const targetValue = 15;
// Get diff information: which beads need to move
const diff = useAbacusDiff(currentValue, targetValue);
return (
<div>
<p>{diff.summary}</p> {/* "add heaven bead in tens column, then..." */}
<AbacusReact
value={currentValue}
stepBeadHighlights={diff.highlights} // Highlight beads that need to change
interactive
onValueChange={setCurrentValue}
/>
<p>Changes needed: {diff.changes.length}</p>
</div>
);
}
```
**Returns:**
- `changes` - Array of bead movements with direction and order
- `highlights` - Bead highlight data for stepBeadHighlights prop
- `hasChanges` - Boolean indicating if any changes needed
- `summary` - Human-readable description of changes (e.g., "add heaven bead in ones column")
### useAbacusState
Convert numbers to abacus bead states:
```tsx
import { useAbacusState } from '@soroban/abacus-react';
function BeadAnalyzer() {
const value = 123;
const state = useAbacusState(value);
// Check bead positions
const onesHasHeaven = state[0].heavenActive; // false (3 < 5)
const tensEarthCount = state[1].earthActive; // 2 (20 = 2 tens)
return <div>Ones column heaven bead: {onesHasHeaven ? 'active' : 'inactive'}</div>;
}
```
### useAbacusDimensions
Get exact sizing information for layout planning:
@@ -300,6 +557,92 @@ function MyComponent() {
}
```
## Utility Functions
Low-level functions for working with abacus states and calculations:
### numberToAbacusState
Convert a number to bead positions:
```tsx
import { numberToAbacusState } from '@soroban/abacus-react';
const state = numberToAbacusState(123, 5); // 5 columns
// Returns: {
// 0: { heavenActive: false, earthActive: 3 }, // ones = 3
// 1: { heavenActive: false, earthActive: 2 }, // tens = 2
// 2: { heavenActive: true, earthActive: 0 }, // hundreds = 1
// ...
// }
```
### abacusStateToNumber
Convert bead positions back to a number:
```tsx
import { abacusStateToNumber } from '@soroban/abacus-react';
const state = {
0: { heavenActive: false, earthActive: 3 },
1: { heavenActive: false, earthActive: 2 },
2: { heavenActive: true, earthActive: 0 }
};
const value = abacusStateToNumber(state); // 123
```
### calculateBeadDiff
Calculate the exact bead movements needed between two states:
```tsx
import { calculateBeadDiff, numberToAbacusState } from '@soroban/abacus-react';
const fromState = numberToAbacusState(5);
const toState = numberToAbacusState(15);
const diff = calculateBeadDiff(fromState, toState);
console.log(diff.summary); // "add heaven bead in tens column"
console.log(diff.changes); // Detailed array of movements with order
```
### calculateBeadDiffFromValues
Convenience wrapper for calculating diff from numbers:
```tsx
import { calculateBeadDiffFromValues } from '@soroban/abacus-react';
const diff = calculateBeadDiffFromValues(42, 57);
// Equivalent to: calculateBeadDiff(numberToAbacusState(42), numberToAbacusState(57))
```
### validateAbacusValue
Check if a value is within the supported range:
```tsx
import { validateAbacusValue } from '@soroban/abacus-react';
const result = validateAbacusValue(123456, 5); // 5 columns max
console.log(result.isValid); // false
console.log(result.error); // "Value exceeds maximum for 5 columns (max: 99999)"
```
### areStatesEqual
Compare two abacus states:
```tsx
import { areStatesEqual, numberToAbacusState } from '@soroban/abacus-react';
const state1 = numberToAbacusState(123);
const state2 = numberToAbacusState(123);
const isEqual = areStatesEqual(state1, state2); // true
```
## Educational Use Cases
### Interactive Math Lessons
@@ -362,14 +705,37 @@ Full TypeScript definitions included:
```tsx
import {
// Components
AbacusReact,
// Hooks
useAbacusDiff,
useAbacusState,
useAbacusDimensions,
// Utility Functions
numberToAbacusState,
abacusStateToNumber,
calculateBeadDiff,
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual,
// Theme Presets
ABACUS_THEMES,
// Types
AbacusConfig,
BeadConfig,
BeadClickEvent,
AbacusCustomStyles,
AbacusOverlay,
AbacusCallbacks,
useAbacusDimensions
AbacusState,
BeadState,
BeadDiffResult,
BeadDiffOutput,
AbacusThemeName
} from '@soroban/abacus-react';
// All interfaces fully typed for excellent developer experience

View File

@@ -103,7 +103,7 @@
/* Wood grain texture overlay */
.abacus-3d-container.enhanced-realistic .frame-wood {
opacity: 0.15;
opacity: 0.4;
mix-blend-mode: multiply;
pointer-events: none;
}
@@ -293,11 +293,6 @@
z-index: -1;
}
/* Wobble physics - applied via inline styles from React Spring */
.bead-wobble {
/* transform-origin set dynamically */
transform-style: preserve-3d;
}
/* Frame depth enhancement */
.abacus-3d-container.enhanced-delightful rect[class*="column-post"],
@@ -307,25 +302,11 @@
drop-shadow(0 0 2px rgba(0, 0, 0, 0.1));
}
/* Wood grain texture - enhanced */
.frame-wood-enhanced {
background-image:
repeating-linear-gradient(
90deg,
transparent,
transparent 2px,
rgba(139, 90, 43, 0.03) 2px,
rgba(139, 90, 43, 0.03) 4px
),
repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(101, 67, 33, 0.02) 1px,
rgba(101, 67, 33, 0.02) 2px
);
opacity: 0.2;
/* Wood grain texture - enhanced for delightful mode */
.abacus-3d-container.enhanced-delightful .frame-wood {
opacity: 0.45;
mix-blend-mode: multiply;
pointer-events: none;
}
/* Accessibility - Reduced motion */

View File

@@ -124,7 +124,7 @@ export function getLightingFilter(lighting: LightingStyle = "top-down"): string
* Calculate Z-depth for a bead based on enhancement level and state
*/
export function getBeadZDepth(
enhanced3d: boolean | "subtle" | "realistic" | "delightful",
enhanced3d: boolean | "subtle" | "realistic",
active: boolean
): number {
if (!enhanced3d || enhanced3d === true) return 0;
@@ -136,77 +136,28 @@ export function getBeadZDepth(
return 6;
case "realistic":
return 10;
case "delightful":
return 12;
default:
return 0;
}
}
/**
* Generate wobble rotation based on velocity (for delightful mode)
*/
export function getWobbleRotation(velocity: number, axis: "x" | "y" = "x"): string {
const maxRotation = 3; // degrees
const rotation = Math.max(-maxRotation, Math.min(maxRotation, velocity * -2));
if (axis === "x") {
return `rotateX(${rotation}deg)`;
}
return `rotateY(${rotation}deg)`;
}
/**
* Calculate parallax offset based on mouse position
*/
export function calculateParallaxOffset(
beadX: number,
beadY: number,
mouseX: number,
mouseY: number,
containerX: number,
containerY: number,
intensity: number = 0.5
): { x: number; y: number; z: number } {
// Calculate distance from bead center to mouse
const dx = (mouseX - containerX) - beadX;
const dy = (mouseY - containerY) - beadY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Max influence radius (pixels)
const maxRadius = 150;
if (distance > maxRadius) {
return { x: 0, y: 0, z: 0 };
}
// Calculate lift amount (inverse square falloff)
const influence = Math.max(0, 1 - (distance / maxRadius));
const lift = influence * influence * intensity;
return {
x: dx * lift * 0.1,
y: dy * lift * 0.1,
z: lift * 8
};
}
/**
* Generate wood grain texture SVG pattern
*/
export function getWoodGrainPattern(id: string): string {
return `
<pattern id="${id}" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
<rect width="100" height="100" fill="#8B5A2B" opacity="0.3"/>
<!-- Grain lines -->
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
<!-- Knots -->
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.2"/>
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.2"/>
<rect width="100" height="100" fill="#8B5A2B" opacity="0.5"/>
<!-- Grain lines - more visible -->
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
<!-- Knots - more prominent -->
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.35"/>
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.35"/>
<ellipse cx="45" cy="82" rx="5" ry="7" fill="#654321" opacity="0.3"/>
</pattern>
`;
}
@@ -215,9 +166,8 @@ export function getWoodGrainPattern(id: string): string {
* Get container class names for 3D enhancement level
*/
export function get3DContainerClasses(
enhanced3d: boolean | "subtle" | "realistic" | "delightful" | undefined,
lighting?: LightingStyle,
parallaxEnabled?: boolean
enhanced3d: boolean | "subtle" | "realistic" | undefined,
lighting?: LightingStyle
): string {
const classes: string[] = ["abacus-3d-container"];
@@ -228,8 +178,6 @@ export function get3DContainerClasses(
classes.push("enhanced-subtle");
} else if (enhanced3d === "realistic") {
classes.push("enhanced-realistic");
} else if (enhanced3d === "delightful") {
classes.push("enhanced-delightful");
}
// Add lighting class
@@ -237,11 +185,6 @@ export function get3DContainerClasses(
classes.push(`lighting-${lighting}`);
}
// Add parallax class
if (parallaxEnabled && enhanced3d === "delightful") {
classes.push("parallax-enabled");
}
return classes.join(" ");
}
@@ -260,7 +203,7 @@ export function getBeadGradientId(
/**
* Physics config for different enhancement levels
*/
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" | "delightful") {
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic") {
const base = {
tension: 300,
friction: 22,
@@ -272,20 +215,11 @@ export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" |
return { ...base, clamp: true };
}
if (enhanced3d === "realistic") {
return {
tension: 320,
friction: 24,
mass: 0.6,
clamp: false
};
}
// delightful
// realistic
return {
tension: 280,
friction: 20,
mass: 0.7,
clamp: false, // Allow overshoot for satisfying settle
tension: 320,
friction: 24,
mass: 0.6,
clamp: false
};
}

View File

@@ -0,0 +1,71 @@
/**
* Utility hooks for working with abacus calculations and state
*/
import { useMemo } from 'react'
import {
calculateBeadDiffFromValues,
numberToAbacusState,
type BeadDiffOutput,
type AbacusState,
} from './AbacusUtils'
/**
* Hook to calculate bead differences between two values
* Useful for tutorials, animations, and highlighting which beads need to move
*
* @param fromValue - Starting value
* @param toValue - Target value
* @param maxPlaces - Maximum number of place values to consider (default: 5)
* @returns BeadDiffOutput with changes, highlights, and summary
*
* @example
* ```tsx
* function Tutorial() {
* const diff = useAbacusDiff(5, 15)
*
* return (
* <AbacusReact
* value={currentValue}
* stepBeadHighlights={diff.highlights}
* />
* )
* }
* ```
*/
export function useAbacusDiff(
fromValue: number | bigint,
toValue: number | bigint,
maxPlaces: number = 5
): BeadDiffOutput {
return useMemo(() => {
return calculateBeadDiffFromValues(fromValue, toValue, maxPlaces)
}, [fromValue, toValue, maxPlaces])
}
/**
* Hook to convert a number to abacus state
* Memoized for performance when used in components
*
* @param value - The number to convert
* @param maxPlaces - Maximum number of place values (default: 5)
* @returns AbacusState representing the bead positions
*
* @example
* ```tsx
* function MyComponent() {
* const state = useAbacusState(123)
*
* // Check if ones column has heaven bead active
* const onesHasHeaven = state[0].heavenActive
* }
* ```
*/
export function useAbacusState(
value: number | bigint,
maxPlaces: number = 5
): AbacusState {
return useMemo(() => {
return numberToAbacusState(value, maxPlaces)
}, [value, maxPlaces])
}

View File

@@ -1,7 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AbacusReact } from './AbacusReact';
import React, { useEffect, useRef } from 'react';
import './Abacus3D.css';
import React from 'react';
const meta: Meta<typeof AbacusReact> = {
title: 'Soroban/3D Effects Showcase',
@@ -13,29 +12,20 @@ const meta: Meta<typeof AbacusReact> = {
component: `
# 3D Enhancement Showcase
Three levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
Two levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
## Proposal 1: Subtle (CSS Perspective + Shadows)
## Subtle (CSS Perspective + Shadows)
- Light perspective tilt
- Depth shadows on active beads
- Smooth transitions
- **Zero performance cost**
## Proposal 2: Realistic (Lighting + Materials)
- Everything from Proposal 1 +
- Realistic lighting effects
- Material-based bead rendering (glossy/satin/matte)
- Ambient occlusion
- Frame depth
## Proposal 3: Delightful (Physics + Micro-interactions)
- Everything from Proposal 2 +
- Enhanced physics with satisfying bounce
- Clack ripple effects when beads snap
- Hover parallax
- Maximum satisfaction
**Note:** Currently these are CSS-only demos. Full integration with React Spring physics coming next!
## Realistic (Lighting + Materials)
- Everything from Subtle +
- Realistic lighting effects with material gradients
- Glossy/Satin/Matte bead materials
- Wood grain textures on frame
- Enhanced physics for realistic motion
`
}
}
@@ -46,596 +36,387 @@ Three levels of progressive 3D enhancement for the abacus to make interactions f
export default meta;
type Story = StoryObj<typeof meta>;
// Wrapper component to apply 3D CSS classes
const Wrapper3D: React.FC<{
children: React.ReactNode;
level: 'subtle' | 'realistic' | 'delightful';
lighting?: 'top-down' | 'ambient' | 'dramatic';
}> = ({ children, level, lighting }) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (containerRef.current) {
const svg = containerRef.current.querySelector('.abacus-svg');
const beads = containerRef.current.querySelectorAll('.abacus-bead');
// Add classes to container
containerRef.current.classList.add('abacus-3d-container');
containerRef.current.classList.add(`enhanced-${level}`);
if (lighting) {
containerRef.current.classList.add(`lighting-${lighting}`);
}
// Apply will-change for performance
if (level === 'delightful') {
beads.forEach(bead => {
(bead as HTMLElement).style.willChange = 'transform, filter';
});
}
}
}, [level, lighting]);
return <div ref={containerRef}>{children}</div>;
};
// ============================================
// PROPOSAL 1: SUBTLE
// SIDE-BY-SIDE COMPARISON
// ============================================
export const Subtle_Static: Story = {
name: '1. Subtle - Static Display',
export const CompareAllLevels: Story = {
name: '🎯 Compare All Levels',
render: () => (
<Wrapper3D level="subtle">
<AbacusReact
value={12345}
columns={5}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Subtle 3D with light perspective tilt and depth shadows. Notice the slight elevation of active beads.'
}
}
}
};
export const Subtle_Interactive: Story = {
name: '1. Subtle - Interactive',
render: () => (
<Wrapper3D level="subtle">
<AbacusReact
value={678}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="heaven-earth"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Subtle 3D + interaction. Click beads to see depth shadows change. Notice how the perspective gives a sense of physicality.'
}
}
}
};
export const Subtle_Tutorial: Story = {
name: '1. Subtle - Tutorial Mode',
render: () => {
const [step, setStep] = React.useState(0);
const highlights = [
{ placeValue: 0, beadType: 'earth' as const, position: 2 },
{ placeValue: 1, beadType: 'heaven' as const },
{ placeValue: 2, beadType: 'earth' as const, position: 0 },
];
return (
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="subtle">
<AbacusReact
value={123}
columns={3}
showNumbers
interactive
animated
highlightBeads={[highlights[step]]}
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<div style={{ marginTop: '20px' }}>
<button onClick={() => setStep((step + 1) % 3)} style={{ padding: '8px 16px' }}>
Next Step ({step + 1}/3)
</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', alignItems: 'center' }}>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>No Enhancement</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
/>
</div>
);
},
parameters: {
docs: {
description: {
story: 'Tutorial mode with subtle 3D effects. The depth helps highlight which bead to focus on.'
}
}
}
};
// ============================================
// PROPOSAL 2: REALISTIC
// ============================================
export const Realistic_TopDown: Story = {
name: '2. Realistic - Top-Down Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={24680}
columns={5}
showNumbers
colorScheme="place-value"
beadShape="circle"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with top-down lighting. Notice the enhanced shadows and sense of illumination from above.'
}
}
}
};
export const Realistic_Ambient: Story = {
name: '2. Realistic - Ambient Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="ambient">
<AbacusReact
value={13579}
columns={5}
showNumbers
colorScheme="place-value"
beadShape="diamond"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with ambient lighting. Softer, more even illumination creates a cozy feel.'
}
}
}
};
export const Realistic_Dramatic: Story = {
name: '2. Realistic - Dramatic Lighting',
render: () => (
<Wrapper3D level="realistic" lighting="dramatic">
<AbacusReact
value={99999}
columns={5}
showNumbers
colorScheme="heaven-earth"
beadShape="square"
colorPalette="colorblind"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D with dramatic lighting. Strong directional light creates bold shadows and depth.'
}
}
}
};
export const Realistic_Interactive: Story = {
name: '2. Realistic - Interactive',
render: () => (
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={555}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="place-value"
colorPalette="nature"
scaleFactor={1.3}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D + interaction. Click beads and watch the enhanced shadows and lighting respond. Feel that satisfaction!'
}
}
}
};
export const Realistic_AllShapes: Story = {
name: '2. Realistic - All Bead Shapes',
render: () => (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="diamond"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Diamond</p>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Subtle</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="subtle"
/>
</div>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="circle"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Circle</p>
</div>
<div style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={777}
columns={3}
showNumbers
beadShape="square"
colorScheme="place-value"
/>
</Wrapper3D>
<p style={{ marginTop: '12px', fontSize: '14px' }}>Square</p>
<div>
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Realistic (Satin Beads + Wood Frame)</h3>
<AbacusReact
value={4242}
columns={4}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down',
woodGrain: true
}}
/>
</div>
</div>
),
parameters: {
docs: {
description: {
story: 'Realistic 3D works beautifully with all three bead shapes.'
story: 'Side-by-side comparison of both enhancement levels. **Click beads** to see how they move!'
}
}
}
};
// ============================================
// PROPOSAL 3: DELIGHTFUL
// PROPOSAL 1: SUBTLE
// ============================================
export const Delightful_Static: Story = {
name: '3. Delightful - Maximum Depth',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={11111}
columns={5}
showNumbers
colorScheme="alternating"
beadShape="circle"
colorPalette="mnemonic"
scaleFactor={1.2}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D with maximum depth and richness. The beads really pop off the page!'
}
}
}
};
export const Delightful_Interactive: Story = {
name: '3. Delightful - Interactive (Physics Ready)',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={987}
columns={3}
showNumbers
interactive
animated
soundEnabled
colorScheme="place-value"
scaleFactor={1.3}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D + interaction. This is the CSS foundation - physics effects (wobble, clack ripple) will be added in the next iteration. Already feels great!'
}
}
}
};
export const Delightful_LargeScale: Story = {
name: '3. Delightful - Large Scale',
render: () => (
<Wrapper3D level="delightful">
<AbacusReact
value={9876543210}
columns={10}
showNumbers
colorScheme="place-value"
scaleFactor={1}
/>
</Wrapper3D>
),
parameters: {
docs: {
description: {
story: 'Delightful 3D scales beautifully even with many columns. The depth hierarchy helps organize the visual.'
}
}
}
};
// ============================================
// COMPARISON VIEWS
// ============================================
export const CompareAllLevels: Story = {
name: 'Compare All Three Levels',
render: () => {
const value = 4242;
const columns = 4;
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', padding: '20px' }}>
{/* No 3D */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
No Enhancement (Current)
</h3>
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</div>
{/* Subtle */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 1: Subtle 😊
</h3>
<Wrapper3D level="subtle">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Light tilt + depth shadows
</p>
</div>
{/* Realistic */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 2: Realistic 😍
</h3>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Lighting + materials + ambient occlusion
</p>
</div>
{/* Delightful */}
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
Proposal 3: Delightful 🤩
</h3>
<Wrapper3D level="delightful">
<AbacusReact
value={value}
columns={columns}
showNumbers
colorScheme="place-value"
scaleFactor={1.2}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
Maximum depth + enhanced lighting (physics effects coming next!)
</p>
</div>
</div>
);
export const Subtle_Basic: Story = {
name: '1⃣ Subtle - Basic',
args: {
value: 12345,
columns: 5,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'place-value',
scaleFactor: 1.2,
enhanced3d: 'subtle'
},
parameters: {
docs: {
description: {
story: 'Side-by-side comparison of all three enhancement levels. Which feels best to you?'
story: 'Subtle 3D with light perspective tilt and depth shadows. Click beads to interact!'
}
}
}
};
export const CompareInteractive: Story = {
name: 'Compare Interactive (Side-by-Side)',
render: () => {
const [value1, setValue1] = React.useState(123);
const [value2, setValue2] = React.useState(456);
const [value3, setValue3] = React.useState(789);
// ============================================
// PROPOSAL 2: REALISTIC (Materials)
// ============================================
return (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Subtle</h4>
<Wrapper3D level="subtle">
<AbacusReact
value={value1}
onValueChange={(v) => setValue1(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Realistic</h4>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value2}
onValueChange={(v) => setValue2(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Delightful</h4>
<Wrapper3D level="delightful">
<AbacusReact
value={value3}
onValueChange={(v) => setValue3(Number(v))}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
/>
</Wrapper3D>
</div>
</div>
);
export const Realistic_GlossyBeads: Story = {
name: '2⃣ Realistic - Glossy Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'top-down'
}
},
parameters: {
docs: {
description: {
story: 'Try all three side-by-side! Click beads and feel the difference in satisfaction.'
story: '**Glossy material** with high shine and strong highlights. Notice the radial gradients on the beads!'
}
}
}
};
export const Realistic_SatinBeads: Story = {
name: '2⃣ Realistic - Satin Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down'
}
},
parameters: {
docs: {
description: {
story: '**Satin material** (default) with balanced shine. Medium highlights, smooth appearance.'
}
}
}
};
export const Realistic_MatteBeads: Story = {
name: '2⃣ Realistic - Matte Beads',
args: {
value: 7890,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'matte',
earthBeads: 'matte',
lighting: 'ambient'
}
},
parameters: {
docs: {
description: {
story: '**Matte material** with subtle shading, no shine. Flat, understated appearance.'
}
}
}
};
export const Realistic_MixedMaterials: Story = {
name: '2⃣ Realistic - Mixed Materials',
args: {
value: 5678,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'heaven-earth',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'glossy', // Heaven beads are shiny
earthBeads: 'matte', // Earth beads are flat
lighting: 'dramatic'
}
},
parameters: {
docs: {
description: {
story: '**Mixed materials**: Glossy heaven beads (5-value) + Matte earth beads (1-value). Different visual weight!'
}
}
}
};
export const Realistic_WoodGrain: Story = {
name: '2⃣ Realistic - Wood Grain Frame',
args: {
value: 3456,
columns: 4,
showNumbers: true,
interactive: true,
animated: true,
colorScheme: 'monochrome',
scaleFactor: 1.3,
enhanced3d: 'realistic',
material3d: {
heavenBeads: 'satin',
earthBeads: 'satin',
lighting: 'top-down',
woodGrain: true // Enable wood texture on frame
}
},
parameters: {
docs: {
description: {
story: '**Wood grain texture** overlaid on the frame (rods and reckoning bar). Traditional soroban aesthetic!'
}
}
}
};
export const Realistic_LightingComparison: Story = {
name: '2⃣ Realistic - Lighting Comparison',
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px', alignItems: 'center' }}>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Top-Down Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'top-down'
}}
/>
</div>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Ambient Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'ambient'
}}
/>
</div>
<div>
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Dramatic Lighting</h4>
<AbacusReact
value={999}
columns={3}
showNumbers
interactive
animated
colorScheme="place-value"
scaleFactor={1.2}
enhanced3d="realistic"
material3d={{
heavenBeads: 'glossy',
earthBeads: 'glossy',
lighting: 'dramatic'
}}
/>
</div>
</div>
),
parameters: {
docs: {
description: {
story: 'Compare different **lighting styles**: top-down (balanced), ambient (soft all around), dramatic (strong directional).'
}
}
}
};
// ============================================
// FEATURE TESTS
// INTERACTIVE PLAYGROUND
// ============================================
export const ColorSchemes_With3D: Story = {
name: '3D Works With All Color Schemes',
export const Playground: Story = {
name: '🎮 Interactive Playground',
render: () => {
const value = 333;
const schemes: Array<'monochrome' | 'place-value' | 'alternating' | 'heaven-earth'> = [
'monochrome',
'place-value',
'alternating',
'heaven-earth'
];
const [level, setLevel] = React.useState<'subtle' | 'realistic'>('realistic');
const [material, setMaterial] = React.useState<'glossy' | 'satin' | 'matte'>('glossy');
const [lighting, setLighting] = React.useState<'top-down' | 'ambient' | 'dramatic'>('dramatic');
const [woodGrain, setWoodGrain] = React.useState(true);
return (
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
{schemes.map(scheme => (
<div key={scheme} style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={3}
showNumbers
colorScheme={scheme}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
{scheme}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '30px', alignItems: 'center' }}>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '20px',
padding: '20px',
background: '#f5f5f5',
borderRadius: '8px',
maxWidth: '500px'
}}>
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Enhancement Level</label>
<select value={level} onChange={e => setLevel(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="subtle">Subtle</option>
<option value="realistic">Realistic</option>
</select>
</div>
))}
</div>
);
},
parameters: {
docs: {
description: {
story: 'The 3D effects work seamlessly with all existing color schemes.'
}
}
}
};
export const ColorPalettes_With3D: Story = {
name: '3D Works With All Palettes',
render: () => {
const value = 555;
const palettes: Array<'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature'> = [
'default',
'colorblind',
'mnemonic',
'grayscale',
'nature'
];
return (
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
{palettes.map(palette => (
<div key={palette} style={{ textAlign: 'center' }}>
<Wrapper3D level="realistic" lighting="top-down">
<AbacusReact
value={value}
columns={3}
showNumbers
colorScheme="place-value"
colorPalette={palette}
/>
</Wrapper3D>
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
{palette}
</p>
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Bead Material</label>
<select value={material} onChange={e => setMaterial(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="glossy">Glossy</option>
<option value="satin">Satin</option>
<option value="matte">Matte</option>
</select>
</div>
))}
<div>
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Lighting</label>
<select value={lighting} onChange={e => setLighting(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
<option value="top-down">Top-Down</option>
<option value="ambient">Ambient</option>
<option value="dramatic">Dramatic</option>
</select>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<input type="checkbox" checked={woodGrain} onChange={e => setWoodGrain(e.target.checked)} />
<span>Wood Grain</span>
</label>
</div>
</div>
<AbacusReact
value={6789}
columns={4}
showNumbers
interactive
animated
soundEnabled
colorScheme="rainbow"
scaleFactor={1.4}
enhanced3d={level}
material3d={{
heavenBeads: material,
earthBeads: material,
lighting: lighting,
woodGrain: woodGrain
}}
/>
<p style={{ maxWidth: '500px', textAlign: 'center', color: '#666' }}>
Click beads to interact! Try different combinations above to find your favorite look and feel.
</p>
</div>
);
},
parameters: {
docs: {
description: {
story: 'The 3D effects enhance all color palettes beautifully.'
story: 'Experiment with all the 3D options! Mix and match materials, lighting, and physics to find your perfect configuration.'
}
}
}

View File

@@ -0,0 +1,613 @@
/**
* Theme system, layout utilities, hooks, and helper functions
* Features: Theme presets, compact mode, column highlighting, hooks, utility functions
*/
import type { Meta, StoryObj } from '@storybook/react';
import React, { useState } from 'react';
import AbacusReact from './AbacusReact';
import {
ABACUS_THEMES,
AbacusThemeName,
useAbacusDiff,
useAbacusState,
numberToAbacusState,
abacusStateToNumber,
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual
} from './index';
const meta = {
title: 'AbacusReact/Themes & Utilities',
component: AbacusReact,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof AbacusReact>;
export default meta;
type Story = StoryObj<typeof meta>;
// ============================================================================
// THEME PRESETS
// ============================================================================
export const AllThemePresets: Story = {
render: () => {
const themes: AbacusThemeName[] = ['light', 'dark', 'trophy', 'translucent', 'solid', 'traditional'];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '30px', padding: '20px' }}>
<h2>Theme Presets</h2>
<p>Pre-defined themes eliminate manual style object creation</p>
{themes.map((themeName) => (
<div key={themeName} style={{
background: themeName === 'dark' ? '#1a1a1a' : '#f5f5f5',
padding: '20px',
borderRadius: '8px'
}}>
<h3 style={{
marginTop: 0,
color: themeName === 'dark' ? 'white' : 'black',
textTransform: 'capitalize'
}}>
{themeName} Theme
</h3>
<AbacusReact
value={12345}
columns={5}
customStyles={ABACUS_THEMES[themeName]}
showNumbers={true}
/>
</div>
))}
</div>
);
},
};
export const LightTheme: Story = {
render: () => (
<div style={{ padding: '20px', background: '#f5f5f5' }}>
<h3>Light Theme - Best for light backgrounds</h3>
<AbacusReact
value={12345}
columns={5}
customStyles={ABACUS_THEMES.light}
showNumbers={true}
/>
</div>
),
};
export const DarkTheme: Story = {
render: () => (
<div style={{ padding: '20px', background: '#1a1a1a' }}>
<h3 style={{ color: 'white' }}>Dark Theme - Best for dark backgrounds</h3>
<AbacusReact
value={12345}
columns={5}
customStyles={ABACUS_THEMES.dark}
showNumbers={true}
/>
</div>
),
};
export const TrophyTheme: Story = {
render: () => (
<div style={{ padding: '20px', background: '#f0f0f0' }}>
<h3>Trophy Theme - Golden frame for achievements</h3>
<AbacusReact
value={9999}
columns={4}
customStyles={ABACUS_THEMES.trophy}
showNumbers={true}
/>
</div>
),
};
export const TraditionalTheme: Story = {
render: () => (
<div style={{ padding: '20px', background: '#f5f5f0' }}>
<h3>Traditional Theme - Brown wooden soroban aesthetic</h3>
<AbacusReact
value={8765}
columns={4}
customStyles={ABACUS_THEMES.traditional}
showNumbers={true}
/>
</div>
),
};
// ============================================================================
// COMPACT MODE & FRAME VISIBILITY
// ============================================================================
export const CompactMode: Story = {
render: () => (
<div style={{ padding: '20px' }}>
<h3>Compact Mode - Inline mini-abacus displays</h3>
<p>Perfect for inline number displays, badges, or game UI</p>
<div style={{ display: 'flex', gap: '20px', alignItems: 'center', marginTop: '20px' }}>
<span>Single digits: </span>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => (
<AbacusReact
key={num}
value={num}
columns={1}
compact={true}
hideInactiveBeads={true}
scaleFactor={0.6}
/>
))}
</div>
<div style={{ marginTop: '30px' }}>
<h4>Two-digit compact displays:</h4>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
{[12, 34, 56, 78, 99].map(num => (
<AbacusReact
key={num}
value={num}
columns={2}
compact={true}
hideInactiveBeads={true}
scaleFactor={0.7}
/>
))}
</div>
</div>
</div>
),
};
export const FrameVisibilityControl: Story = {
render: () => {
const [frameVisible, setFrameVisible] = useState(true);
return (
<div style={{ padding: '20px' }}>
<h3>Frame Visibility Control</h3>
<p>Toggle column posts and reckoning bar on/off</p>
<div style={{ marginBottom: '20px' }}>
<label>
<input
type="checkbox"
checked={frameVisible}
onChange={(e) => setFrameVisible(e.target.checked)}
/>
{' '}Show Frame
</label>
</div>
<AbacusReact
value={12345}
columns={5}
frameVisible={frameVisible}
showNumbers={true}
/>
</div>
);
},
};
// ============================================================================
// COLUMN HIGHLIGHTING & LABELS
// ============================================================================
export const ColumnHighlighting: Story = {
render: () => (
<div style={{ padding: '20px' }}>
<h3>Column Highlighting</h3>
<p>Highlight specific columns for educational purposes</p>
<div style={{ marginBottom: '30px' }}>
<h4>Highlight ones column:</h4>
<AbacusReact
value={12345}
columns={5}
highlightColumns={[0]}
showNumbers={true}
/>
</div>
<div style={{ marginBottom: '30px' }}>
<h4>Highlight tens and hundreds:</h4>
<AbacusReact
value={12345}
columns={5}
highlightColumns={[1, 2]}
showNumbers={true}
/>
</div>
<div>
<h4>Highlight all columns:</h4>
<AbacusReact
value={12345}
columns={5}
highlightColumns={[0, 1, 2, 3, 4]}
showNumbers={true}
/>
</div>
</div>
),
};
export const ColumnLabels: Story = {
render: () => (
<div style={{ padding: '20px' }}>
<h3>Column Labels</h3>
<p>Add educational labels above columns</p>
<div style={{ marginBottom: '30px' }}>
<h4>Standard place value labels:</h4>
<AbacusReact
value={12345}
columns={5}
columnLabels={['ones', 'tens', 'hundreds', 'thousands', '10k']}
showNumbers={true}
/>
</div>
<div>
<h4>Custom labels:</h4>
<AbacusReact
value={789}
columns={3}
columnLabels={['1s', '10s', '100s']}
highlightColumns={[1]}
showNumbers={true}
/>
</div>
</div>
),
};
export const ColumnHighlightingWithLabels: Story = {
render: () => (
<div style={{ padding: '20px' }}>
<h3>Combined: Column Highlighting + Labels</h3>
<p>Perfect for tutorials showing which column to work with</p>
<AbacusReact
value={42}
columns={3}
highlightColumns={[1]}
columnLabels={['ones', 'tens', 'hundreds']}
showNumbers={true}
/>
<p style={{ marginTop: '20px', fontStyle: 'italic' }}>
"Add 10 to the tens column"
</p>
</div>
),
};
// ============================================================================
// HOOKS: useAbacusDiff
// ============================================================================
function AbacusDiffDemo() {
const [currentValue, setCurrentValue] = useState(5);
const targetValue = 23;
const diff = useAbacusDiff(currentValue, targetValue);
return (
<div style={{ padding: '20px' }}>
<h3>useAbacusDiff Hook</h3>
<p>Automatically calculate which beads need to move</p>
<div style={{ marginBottom: '20px' }}>
<p><strong>Current value:</strong> {currentValue}</p>
<p><strong>Target value:</strong> {targetValue}</p>
<p><strong>Instructions:</strong> {diff.summary}</p>
<p><strong>Changes needed:</strong> {diff.changes.length}</p>
</div>
<AbacusReact
value={currentValue}
columns={2}
stepBeadHighlights={diff.highlights}
showNumbers={true}
interactive={true}
onValueChange={setCurrentValue}
/>
<div style={{ marginTop: '20px' }}>
<button onClick={() => setCurrentValue(5)}>Reset to 5</button>
{' '}
<button onClick={() => setCurrentValue(targetValue)}>Jump to target (23)</button>
</div>
{diff.hasChanges ? (
<div style={{ marginTop: '20px', color: '#666' }}>
<p><strong>Detailed changes:</strong></p>
<pre style={{ fontSize: '12px' }}>
{JSON.stringify(diff.changes, null, 2)}
</pre>
</div>
) : (
<p style={{ marginTop: '20px', color: 'green', fontWeight: 'bold' }}>
Target reached!
</p>
)}
</div>
);
}
export const UseAbacusDiffHook: Story = {
render: () => <AbacusDiffDemo />,
};
// ============================================================================
// HOOKS: useAbacusState
// ============================================================================
function AbacusStateDemo() {
const [value, setValue] = useState(123);
const state = useAbacusState(value);
return (
<div style={{ padding: '20px' }}>
<h3>useAbacusState Hook</h3>
<p>Convert numbers to bead positions (memoized)</p>
<div style={{ marginBottom: '20px' }}>
<label>
Value:
<input
type="number"
value={value}
onChange={(e) => setValue(parseInt(e.target.value) || 0)}
style={{ marginLeft: '10px', width: '100px' }}
/>
</label>
</div>
<AbacusReact
value={value}
columns={3}
showNumbers={true}
/>
<div style={{ marginTop: '20px', fontSize: '14px' }}>
<p><strong>Bead State Analysis:</strong></p>
<table style={{ borderCollapse: 'collapse', marginTop: '10px' }}>
<thead>
<tr style={{ background: '#f0f0f0' }}>
<th style={{ border: '1px solid #ccc', padding: '8px' }}>Place</th>
<th style={{ border: '1px solid #ccc', padding: '8px' }}>Heaven Active?</th>
<th style={{ border: '1px solid #ccc', padding: '8px' }}>Earth Count</th>
<th style={{ border: '1px solid #ccc', padding: '8px' }}>Digit</th>
</tr>
</thead>
<tbody>
{[0, 1, 2].map(place => {
const placeState = state[place];
const digit = (placeState.heavenActive ? 5 : 0) + placeState.earthActive;
const placeName = ['Ones', 'Tens', 'Hundreds'][place];
return (
<tr key={place}>
<td style={{ border: '1px solid #ccc', padding: '8px' }}>{placeName}</td>
<td style={{ border: '1px solid #ccc', padding: '8px', textAlign: 'center' }}>
{placeState.heavenActive ? '✓' : '✗'}
</td>
<td style={{ border: '1px solid #ccc', padding: '8px', textAlign: 'center' }}>
{placeState.earthActive}
</td>
<td style={{ border: '1px solid #ccc', padding: '8px', textAlign: 'center' }}>
{digit}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
export const UseAbacusStateHook: Story = {
render: () => <AbacusStateDemo />,
};
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
function UtilityFunctionsDemo() {
const [inputValue, setInputValue] = useState(123);
const state = numberToAbacusState(inputValue, 5);
const backToNumber = abacusStateToNumber(state);
const fromValue = 42;
const toValue = 57;
const diff = calculateBeadDiffFromValues(fromValue, toValue);
const validation1 = validateAbacusValue(inputValue, 5);
const validation2 = validateAbacusValue(123456, 5);
const state1 = numberToAbacusState(100);
const state2 = numberToAbacusState(100);
const state3 = numberToAbacusState(200);
const areEqual1 = areStatesEqual(state1, state2);
const areEqual2 = areStatesEqual(state1, state3);
return (
<div style={{ padding: '20px' }}>
<h3>Utility Functions</h3>
<p>Low-level functions for working with abacus states</p>
<div style={{ marginBottom: '30px' }}>
<h4>numberToAbacusState & abacusStateToNumber:</h4>
<label>
Input value:
<input
type="number"
value={inputValue}
onChange={(e) => setInputValue(parseInt(e.target.value) || 0)}
style={{ marginLeft: '10px', width: '100px' }}
/>
</label>
<pre style={{ background: '#f5f5f5', padding: '10px', marginTop: '10px', fontSize: '12px' }}>
{`numberToAbacusState(${inputValue}, 5) = ${JSON.stringify(state, null, 2)}`}
</pre>
<pre style={{ background: '#f5f5f5', padding: '10px', fontSize: '12px' }}>
{`abacusStateToNumber(state) = ${backToNumber}`}
</pre>
</div>
<div style={{ marginBottom: '30px' }}>
<h4>calculateBeadDiffFromValues:</h4>
<p>From {fromValue} to {toValue}:</p>
<pre style={{ background: '#f5f5f5', padding: '10px', fontSize: '12px' }}>
{`Summary: ${diff.summary}\nChanges: ${diff.changes.length}`}
</pre>
</div>
<div style={{ marginBottom: '30px' }}>
<h4>validateAbacusValue:</h4>
<pre style={{ background: '#f5f5f5', padding: '10px', fontSize: '12px' }}>
{`validateAbacusValue(${inputValue}, 5):\n isValid: ${validation1.isValid}\n error: ${validation1.error || 'none'}`}
</pre>
<pre style={{ background: '#f5f5f5', padding: '10px', fontSize: '12px' }}>
{`validateAbacusValue(123456, 5):\n isValid: ${validation2.isValid}\n error: ${validation2.error || 'none'}`}
</pre>
</div>
<div>
<h4>areStatesEqual:</h4>
<pre style={{ background: '#f5f5f5', padding: '10px', fontSize: '12px' }}>
{`areStatesEqual(state(100), state(100)) = ${areEqual1}\nareStatesEqual(state(100), state(200)) = ${areEqual2}`}
</pre>
</div>
</div>
);
}
export const UtilityFunctions: Story = {
render: () => <UtilityFunctionsDemo />,
};
// ============================================================================
// COMBINED FEATURES
// ============================================================================
export const AllFeaturesShowcase: Story = {
render: () => {
const [value, setValue] = useState(42);
const targetValue = 75;
const diff = useAbacusDiff(value, targetValue);
return (
<div style={{ padding: '20px', maxWidth: '800px' }}>
<h2>All New Features Combined</h2>
<p>Theme preset + column highlighting + labels + diff hook</p>
<div style={{
background: '#1a1a1a',
padding: '30px',
borderRadius: '8px',
marginTop: '20px'
}}>
<h3 style={{ color: 'white', marginTop: 0 }}>
Tutorial: Add to reach {targetValue}
</h3>
<p style={{ color: '#ccc' }}>
<strong>Current:</strong> {value} <strong>Target:</strong> {targetValue}
</p>
<p style={{ color: '#fbbf24' }}>
<strong>Instructions:</strong> {diff.summary}
</p>
<AbacusReact
value={value}
columns={2}
customStyles={ABACUS_THEMES.dark}
highlightColumns={[0, 1]}
columnLabels={['ones', 'tens']}
stepBeadHighlights={diff.highlights}
showNumbers={true}
interactive={true}
onValueChange={setValue}
/>
<div style={{ marginTop: '20px' }}>
<button onClick={() => setValue(42)}>Reset</button>
{' '}
<button onClick={() => setValue(targetValue)}>Show Answer</button>
</div>
{!diff.hasChanges && (
<p style={{ color: '#4ade80', fontWeight: 'bold', marginTop: '20px' }}>
🎉 Perfect! You reached the target!
</p>
)}
</div>
</div>
);
},
};
export const CompactThemeComparison: Story = {
render: () => {
const themes: AbacusThemeName[] = ['light', 'dark', 'trophy', 'traditional'];
return (
<div style={{ padding: '20px' }}>
<h3>Compact Mode with Different Themes</h3>
<p>Inline displays work with all theme presets</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', marginTop: '20px' }}>
{themes.map(theme => (
<div
key={theme}
style={{
background: theme === 'dark' ? '#1a1a1a' : '#f5f5f5',
padding: '15px',
borderRadius: '6px'
}}
>
<h4 style={{
margin: '0 0 15px 0',
color: theme === 'dark' ? 'white' : 'black',
textTransform: 'capitalize'
}}>
{theme}:
</h4>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
{[7, 42, 99].map(num => (
<div key={num}>
<AbacusReact
value={num}
columns={num < 10 ? 1 : 2}
compact={true}
hideInactiveBeads={true}
customStyles={ABACUS_THEMES[theme]}
scaleFactor={0.7}
/>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
},
};

View File

@@ -253,14 +253,6 @@ export interface Abacus3DMaterial {
woodGrain?: boolean; // Add wood texture to frame
}
export interface Abacus3DPhysics {
wobble?: boolean; // Beads rotate slightly during movement
clackEffect?: boolean; // Visual ripple when beads snap
hoverParallax?: boolean; // Beads lift on hover
particleSnap?: "off" | "subtle" | "sparkle"; // Particle effects on snap
hapticFeedback?: boolean; // Trigger haptic feedback on mobile
}
export interface AbacusConfig {
// Basic configuration
value?: number | bigint;
@@ -278,10 +270,13 @@ export interface AbacusConfig {
soundEnabled?: boolean;
soundVolume?: number;
// Layout & Frame
frameVisible?: boolean; // Show/hide column posts and reckoning bar
compact?: boolean; // Compact layout for inline display (implies frameVisible=false, optimized spacing)
// 3D Enhancement
enhanced3d?: boolean | "subtle" | "realistic" | "delightful";
enhanced3d?: boolean | "subtle" | "realistic";
material3d?: Abacus3DMaterial;
physics3d?: Abacus3DPhysics;
// Advanced customization
customStyles?: AbacusCustomStyles;
@@ -290,6 +285,7 @@ export interface AbacusConfig {
// Tutorial and accessibility features
highlightColumns?: number[]; // Highlight specific columns (legacy - array indices)
columnLabels?: string[]; // Optional labels for columns (indexed by column index, left to right)
highlightBeads?: BeadHighlight[]; // Support both place-value and column-index based highlighting
stepBeadHighlights?: StepBeadHighlight[]; // Progressive step-based highlighting with directions
currentStep?: number; // Current step index for progressive highlighting
@@ -1247,6 +1243,10 @@ interface BeadProps {
colorScheme?: string;
colorPalette?: string;
totalColumns?: number;
// 3D Enhancement
enhanced3d?: boolean | "subtle" | "realistic";
material3d?: Abacus3DMaterial;
columnIndex?: number;
}
const Bead: React.FC<BeadProps> = ({
@@ -1275,16 +1275,25 @@ const Bead: React.FC<BeadProps> = ({
colorScheme = "monochrome",
colorPalette = "default",
totalColumns = 1,
enhanced3d,
material3d,
columnIndex,
}) => {
// Detect server-side rendering
const isServer = typeof window === 'undefined';
// Use springs only if not on server and animations are enabled
// Even on server, we must call hooks unconditionally, so we provide static values
// Enhanced physics config for 3D modes
const physicsConfig = React.useMemo(() => {
if (!enableAnimation || isServer) return { duration: 0 };
if (!enhanced3d || enhanced3d === true || enhanced3d === 'subtle') return config.default;
return Abacus3DUtils.getPhysicsConfig(enhanced3d);
}, [enableAnimation, isServer, enhanced3d]);
const [{ x: springX, y: springY }, api] = useSpring(() => ({
x,
y,
config: enableAnimation && !isServer ? config.default : { duration: 0 }
config: physicsConfig
}));
// Arrow pulse animation for urgency indication
@@ -1363,11 +1372,11 @@ const Bead: React.FC<BeadProps> = ({
React.useEffect(() => {
if (enableAnimation) {
api.start({ x, y, config: { tension: 400, friction: 30, mass: 0.8 } });
api.start({ x, y, config: physicsConfig });
} else {
api.set({ x, y });
}
}, [x, y, enableAnimation, api]);
}, [x, y, enableAnimation, api, physicsConfig]);
// Pulse animation for direction arrows to indicate urgency
React.useEffect(() => {
@@ -1396,12 +1405,22 @@ const Bead: React.FC<BeadProps> = ({
const renderShape = () => {
const halfSize = size / 2;
// Determine fill - use gradient for realistic mode, otherwise use color
let fillValue = color;
if (enhanced3d === 'realistic' && columnIndex !== undefined) {
if (bead.type === 'heaven') {
fillValue = `url(#bead-gradient-${columnIndex}-heaven)`;
} else {
fillValue = `url(#bead-gradient-${columnIndex}-earth-${bead.position})`;
}
}
switch (shape) {
case "diamond":
return (
<polygon
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
/>
@@ -1411,7 +1430,7 @@ const Bead: React.FC<BeadProps> = ({
<rect
width={size}
height={size}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
rx="1"
@@ -1424,7 +1443,7 @@ const Bead: React.FC<BeadProps> = ({
cx={halfSize}
cy={halfSize}
r={halfSize}
fill={color}
fill={fillValue}
stroke="#000"
strokeWidth="0.5"
/>
@@ -1458,8 +1477,7 @@ const Bead: React.FC<BeadProps> = ({
? {
transform: to(
[springX, springY],
(sx, sy) =>
`translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
(sx, sy) => `translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
),
cursor: enableGestures ? "grab" : onClick ? "pointer" : "default",
touchAction: "none" as const,
@@ -1568,15 +1586,18 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
showNumbers,
soundEnabled,
soundVolume,
// Layout & Frame props
frameVisible,
compact,
// 3D enhancement props
enhanced3d,
material3d,
physics3d,
// Advanced customization props
customStyles,
callbacks,
overlays = [],
highlightColumns = [],
columnLabels = [],
highlightBeads = [],
stepBeadHighlights = [],
currentStep = 0,
@@ -1597,6 +1618,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
}
// Use props if provided, otherwise fall back to context config
// Apply compact preset logic
const effectiveFrameVisible = compact ? false : (frameVisible ?? true);
const finalConfig = {
hideInactiveBeads: hideInactiveBeads ?? contextConfig.hideInactiveBeads,
beadShape: beadShape ?? contextConfig.beadShape,
@@ -1609,6 +1633,8 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
showNumbers: showNumbers ?? contextConfig.showNumbers,
soundEnabled: soundEnabled ?? contextConfig.soundEnabled,
soundVolume: soundVolume ?? contextConfig.soundVolume,
frameVisible: effectiveFrameVisible,
compact: compact ?? false,
};
// Calculate effective columns first, without depending on columnStates
const effectiveColumns = useMemo(() => {
@@ -1992,9 +2018,15 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
// console.log(`🎯 activeColumn changed to: ${activeColumn}`);
}, [activeColumn]);
// 3D Enhancement: Calculate container classes
const containerClasses = Abacus3DUtils.get3DContainerClasses(
enhanced3d,
material3d?.lighting
);
return (
<div
className="abacus-container"
className={containerClasses}
style={{
display: "inline-block",
textAlign: "center",
@@ -2053,6 +2085,68 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
opacity: 0 !important;
}
`}</style>
{/* 3D Enhancement: Material gradients for beads */}
{enhanced3d === 'realistic' && material3d && (
<>
{/* Generate gradients for all beads based on material type */}
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
const placeValue = (effectiveColumns - 1 - colIndex) as ValidPlaceValues;
// Create dummy beads to get their colors
const heavenBead: BeadConfig = {
type: 'heaven',
value: 5,
active: true,
position: 0,
placeValue
};
const earthBead: BeadConfig = {
type: 'earth',
value: 1,
active: true,
position: 0,
placeValue
};
const heavenColor = getBeadColor(heavenBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
const earthColor = getBeadColor(earthBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
return (
<React.Fragment key={`gradients-col-${colIndex}`}>
{/* Heaven bead gradient */}
<defs dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getBeadGradient(
`bead-gradient-${colIndex}-heaven`,
heavenColor,
material3d.heavenBeads || 'satin',
true
)
}} />
{/* Earth bead gradients */}
{[0, 1, 2, 3].map(pos => (
<defs key={`earth-${pos}`} dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getBeadGradient(
`bead-gradient-${colIndex}-earth-${pos}`,
earthColor,
material3d.earthBeads || 'satin',
true
)
}} />
))}
</React.Fragment>
);
}).filter(Boolean)}
{/* Wood grain texture pattern */}
{material3d.woodGrain && (
<defs dangerouslySetInnerHTML={{
__html: Abacus3DUtils.getWoodGrainPattern('wood-grain-pattern')
}} />
)}
</>
)}
</defs>
{/* Background glow effects - rendered behind everything */}
@@ -2087,8 +2181,55 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
);
})}
{/* Column highlights - rendered behind everything for tutorial/educational purposes */}
{highlightColumns.map((colIndex) => {
if (colIndex < 0 || colIndex >= effectiveColumns) return null;
const x = colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
const highlightWidth = dimensions.rodSpacing * 0.9; // Slightly narrower than full column
const highlightHeight = dimensions.height;
return (
<rect
key={`column-highlight-${colIndex}`}
x={x - highlightWidth / 2}
y={0}
width={highlightWidth}
height={highlightHeight}
fill="rgba(59, 130, 246, 0.15)" // Light blue highlight
stroke="rgba(59, 130, 246, 0.4)" // Slightly darker blue border
strokeWidth={2}
rx={6}
style={{ pointerEvents: "none" }}
/>
);
})}
{/* Column labels - rendered above columns for tutorial/educational purposes */}
{columnLabels.map((label, colIndex) => {
if (!label || colIndex >= effectiveColumns) return null;
const x = colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
const labelY = -20; // Position above the abacus
return (
<text
key={`column-label-${colIndex}`}
x={x}
y={labelY}
textAnchor="middle"
fontSize="14"
fontWeight="600"
fill="rgba(0, 0, 0, 0.7)"
style={{ pointerEvents: "none", userSelect: "none" }}
>
{label}
</text>
);
})}
{/* Rods - positioned as rectangles like in Typst */}
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
{finalConfig.frameVisible && Array.from({ length: effectiveColumns }, (_, colIndex) => {
const placeValue = effectiveColumns - 1 - colIndex;
const x =
colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
@@ -2120,33 +2261,66 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
};
return (
<rect
key={`rod-pv${placeValue}`}
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill={rodStyle.fill}
stroke={rodStyle.stroke}
strokeWidth={rodStyle.strokeWidth}
opacity={rodStyle.opacity}
/>
<React.Fragment key={`rod-pv${placeValue}`}>
<rect
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill={rodStyle.fill}
stroke={rodStyle.stroke}
strokeWidth={rodStyle.strokeWidth}
opacity={rodStyle.opacity}
className="column-post"
/>
{/* Wood grain texture overlay for column posts */}
{enhanced3d === 'realistic' && material3d?.woodGrain && (
<rect
x={x - dimensions.rodWidth / 2}
y={rodStartY}
width={dimensions.rodWidth}
height={rodEndY - rodStartY}
fill="url(#wood-grain-pattern)"
className="frame-wood"
style={{ pointerEvents: 'none' }}
/>
)}
</React.Fragment>
);
})}
{/* Reckoning bar - spans from leftmost to rightmost bead */}
<rect
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
y={barY}
width={
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
}
height={dimensions.barThickness}
fill={customStyles?.reckoningBar?.fill || "black"} // Typst default is black
stroke={customStyles?.reckoningBar?.stroke || "none"}
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
opacity={customStyles?.reckoningBar?.opacity ?? 1}
/>
{finalConfig.frameVisible && (
<>
<rect
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
y={barY}
width={
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
}
height={dimensions.barThickness}
fill={customStyles?.reckoningBar?.fill || "black"} // Typst default is black
stroke={customStyles?.reckoningBar?.stroke || "none"}
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
opacity={customStyles?.reckoningBar?.opacity ?? 1}
className="reckoning-bar"
/>
{/* Wood grain texture overlay for reckoning bar */}
{enhanced3d === 'realistic' && material3d?.woodGrain && (
<rect
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
y={barY}
width={
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
}
height={dimensions.barThickness}
fill="url(#wood-grain-pattern)"
className="frame-wood"
style={{ pointerEvents: 'none' }}
/>
)}
</>
)}
{/* Beads */}
{beadStates.map((columnBeads, colIndex) =>
@@ -2329,6 +2503,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
colorScheme={finalConfig.colorScheme}
colorPalette={finalConfig.colorPalette}
totalColumns={effectiveColumns}
enhanced3d={enhanced3d}
material3d={material3d}
columnIndex={colIndex}
/>
);
}),

View File

@@ -0,0 +1,264 @@
import type { Meta, StoryObj } from '@storybook/react'
import { AbacusStatic } from './AbacusStatic'
import { ABACUS_THEMES } from './AbacusThemes'
/**
* AbacusStatic - Server Component compatible static abacus
*
* ## Key Features:
* - ✅ Works in React Server Components (no "use client")
* - ✅ Shares core utilities with AbacusReact (numberToAbacusState, color logic)
* - ✅ No animations, hooks, or client-side JavaScript
* - ✅ Lightweight rendering for static displays
*
* ## Shared Code (No Duplication!):
* - Uses `numberToAbacusState()` from AbacusUtils
* - Uses same color scheme logic as AbacusReact
* - Uses same bead positioning concepts
* - Accepts same `customStyles` prop structure
*
* ## When to Use:
* - React Server Components (Next.js App Router)
* - Static site generation
* - Non-interactive previews
* - Server-side rendering without hydration
*/
const meta = {
title: 'AbacusStatic/Server Component Ready',
component: AbacusStatic,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof AbacusStatic>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
value: 123,
columns: 'auto',
},
}
export const DifferentValues: Story = {
render: () => (
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap' }}>
{[1, 5, 10, 25, 50, 100, 456, 789].map((value) => (
<div key={value} style={{ textAlign: 'center' }}>
<AbacusStatic value={value} columns="auto" />
<p style={{ marginTop: '10px', color: '#64748b' }}>{value}</p>
</div>
))}
</div>
),
}
export const ColorSchemes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={456} colorScheme="place-value" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Place Value</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={456} colorScheme="monochrome" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Monochrome</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={456} colorScheme="heaven-earth" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Heaven-Earth</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={456} colorScheme="alternating" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Alternating</p>
</div>
</div>
),
}
export const BeadShapes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '40px' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={42} beadShape="circle" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Circle</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={42} beadShape="diamond" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Diamond</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={42} beadShape="square" />
<p style={{ marginTop: '10px', color: '#64748b' }}>Square</p>
</div>
</div>
),
}
export const CompactMode: Story = {
render: () => (
<div style={{ fontSize: '24px', display: 'flex', alignItems: 'center', gap: '15px' }}>
<span>The equation:</span>
<AbacusStatic value={5} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
<span>+</span>
<AbacusStatic value={3} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
<span>=</span>
<AbacusStatic value={8} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
</div>
),
}
export const HideInactiveBeads: Story = {
render: () => (
<div style={{ display: 'flex', gap: '40px' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={25} hideInactiveBeads={false} />
<p style={{ marginTop: '10px', color: '#64748b' }}>Show All</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={25} hideInactiveBeads />
<p style={{ marginTop: '10px', color: '#64748b' }}>Hide Inactive</p>
</div>
</div>
),
}
export const WithThemes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={123} customStyles={ABACUS_THEMES.light} />
<p style={{ marginTop: '10px', color: '#64748b' }}>Light</p>
</div>
<div style={{ textAlign: 'center', padding: '20px', background: '#1e293b', borderRadius: '8px' }}>
<AbacusStatic value={123} customStyles={ABACUS_THEMES.dark} />
<p style={{ marginTop: '10px', color: '#cbd5e1' }}>Dark</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={123} customStyles={ABACUS_THEMES.trophy} />
<p style={{ marginTop: '10px', color: '#64748b' }}>Trophy</p>
</div>
</div>
),
}
export const ColumnHighlightingAndLabels: Story = {
render: () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic
value={456}
highlightColumns={[1]}
columnLabels={['ones', 'tens', 'hundreds']}
/>
<p style={{ marginTop: '10px', color: '#64748b' }}>Highlighting tens place</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic
value={789}
highlightColumns={[0, 2]}
columnLabels={['ones', 'tens', 'hundreds']}
/>
<p style={{ marginTop: '10px', color: '#64748b' }}>Multiple highlights</p>
</div>
</div>
),
}
export const Scaling: Story = {
render: () => (
<div style={{ display: 'flex', gap: '40px', alignItems: 'flex-end' }}>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={9} scaleFactor={0.5} />
<p style={{ marginTop: '10px', color: '#64748b' }}>0.5x</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={9} scaleFactor={1} />
<p style={{ marginTop: '10px', color: '#64748b' }}>1x</p>
</div>
<div style={{ textAlign: 'center' }}>
<AbacusStatic value={9} scaleFactor={1.5} />
<p style={{ marginTop: '10px', color: '#64748b' }}>1.5x</p>
</div>
</div>
),
}
export const ServerComponentExample: Story = {
render: () => (
<div style={{ maxWidth: '700px', padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
<h3 style={{ marginTop: 0 }}>React Server Component Usage</h3>
<pre
style={{
background: '#1e293b',
color: '#e2e8f0',
padding: '15px',
borderRadius: '6px',
overflow: 'auto',
fontSize: '13px',
}}
>
{`// app/flashcards/page.tsx (Server Component)
import { AbacusStatic } from '@soroban/abacus-react'
export default function FlashcardsPage() {
const numbers = [1, 5, 10, 25, 50, 100]
return (
<div className="grid grid-cols-3 gap-4">
{numbers.map(num => (
<div key={num} className="card">
<AbacusStatic
value={num}
columns="auto"
hideInactiveBeads
compact
/>
<p>{num}</p>
</div>
))}
</div>
)
}
// ✅ No "use client" needed!
// ✅ Rendered on server
// ✅ Zero client JavaScript`}
</pre>
</div>
),
}
export const PreviewCards: Story = {
render: () => (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: '20px',
maxWidth: '900px',
}}
>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50].map((value) => (
<div
key={value}
style={{
padding: '15px',
background: 'white',
border: '2px solid #e2e8f0',
borderRadius: '12px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '10px',
}}
>
<AbacusStatic value={value} columns="auto" scaleFactor={0.8} hideInactiveBeads />
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#475569' }}>{value}</span>
</div>
))}
</div>
),
}

View File

@@ -0,0 +1,366 @@
/**
* AbacusStatic - Server Component compatible static abacus
*
* Shares core logic with AbacusReact but uses static rendering without hooks/animations.
* Reuses: numberToAbacusState, getBeadColor logic, positioning calculations
* Different: No hooks, no animations, no interactions, simplified rendering
*/
import { numberToAbacusState } from './AbacusUtils'
import { AbacusStaticBead } from './AbacusStaticBead'
import type {
AbacusCustomStyles,
BeadConfig,
ValidPlaceValues
} from './AbacusReact'
export interface AbacusStaticConfig {
value: number | bigint
columns?: number | 'auto'
beadShape?: 'circle' | 'diamond' | 'square'
colorScheme?: 'monochrome' | 'place-value' | 'alternating' | 'heaven-earth'
colorPalette?: 'default' | 'pastel' | 'vibrant' | 'earth-tones'
showNumbers?: boolean | 'always' | 'never'
hideInactiveBeads?: boolean
scaleFactor?: number
frameVisible?: boolean
compact?: boolean
customStyles?: AbacusCustomStyles
highlightColumns?: number[]
columnLabels?: string[]
}
// Shared color logic from AbacusReact (simplified for static use)
function getBeadColor(
bead: BeadConfig,
totalColumns: number,
colorScheme: string,
colorPalette: string
): string {
const placeValue = bead.placeValue
// Place-value coloring
if (colorScheme === 'place-value') {
const colors: Record<string, string[]> = {
default: [
'#ef4444', // red - ones
'#f59e0b', // amber - tens
'#10b981', // emerald - hundreds
'#3b82f6', // blue - thousands
'#8b5cf6', // purple - ten thousands
'#ec4899', // pink - hundred thousands
'#14b8a6', // teal - millions
'#f97316', // orange - ten millions
'#6366f1', // indigo - hundred millions
'#84cc16', // lime - billions
],
pastel: [
'#fca5a5', '#fcd34d', '#6ee7b7', '#93c5fd', '#c4b5fd',
'#f9a8d4', '#5eead4', '#fdba74', '#a5b4fc', '#bef264',
],
vibrant: [
'#dc2626', '#d97706', '#059669', '#2563eb', '#7c3aed',
'#db2777', '#0d9488', '#ea580c', '#4f46e5', '#65a30d',
],
'earth-tones': [
'#92400e', '#78350f', '#365314', '#1e3a8a', '#4c1d95',
'#831843', '#134e4a', '#7c2d12', '#312e81', '#3f6212',
],
}
const palette = colors[colorPalette] || colors.default
return palette[placeValue % palette.length]
}
// Heaven-earth coloring
if (colorScheme === 'heaven-earth') {
return bead.type === 'heaven' ? '#3b82f6' : '#10b981'
}
// Alternating coloring
if (colorScheme === 'alternating') {
const columnIndex = totalColumns - 1 - placeValue
return columnIndex % 2 === 0 ? '#3b82f6' : '#10b981'
}
// Monochrome (default)
return '#3b82f6'
}
// Calculate bead positions (simplified from AbacusReact)
function calculateBeadPosition(
bead: BeadConfig,
dimensions: { beadSize: number; rodSpacing: number; heavenY: number; earthY: number; barY: number; totalColumns: number }
): { x: number; y: number } {
const { beadSize, rodSpacing, heavenY, earthY, barY, totalColumns } = dimensions
// X position based on place value (rightmost = ones place)
const columnIndex = totalColumns - 1 - bead.placeValue
const x = columnIndex * rodSpacing + rodSpacing / 2
// Y position based on bead type and active state
if (bead.type === 'heaven') {
// Heaven bead: if active, near bar; if inactive, at top
const y = bead.active ? barY - beadSize - 5 : heavenY
return { x, y }
} else {
// Earth bead: if active, stack up from bar; if inactive, at bottom
const earthSpacing = beadSize + 4
if (bead.active) {
// Active earth beads stack upward from the bar
const y = barY + beadSize / 2 + 10 + bead.position * earthSpacing
return { x, y }
} else {
// Inactive earth beads rest at the bottom
const y = earthY + (bead.position - 2) * earthSpacing
return { x, y }
}
}
}
/**
* AbacusStatic - Pure static abacus component (Server Component compatible)
*/
export function AbacusStatic({
value,
columns = 'auto',
beadShape = 'circle',
colorScheme = 'place-value',
colorPalette = 'default',
showNumbers = true,
hideInactiveBeads = false,
scaleFactor = 1,
frameVisible = true,
compact = false,
customStyles,
highlightColumns = [],
columnLabels = [],
}: AbacusStaticConfig) {
// Calculate columns
const valueStr = value.toString().replace('-', '')
const minColumns = Math.max(1, valueStr.length)
const effectiveColumns = columns === 'auto' ? minColumns : Math.max(columns, minColumns)
// Use shared utility to convert value to bead states
const state = numberToAbacusState(value, effectiveColumns)
// Generate bead configs (matching AbacusReact's structure)
const beadConfigs: BeadConfig[][] = []
for (let colIndex = 0; colIndex < effectiveColumns; colIndex++) {
const placeValue = (effectiveColumns - 1 - colIndex) as ValidPlaceValues
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
const beads: BeadConfig[] = []
// Heaven bead
beads.push({
type: 'heaven',
value: 5,
active: columnState.heavenActive,
position: 0,
placeValue,
})
// Earth beads
for (let i = 0; i < 4; i++) {
beads.push({
type: 'earth',
value: 1,
active: i < columnState.earthActive,
position: i,
placeValue,
})
}
beadConfigs.push(beads)
}
// Calculate dimensions (matching AbacusReact)
const beadSize = 20
const rodSpacing = 40
const heavenHeight = 60
const earthHeight = 120
const barHeight = 10
const padding = 20
const numberHeightCalc = showNumbers ? 30 : 0
const labelHeight = columnLabels.length > 0 ? 30 : 0
const width = effectiveColumns * rodSpacing + padding * 2
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
const dimensions = {
width,
height,
beadSize,
rodSpacing,
heavenY: padding + labelHeight + heavenHeight / 3,
earthY: padding + labelHeight + heavenHeight + barHeight + earthHeight * 0.7,
barY: padding + labelHeight + heavenHeight,
padding,
totalColumns: effectiveColumns,
}
// Compact mode hides frame
const effectiveFrameVisible = compact ? false : frameVisible
return (
<svg
width={width * scaleFactor}
height={height * scaleFactor}
viewBox={`0 0 ${width} ${height}`}
className={`abacus-svg ${hideInactiveBeads ? 'hide-inactive-mode' : ''}`}
style={{ overflow: 'visible', display: 'block' }}
>
<defs>
<style>{`
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0 !important;
}
`}</style>
</defs>
{/* Column highlights */}
{highlightColumns.map((colIndex) => {
if (colIndex < 0 || colIndex >= effectiveColumns) return null
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
const highlightWidth = rodSpacing * 0.9
const highlightHeight = height - padding * 2 - numberHeightCalc - labelHeight
return (
<rect
key={`column-highlight-${colIndex}`}
x={x - highlightWidth / 2}
y={padding + labelHeight}
width={highlightWidth}
height={highlightHeight}
fill="rgba(59, 130, 246, 0.15)"
stroke="rgba(59, 130, 246, 0.4)"
strokeWidth={2}
rx={6}
style={{ pointerEvents: 'none' }}
/>
)
})}
{/* Column labels */}
{columnLabels.map((label, colIndex) => {
if (!label || colIndex >= effectiveColumns) return null
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
return (
<text
key={`column-label-${colIndex}`}
x={x}
y={padding + 15}
textAnchor="middle"
fontSize="14"
fontWeight="600"
fill="rgba(0, 0, 0, 0.7)"
style={{ pointerEvents: 'none', userSelect: 'none' }}
>
{label}
</text>
)
})}
{/* Rods (column posts) */}
{effectiveFrameVisible && beadConfigs.map((_, colIndex) => {
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
return (
<rect
key={`rod-${colIndex}`}
x={x - 3}
y={padding + labelHeight}
width={6}
height={heavenHeight + earthHeight + barHeight}
fill={customStyles?.columnPosts?.fill || 'rgb(0, 0, 0, 0.1)'}
stroke={customStyles?.columnPosts?.stroke || 'rgba(0, 0, 0, 0.2)'}
strokeWidth={customStyles?.columnPosts?.strokeWidth || 1}
opacity={customStyles?.columnPosts?.opacity ?? 1}
/>
)
})}
{/* Reckoning bar */}
{effectiveFrameVisible && (
<rect
x={padding}
y={dimensions.barY}
width={effectiveColumns * rodSpacing}
height={barHeight}
fill={customStyles?.reckoningBar?.fill || 'rgb(0, 0, 0, 0.15)'}
stroke={customStyles?.reckoningBar?.stroke || 'rgba(0, 0, 0, 0.3)'}
strokeWidth={customStyles?.reckoningBar?.strokeWidth || 2}
opacity={customStyles?.reckoningBar?.opacity ?? 1}
/>
)}
{/* Beads */}
{beadConfigs.map((columnBeads, colIndex) => {
const placeValue = effectiveColumns - 1 - colIndex
return (
<g key={`column-${colIndex}`}>
{columnBeads.map((bead, beadIndex) => {
const position = calculateBeadPosition(bead, dimensions)
// Adjust X for padding
position.x += padding
const color = getBeadColor(bead, effectiveColumns, colorScheme, colorPalette)
return (
<AbacusStaticBead
key={`bead-${colIndex}-${beadIndex}`}
bead={bead}
x={position.x}
y={position.y}
size={beadSize}
shape={beadShape}
color={color}
hideInactiveBeads={hideInactiveBeads}
customStyle={
bead.type === 'heaven'
? customStyles?.heavenBeads
: customStyles?.earthBeads
}
/>
)
})}
</g>
)
})}
{/* Column numbers */}
{showNumbers && beadConfigs.map((_, colIndex) => {
const placeValue = effectiveColumns - 1 - colIndex
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
const digit = (columnState.heavenActive ? 5 : 0) + columnState.earthActive
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
return (
<text
key={`number-${colIndex}`}
x={x}
y={height - padding + 5}
textAnchor="middle"
fontSize={customStyles?.numerals?.fontSize || 16}
fontWeight={customStyles?.numerals?.fontWeight || '600'}
fill={customStyles?.numerals?.color || 'rgba(0, 0, 0, 0.8)'}
style={{ pointerEvents: 'none', userSelect: 'none' }}
>
{digit}
</text>
)
})}
</svg>
)
}
export default AbacusStatic

View File

@@ -0,0 +1,100 @@
/**
* StaticBead - Pure SVG bead with no animations or interactions
* Used by AbacusStatic for server-side rendering
*/
import type { BeadConfig, BeadStyle } from './AbacusReact'
export interface StaticBeadProps {
bead: BeadConfig
x: number
y: number
size: number
shape: 'diamond' | 'square' | 'circle'
color: string
customStyle?: BeadStyle
hideInactiveBeads?: boolean
}
export function AbacusStaticBead({
bead,
x,
y,
size,
shape,
color,
customStyle,
hideInactiveBeads = false,
}: StaticBeadProps) {
// Don't render inactive beads if hideInactiveBeads is true
if (!bead.active && hideInactiveBeads) {
return null
}
const halfSize = size / 2
const opacity = bead.active ? (customStyle?.opacity ?? 1) : 0.3
const fill = customStyle?.fill || color
const stroke = customStyle?.stroke || '#000'
const strokeWidth = customStyle?.strokeWidth || 0.5
// Calculate offset based on shape (matching AbacusReact positioning)
const getXOffset = () => {
return shape === 'diamond' ? size * 0.7 : halfSize
}
const getYOffset = () => {
return halfSize
}
const transform = `translate(${x - getXOffset()}, ${y - getYOffset()})`
const renderShape = () => {
switch (shape) {
case 'diamond':
return (
<polygon
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
opacity={opacity}
/>
)
case 'square':
return (
<rect
width={size}
height={size}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
rx="1"
opacity={opacity}
/>
)
case 'circle':
default:
return (
<circle
cx={halfSize}
cy={halfSize}
r={halfSize}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
opacity={opacity}
/>
)
}
}
return (
<g
className={`abacus-bead ${bead.active ? 'active' : 'inactive'} ${hideInactiveBeads && !bead.active ? 'hidden-inactive' : ''}`}
transform={transform}
style={{ transition: 'opacity 0.2s ease-in-out' }}
>
{renderShape()}
</g>
)
}

View File

@@ -0,0 +1,115 @@
/**
* Pre-defined theme presets for AbacusReact component
* These eliminate the need for manual style object creation
*/
import type { AbacusCustomStyles } from './AbacusReact'
export const ABACUS_THEMES = {
/**
* Light theme - solid white frame with subtle gray accents
* Best for: Clean, minimalist designs on light backgrounds
*/
light: {
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
} as AbacusCustomStyles,
/**
* Dark theme - translucent white with subtle glow
* Best for: Dark backgrounds, hero sections, dramatic presentations
*/
dark: {
columnPosts: {
fill: 'rgba(255, 255, 255, 0.3)',
stroke: 'rgba(255, 255, 255, 0.2)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgba(255, 255, 255, 0.4)',
stroke: 'rgba(255, 255, 255, 0.25)',
strokeWidth: 3,
},
} as AbacusCustomStyles,
/**
* Trophy/Premium theme - golden frame with warm tones
* Best for: Achievements, rewards, celebration contexts
*/
trophy: {
columnPosts: {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 3,
},
reckoningBar: {
fill: '#fbbf24',
stroke: '#f59e0b',
strokeWidth: 4,
},
} as AbacusCustomStyles,
/**
* Translucent theme - subtle, nearly invisible frame
* Best for: Inline displays, minimal UI, focus on beads
*/
translucent: {
columnPosts: {
fill: 'rgba(0, 0, 0, 0.05)',
stroke: 'rgba(0, 0, 0, 0.1)',
strokeWidth: 1,
},
reckoningBar: {
fill: 'rgba(0, 0, 0, 0.1)',
stroke: 'none',
strokeWidth: 0,
},
} as AbacusCustomStyles,
/**
* Solid/High-contrast theme - black frame for maximum visibility
* Best for: Educational contexts, high visibility requirements
*/
solid: {
columnPosts: {
fill: 'rgb(0, 0, 0)',
stroke: 'rgb(0, 0, 0)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(0, 0, 0)',
stroke: 'none',
strokeWidth: 0,
},
} as AbacusCustomStyles,
/**
* Traditional/Natural theme - brown wooden appearance
* Best for: Traditional soroban aesthetic, cultural contexts
*/
traditional: {
columnPosts: {
fill: '#8B5A2B',
stroke: '#654321',
strokeWidth: 2,
},
reckoningBar: {
fill: '#8B5A2B',
stroke: '#654321',
strokeWidth: 3,
},
} as AbacusCustomStyles,
} as const
/**
* Theme names type for TypeScript autocomplete
*/
export type AbacusThemeName = keyof typeof ABACUS_THEMES

View File

@@ -0,0 +1,358 @@
/**
* Utility functions for working with abacus states and calculations
* These help convert between numbers and bead positions, calculate diffs, etc.
*/
import type { ValidPlaceValues, BeadHighlight } from './AbacusReact'
/**
* Represents the state of beads in a single column
*/
export interface BeadState {
heavenActive: boolean
earthActive: number // 0-4
}
/**
* Represents the complete state of an abacus
* Key is the place value (0 = ones, 1 = tens, etc.)
*/
export interface AbacusState {
[placeValue: number]: BeadState
}
/**
* Convert a number to abacus state representation
* @param value - The number to convert
* @param maxPlaces - Maximum number of place values to include
* @returns AbacusState object representing the bead positions
*/
export function numberToAbacusState(value: number | bigint, maxPlaces: number = 5): AbacusState {
const state: AbacusState = {}
const valueNum = typeof value === 'bigint' ? Number(value) : value
for (let place = 0; place < maxPlaces; place++) {
const placeValueNum = 10 ** place
const digit = Math.floor(valueNum / placeValueNum) % 10
state[place] = {
heavenActive: digit >= 5,
earthActive: digit >= 5 ? digit - 5 : digit,
}
}
return state
}
/**
* Convert abacus state to a number
* @param state - The abacus state to convert
* @returns The numeric value represented by the abacus
*/
export function abacusStateToNumber(state: AbacusState): number {
let total = 0
for (const placeStr in state) {
const place = parseInt(placeStr, 10)
const beadState = state[place]
const digit = (beadState.heavenActive ? 5 : 0) + beadState.earthActive
total += digit * (10 ** place)
}
return total
}
/**
* Bead highlight with place value (internal type for calculations)
*/
export interface PlaceValueBasedBead {
placeValue: ValidPlaceValues
beadType: 'heaven' | 'earth'
position?: 0 | 1 | 2 | 3
}
/**
* Calculate which beads need to change between two abacus states
* @param startState - The starting abacus state
* @param targetState - The target abacus state
* @returns Object with arrays of bead additions and removals
*/
export function calculateBeadChanges(
startState: AbacusState,
targetState: AbacusState
): {
additions: PlaceValueBasedBead[]
removals: PlaceValueBasedBead[]
placeValue: number
} {
const additions: PlaceValueBasedBead[] = []
const removals: PlaceValueBasedBead[] = []
let mainPlaceValue = 0
for (const placeStr in targetState) {
const place = parseInt(placeStr, 10) as ValidPlaceValues
const start = startState[place] || { heavenActive: false, earthActive: 0 }
const target = targetState[place]
// Check heaven bead changes
if (!start.heavenActive && target.heavenActive) {
additions.push({ placeValue: place, beadType: 'heaven' })
mainPlaceValue = place
} else if (start.heavenActive && !target.heavenActive) {
removals.push({ placeValue: place, beadType: 'heaven' })
mainPlaceValue = place
}
// Check earth bead changes
if (target.earthActive > start.earthActive) {
// Adding earth beads
for (let pos = start.earthActive; pos < target.earthActive; pos++) {
additions.push({ placeValue: place, beadType: 'earth', position: pos as 0 | 1 | 2 | 3 })
mainPlaceValue = place
}
} else if (target.earthActive < start.earthActive) {
// Removing earth beads
for (let pos = start.earthActive - 1; pos >= target.earthActive; pos--) {
removals.push({ placeValue: place, beadType: 'earth', position: pos as 0 | 1 | 2 | 3 })
mainPlaceValue = place
}
}
}
return { additions, removals, placeValue: mainPlaceValue }
}
/**
* Result of a bead diff calculation
*/
export interface BeadDiffResult {
placeValue: ValidPlaceValues
beadType: 'heaven' | 'earth'
position?: number
direction: 'activate' | 'deactivate'
order: number // Order of operations for animations
}
/**
* Output of calculateBeadDiff function
*/
export interface BeadDiffOutput {
changes: BeadDiffResult[]
highlights: PlaceValueBasedBead[]
hasChanges: boolean
summary: string
}
/**
* Calculate the diff between two abacus states
* Returns exactly which beads need to move with directions and order
* @param fromState - Starting state
* @param toState - Target state
* @returns BeadDiffOutput with changes, highlights, and summary
*/
export function calculateBeadDiff(fromState: AbacusState, toState: AbacusState): BeadDiffOutput {
const { additions, removals } = calculateBeadChanges(fromState, toState)
const changes: BeadDiffResult[] = []
const highlights: PlaceValueBasedBead[] = []
let order = 0
// Process removals first (pedagogical order: clear before adding)
removals.forEach((removal) => {
changes.push({
placeValue: removal.placeValue,
beadType: removal.beadType,
position: removal.position,
direction: 'deactivate',
order: order++,
})
highlights.push({
placeValue: removal.placeValue,
beadType: removal.beadType,
position: removal.position,
})
})
// Process additions second (pedagogical order: add after clearing)
additions.forEach((addition) => {
changes.push({
placeValue: addition.placeValue,
beadType: addition.beadType,
position: addition.position,
direction: 'activate',
order: order++,
})
highlights.push({
placeValue: addition.placeValue,
beadType: addition.beadType,
position: addition.position,
})
})
// Generate summary
const summary = generateDiffSummary(changes)
return {
changes,
highlights,
hasChanges: changes.length > 0,
summary,
}
}
/**
* Calculate bead diff from numeric values
* Convenience function for when you have numbers instead of states
* @param fromValue - Starting numeric value
* @param toValue - Target numeric value
* @param maxPlaces - Maximum number of place values to consider
* @returns BeadDiffOutput
*/
export function calculateBeadDiffFromValues(
fromValue: number | bigint,
toValue: number | bigint,
maxPlaces: number = 5
): BeadDiffOutput {
const fromState = numberToAbacusState(fromValue, maxPlaces)
const toState = numberToAbacusState(toValue, maxPlaces)
return calculateBeadDiff(fromState, toState)
}
/**
* Validate that an abacus value is within the supported range
* @param value - The value to validate
* @param maxPlaces - Maximum number of place values supported
* @returns Object with isValid boolean and optional error message
*/
export function validateAbacusValue(
value: number | bigint,
maxPlaces: number = 5
): { isValid: boolean; error?: string } {
const valueNum = typeof value === 'bigint' ? Number(value) : value
if (valueNum < 0) {
return { isValid: false, error: 'Negative values are not supported' }
}
const maxValue = 10 ** maxPlaces - 1
if (valueNum > maxValue) {
return { isValid: false, error: `Value exceeds maximum for ${maxPlaces} columns (max: ${maxValue})` }
}
return { isValid: true }
}
/**
* Check if two abacus states are equal
* @param state1 - First state
* @param state2 - Second state
* @returns true if states are equal
*/
export function areStatesEqual(state1: AbacusState, state2: AbacusState): boolean {
const places1 = Object.keys(state1)
.map((k) => parseInt(k, 10))
.sort()
const places2 = Object.keys(state2)
.map((k) => parseInt(k, 10))
.sort()
if (places1.length !== places2.length) return false
for (const place of places1) {
const bead1 = state1[place]
const bead2 = state2[place]
if (!bead2) return false
if (bead1.heavenActive !== bead2.heavenActive) return false
if (bead1.earthActive !== bead2.earthActive) return false
}
return true
}
// Internal helper functions
function generateDiffSummary(changes: BeadDiffResult[]): string {
if (changes.length === 0) {
return 'No changes needed'
}
// Sort by order to respect pedagogical sequence
const sortedChanges = [...changes].sort((a, b) => a.order - b.order)
const deactivations = sortedChanges.filter((c) => c.direction === 'deactivate')
const activations = sortedChanges.filter((c) => c.direction === 'activate')
const parts: string[] = []
// Process deactivations first (pedagogical order)
if (deactivations.length > 0) {
const deactivationsByPlace = groupByPlace(deactivations)
Object.entries(deactivationsByPlace).forEach(([place, beads]) => {
const placeName = getPlaceName(parseInt(place, 10))
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
const earthBeads = beads.filter((b) => b.beadType === 'earth')
if (heavenBeads.length > 0) {
parts.push(`remove heaven bead in ${placeName}`)
}
if (earthBeads.length > 0) {
const count = earthBeads.length
parts.push(`remove ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
}
})
}
// Process activations second (pedagogical order)
if (activations.length > 0) {
const activationsByPlace = groupByPlace(activations)
Object.entries(activationsByPlace).forEach(([place, beads]) => {
const placeName = getPlaceName(parseInt(place, 10))
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
const earthBeads = beads.filter((b) => b.beadType === 'earth')
if (heavenBeads.length > 0) {
parts.push(`add heaven bead in ${placeName}`)
}
if (earthBeads.length > 0) {
const count = earthBeads.length
parts.push(`add ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
}
})
}
return parts.join(', then ')
}
function groupByPlace(changes: BeadDiffResult[]): {
[place: string]: BeadDiffResult[]
} {
return changes.reduce(
(groups, change) => {
const place = change.placeValue.toString()
if (!groups[place]) {
groups[place] = []
}
groups[place].push(change)
return groups
},
{} as { [place: string]: BeadDiffResult[] }
)
}
function getPlaceName(place: number): string {
switch (place) {
case 0:
return 'ones column'
case 1:
return 'tens column'
case 2:
return 'hundreds column'
case 3:
return 'thousands column'
default:
return `place ${place} column`
}
}

View File

@@ -1,5 +1,20 @@
export { default as AbacusReact } from "./AbacusReact";
export type { AbacusConfig, BeadConfig, AbacusDimensions } from "./AbacusReact";
export type {
AbacusConfig,
BeadConfig,
AbacusDimensions,
AbacusCustomStyles,
BeadStyle,
ColumnPostStyle,
ReckoningBarStyle,
NumeralStyle,
ValidPlaceValues,
BeadHighlight,
StepBeadHighlight,
BeadClickEvent,
AbacusCallbacks,
AbacusOverlay,
} from "./AbacusReact";
export {
useAbacusConfig,
@@ -17,3 +32,28 @@ export type {
export { StandaloneBead } from "./StandaloneBead";
export type { StandaloneBeadProps } from "./StandaloneBead";
export { AbacusStatic } from "./AbacusStatic";
export type { AbacusStaticConfig } from "./AbacusStatic";
export { ABACUS_THEMES } from "./AbacusThemes";
export type { AbacusThemeName } from "./AbacusThemes";
export {
numberToAbacusState,
abacusStateToNumber,
calculateBeadChanges,
calculateBeadDiff,
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual,
} from "./AbacusUtils";
export type {
BeadState,
AbacusState,
BeadDiffResult,
BeadDiffOutput,
PlaceValueBasedBead,
} from "./AbacusUtils";
export { useAbacusDiff, useAbacusState } from "./AbacusHooks";