Compare commits

..

116 Commits

Author SHA1 Message Date
semantic-release-bot
e852afddc5 chore(abacus-react): release v2.8.1 [skip ci]
## [2.8.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.8.0...abacus-react-v2.8.1) (2025-11-04)

### Bug Fixes

* **abacus-react:** fix animations by preventing component remounting ([be7d4c4](be7d4c4713))
* **abacus-react:** restore original AbacusReact measurements and positioning ([88c0baa](88c0baaad9))
2025-11-04 19:10:18 +00:00
Thomas Hallock
645140648a chore(abacus-react): remove debug logging and backup file 2025-11-04 13:09:01 -06:00
Thomas Hallock
be7d4c4713 fix(abacus-react): fix animations by preventing component remounting
The issue was that WrappedBeadComponent was causing all beads to remount
on every render, preventing React Spring animations from working. Even
though the wrapper was memoized with useCallback, any dependency change
caused React to see it as a completely new component type, unmounting all
old beads and mounting new ones at their new positions (instant jump instead
of animation).

Solution: Refactor to use calculateExtraBeadProps pattern instead of wrapper
- Pass AbacusAnimatedBead directly as BeadComponent (stable reference)
- Add calculateExtraBeadProps function to AbacusSVGRenderer interface
- This function computes animation props (enableAnimation, physicsConfig, etc.)
  without changing the component type
- Result: Beads update props instead of remounting, allowing animations to work

Key changes:
- AbacusSVGRenderer: Accept calculateExtraBeadProps prop
- AbacusSVGRenderer: Call calculateExtraBeadProps for each bead, spread result
- AbacusReact: Replace WrappedBeadComponent with calculateExtraBeadProps callback
- AbacusReact: Pass AbacusAnimatedBead directly (not wrapped)
- AbacusSVGRenderer: Change BeadComponent type to React.ComponentType<any>
- AbacusSVGRenderer: Use stable keys: bead-pv{placeValue}-{type}-{position}

Debugging logs added temporarily to verify fix works.
2025-11-04 13:09:01 -06:00
Thomas Hallock
88c0baaad9 fix(abacus-react): restore original AbacusReact measurements and positioning
This restores the exact dimension calculations and bead positioning
formulas from the original useAbacusDimensions hook and inline positioning
logic, ensuring correct visual layout and maintaining animations.

Changes:
- Fix barY calculation: use heavenEarthGap directly (30px), not +labelSpace
- Restore original Typst positioning formulas for all beads:
  * Heaven inactive: heavenEarthGap - inactiveGap - beadSize/2
  * Earth positioning now accounts for earthActive count correctly
- Pass empty columnLabels array to calculateStandardDimensions from
  AbacusReact since it renders labels separately at y=-20
- Add columnState parameter to calculateBeadPosition() for accurate
  inactive earth bead positioning
- Update AbacusSVGRenderer to pass column state when calculating positions

This fixes the issue where beads appeared at wrong positions after the
refactor due to incorrect dimension calculations.

Related: AbacusStatic continues to work correctly with labelSpace since
it renders labels within the SVG coordinate space.
2025-11-04 13:09:01 -06:00
semantic-release-bot
20ab40b2df chore(abacus-react): release v2.8.0 [skip ci]
# [2.8.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.7.1...abacus-react-v2.8.0) (2025-11-04)

### Bug Fixes

* **docker:** add scripts, abacus-react, and tsx for production calendar generation ([33eb90e](33eb90e316))
* **web:** generate styled-system artifacts during build ([293390a](293390ae35))
* **web:** move react-dom/server import to API route to satisfy Next.js ([00a8bc3](00a8bc3e5e))
* **web:** prevent abacus overlap in composite calendar ([448f93c](448f93c1e2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use dynamic import for react-dom/server in API route ([4f93c7d](4f93c7d996))
* **web:** use nested SVG elements to prevent coordinate space conflicts ([f9cbee8](f9cbee8fcd))

### Features

* **abacus-react:** add shared dimension calculator for consistent sizing ([e5ba772](e5ba772fde))
* **web:** add Typst-based preview endpoint with React Suspense ([599a758](599a758471))
* **web:** add year abacus to calendar header and make grid bolder ([867c7ee](867c7ee172)), closes [#333](https://github.com/antialias/soroban-abacus-flashcards/issues/333)
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](8ce8038bae))
2025-11-04 16:44:44 +00:00
Thomas Hallock
06f68cc74c refactor(web): use stdin/stdout for Typst compilation
- Pipe Typst document content via stdin instead of writing .typ files
- Read PDF/SVG output from stdout instead of temp files
- Keep SVG files in temp directory (Typst requires file paths)
- Reduces disk I/O and eliminates .typ/.pdf file writes
- Add 50MB maxBuffer for large calendar PDFs

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
599a758471 feat(web): add Typst-based preview endpoint with React Suspense
- Create /api/create/calendar/preview endpoint using Typst compilation
- Refactor CalendarPreview to use useSuspenseQuery for data fetching
- Add Suspense boundary in calendar page with loading fallback
- Preview now matches PDF exactly as both use Typst rendering
- Remove post-generation SVG replacement to preserve Typst preview

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
e5ba772fde feat(abacus-react): add shared dimension calculator for consistent sizing
- Add calculateAbacusDimensions utility to AbacusUtils
- Refactor AbacusStatic to use shared dimension calculator
- Update calendar composite generator to use shared utility
- Export calculateAbacusDimensions from index and static entry points
- Ensures consistent sizing between preview and PDF generation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
293390ae35 fix(web): generate styled-system artifacts during build
- Add @pandacss/dev to build script before Next.js build
- Ensures styled-system directory is generated in CI/CD pipeline
- Fixes "Cannot find module styled-system/css" build errors

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
f880cbe4bf refactor(web): use client-side React rendering for live calendar preview
- Render calendar using AbacusStatic components directly in browser
- Remove API call and React Query dependency for preview
- Show live preview that updates instantly when month/year changes
- Display generated PDF SVG after user clicks generate button
- Eliminates unnecessary server round-trip for interactive preview

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
14a5de0dfa refactor(web): return calendar SVG preview with PDF generation
- Change generate API to return JSON instead of binary PDF
- Include base64-encoded PDF and SVG preview in response
- Update client to decode base64 PDF and trigger download
- Store SVG preview for display after generation
- Improve error handling to surface actual error messages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
Thomas Hallock
867c7ee172 feat(web): add year abacus to calendar header and make grid bolder
- Separate month name and year display in calendar header
- Render year as abacus element (15% of page width)
- Increase grid line weight from 1px to 2px
- Change grid color from light gray (#ddd) to dark (#333)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 10:43:21 -06:00
semantic-release-bot
3a20b46185 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](8ce8038bae))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* 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))
* **docker:** add scripts, abacus-react, and tsx for production calendar generation ([33eb90e](33eb90e316))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** move react-dom/server import to API route to satisfy Next.js ([00a8bc3](00a8bc3e5e))
* **web:** prevent abacus overlap in composite calendar ([448f93c](448f93c1e2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))
* **web:** use dynamic import for react-dom/server in API route ([4f93c7d](4f93c7d996))
* **web:** use nested SVG elements to prevent coordinate space conflicts ([f9cbee8](f9cbee8fcd))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))
* **web:** use direct function imports instead of execSync for calendar generation ([9f1715f](9f1715f085))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 14:58:17 +00:00
Thomas Hallock
4f93c7d996 fix(web): use dynamic import for react-dom/server in API route
Next.js bundler flags react-dom/server imports even in API routes during static analysis.
Using dynamic import (await import()) ensures it only loads at runtime on the server.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:55:05 -06:00
semantic-release-bot
5956217979 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](8ce8038bae))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* 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))
* **docker:** add scripts, abacus-react, and tsx for production calendar generation ([33eb90e](33eb90e316))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** move react-dom/server import to API route to satisfy Next.js ([00a8bc3](00a8bc3e5e))
* **web:** prevent abacus overlap in composite calendar ([448f93c](448f93c1e2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))
* **web:** use nested SVG elements to prevent coordinate space conflicts ([f9cbee8](f9cbee8fcd))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))
* **web:** use direct function imports instead of execSync for calendar generation ([9f1715f](9f1715f085))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 14:51:17 +00:00
Thomas Hallock
00a8bc3e5e fix(web): move react-dom/server import to API route to satisfy Next.js
Changes:
- Scripts now export JSX elements (generateAbacusElement) or accept renderToString param
- API route imports react-dom/server (API routes are server-only, allowed)
- renderToStaticMarkup called in API route, not in imported scripts
- CLI interfaces use dynamic require() for react-dom/server when run directly

This satisfies Next.js requirement that react-dom/server not be imported in files that could be client-bundled.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:48:14 -06:00
semantic-release-bot
42016acec1 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](8ce8038bae))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* 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))
* **docker:** add scripts, abacus-react, and tsx for production calendar generation ([33eb90e](33eb90e316))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** prevent abacus overlap in composite calendar ([448f93c](448f93c1e2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))
* **web:** use nested SVG elements to prevent coordinate space conflicts ([f9cbee8](f9cbee8fcd))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))
* **web:** use direct function imports instead of execSync for calendar generation ([9f1715f](9f1715f085))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 14:30:40 +00:00
Thomas Hallock
9f1715f085 refactor(web): use direct function imports instead of execSync for calendar generation
Changes:
- Refactored scripts/generateCalendarAbacus.tsx to export generateAbacusSVG function
- Refactored scripts/generateCalendarComposite.tsx to export generateCalendarComposite function
- Updated API route to import and call functions directly instead of spawning subprocesses
- Removed tsx from production Dockerfile (not needed - Next.js transpiles at build time)
- Kept scripts and abacus-react package copies in Dockerfile (needed for imports)

This eliminates the need for tsx at runtime and improves performance by avoiding subprocess overhead.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:27:41 -06:00
Thomas Hallock
33eb90e316 fix(docker): add scripts, abacus-react, and tsx for production calendar generation
- Copy scripts directory to production container (needed for generateCalendarComposite.tsx)
- Copy abacus-react package (scripts import from @soroban/abacus-react/static)
- Install tsx globally to fix npm EACCES /nonexistent error in production

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:27:41 -06:00
Thomas Hallock
f9cbee8fcd fix(web): use nested SVG elements to prevent coordinate space conflicts
Fixed persistent overlap by using proper SVG nesting:

Previous approach: Extracted SVG content and used g transforms
- Problem: Inner coordinates remained in original 120x230 space
- Caused overlapping when elements had absolute positioning

New approach: Nested SVG elements with viewBox
- Each abacus rendered at natural scale=1 (120×230)
- Wrapped in <svg> with:
  - x, y: position in parent calendar
  - width, height: scaled display size
  - viewBox="0 0 120 230": maps content to display size
- Creates isolated coordinate space per abacus

This is the correct SVG pattern for embedding scaled content.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 08:27:41 -06:00
semantic-release-bot
8aaec90e11 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](8ce8038bae))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** prevent abacus overlap in composite calendar ([448f93c](448f93c1e2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 02:27:21 +00:00
Thomas Hallock
448f93c1e2 fix(web): prevent abacus overlap in composite calendar
Fixed spacing issues causing abaci to overlap:

Layout improvements:
- Calculate proper scale based on cell dimensions (CELL_WIDTH / 7, CELL_HEIGHT / 6)
- Account for natural abacus size (120×230) in scale calculation
- Use MAX_SCALE_X and MAX_SCALE_Y with 90% safety margin
- Center abacus in each cell using calculated scaled dimensions
- Add debug cell borders to visualize grid (stroke="#f0f0f0")

Math:
- ABACUS_SCALE = min(MAX_SCALE_X, MAX_SCALE_Y) × 0.9
- Position = cellCenter - (scaledSize / 2)
- This ensures abaci stay within cell boundaries

Before: Hardcoded offsets (-60, -115) caused overlaps
After: Dynamic centering based on actual scaled dimensions

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 20:24:16 -06:00
Thomas Hallock
8ce8038bae feat(web): redesign monthly calendar as single composite SVG
BREAKING FIX: Monthly calendars were overflowing to multiple pages

New "page-first" design approach:
- Generate entire calendar as one composite SVG (850x1100px)
- Includes title, year abacus, weekday headers, and all day abacuses
- Typst scales the single image to fit page (width: 100%, fit: "contain")
- Impossible to overflow - it's just one scalable image

Benefits:
 Guaranteed single-page layout across all paper sizes
 No grid overflow issues
 Consistent rendering regardless of month length
 Fast generation (~97KB SVG vs multiple small files)

Implementation:
- Created generateCalendarComposite.tsx script
- Updated route to use composite for monthly, individual SVGs for daily
- Simplified generateMonthlyTypst to just scale one image

Daily calendars unchanged (intentionally multi-page, one per day).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 20:21:40 -06:00
semantic-release-bot
c93409fc8c 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
* **web:** optimize monthly calendar for single-page layout ([b277a89](b277a89415))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 02:20:19 +00:00
Thomas Hallock
b277a89415 feat(web): optimize monthly calendar for single-page layout
Design changes for better single-page fit:
- Reduce margins: US Letter 0.5in (was 1in), A4 1.3cm (was 2.5cm)
- Compact header: inline year abacus next to title (20% width vs 35%)
- Smaller fonts: title 18pt (was 24pt), weekdays 9pt (was 12pt)
- Tighter grid: 2pt gutter (was 4pt), added row-gutter
- Reduced vertical spacing: 0.5em (was 1.5em) after header

This ensures monthly calendars fit on one page across all paper sizes:
- US Letter (8.5" × 11")
- A4 (210mm × 297mm)
- A3 (297mm × 420mm)
- Tabloid (11" × 17")

Daily calendars continue to use one page per day (intentional multi-page).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 20:17:23 -06:00
semantic-release-bot
203f110b65 chore(abacus-react): release v2.7.1 [skip ci]
## [2.7.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.7.0...abacus-react-v2.7.1) (2025-11-04)

### Bug Fixes

* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](98cd019d4a))
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))
2025-11-04 02:11:27 +00:00
Thomas Hallock
98cd019d4a fix: add xmlns to AbacusStatic for Typst SVG parsing
- Add xmlns="http://www.w3.org/2000/svg" to AbacusStatic root svg element
- Fixes "failed to parse SVG (missing root node)" Typst error
- React's renderToStaticMarkup doesn't add xmlns by default
- Required for standalone SVG files used outside HTML context
- Added debug logging to calendar generation route

Root cause: Typst's SVG parser requires explicit XML namespace declaration.
React assumes SVGs are embedded in HTML where xmlns is optional.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 20:10:22 -06:00
semantic-release-bot
858a1b4976 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-04)

### Features

* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](3b8e864cfa))
* **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:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](ce4e44d630))
* **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)
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** add test page for AbacusStatic Server Component ([3588d5a](3588d5acde))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))

### Bug Fixes

* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](0641eb719e))
* 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))
* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](08c6a419e2))

### 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))
* **web:** import utility functions from abacus-react ([7228bbc](7228bbc2eb))
* **web:** use ABACUS_THEMES instead of manual style definitions ([9f7f001](9f7f001d74))
* **web:** use compact prop for inline mini-abacus ([ff1d60a](ff1d60a233))

### Documentation

* **abacus-react:** add Storybook stories for AbacusStatic ([4f9dc46](4f9dc4666d))
* **abacus-react:** add Storybook stories for new features ([6a1cec0](6a1cec06a7))
* **abacus-react:** export AbacusStatic and update README ([74f2d97](74f2d97434))
* **abacus-react:** update documentation for new features ([35d8734](35d8734a3a))
* **abacus-react:** update README with /static import path for RSC ([72a4c2b](72a4c2b80c))
* 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-04 01:28:17 +00:00
Thomas Hallock
08c6a419e2 fix(web): use AbacusStatic for calendar SVG generation
- Switch from AbacusReact to AbacusStatic in generateCalendarAbacus.tsx
- Fixes "failed to parse SVG (missing root node)" Typst error
- AbacusReact can't be rendered server-side with renderToStaticMarkup
- AbacusStatic is designed for pure server-side rendering

Root cause: AbacusReact has "use client" directive and uses hooks,
which causes renderToStaticMarkup to generate invalid/empty SVG.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 19:25:18 -06:00
semantic-release-bot
a9664bdcb4 chore(abacus-react): release v2.7.0 [skip ci]
# [2.7.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.6.0...abacus-react-v2.7.0) (2025-11-04)

### Bug Fixes

* **web:** add dynamic export to rithmomachia page ([329e623](329e623212))
* **web:** fix Typst PDF generation path resolution ([7ce1287](7ce1287525))

### Features

* **abacus-react:** add separate /static export path for React Server Components ([ed69f6b](ed69f6b917))
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](903dea2584))
* **web:** improve calendar abacus preview styling ([8439727](8439727b15))
2025-11-04 00:52:55 +00:00
Thomas Hallock
329e623212 fix(web): add dynamic export to rithmomachia page
Attempt to fix "Cannot access 'Z' before initialization" error during
static generation by forcing dynamic rendering.

Note: This doesn't fully resolve the initialization issue, but prevents
build from attempting static generation which causes the error.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
Thomas Hallock
8439727b15 feat(web): improve calendar abacus preview styling
CalendarPreview changes:
- Remove showNumbers display from all abaci for cleaner calendar look
- Increase day abaci scaleFactor from 0.35 to 1.0 (fills grid squares)
- Remove year from month title (shows only month name)

CalendarConfigPanel changes:
- Replace link to /create with inline AbacusDisplayDropdown
- Add preview abacus showing current style (value=12, 2 columns)
- Users can now edit abacus styles directly in calendar page

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
Thomas Hallock
7ce1287525 fix(web): fix Typst PDF generation path resolution
- Change SVG image paths from absolute to relative in typstGenerator
- Execute typst compilation from tempDir with cwd option
- Fixes "file not found" error caused by Typst doubling absolute paths

Root cause: Typst treats absolute paths as relative and prepends working
directory, resulting in incorrect paths like:
/tmp/calendar-123/tmp/calendar-123/year.svg

Solution: Use relative paths ("year.svg") and run from tempDir.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
Thomas Hallock
903dea2584 feat(web): add test page for AbacusStatic RSC compatibility
- Create /test-static-abacus page demonstrating pure Server Component usage
- Uses @soroban/abacus-react/static import (no client code)
- Renders 12 abacus displays with zero client-side JavaScript
- Verifies RSC export path works correctly in Next.js build

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
Thomas Hallock
72a4c2b80c docs(abacus-react): update README with /static import path for RSC
- Document @soroban/abacus-react/static import path
- Add note about requiring /static for React Server Components
- Update code examples to use correct import path

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
Thomas Hallock
ed69f6b917 feat(abacus-react): add separate /static export path for React Server Components
- Create src/static.ts entry point with only server-compatible exports
- Configure Vite to build separate static.es.js and static.cjs.js bundles
- Add @soroban/abacus-react/static export path in package.json
- Enables RSC usage without importing client-side hooks/animations

Fixes build errors when importing AbacusStatic in Next.js App Router pages
without "use client" directive.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 18:51:46 -06:00
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
semantic-release-bot
21d2053205 chore(abacus-react): release v2.3.0 [skip ci]
# [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.2.0...abacus-react-v2.3.0) (2025-11-03)

### Bug Fixes

* adjust hero abacus position to avoid covering subtitle ([f03d341](f03d341314))
* configure favicon metadata and improve bead visibility ([e1369fa](e1369fa275))
* correct hero abacus scroll direction to flow with page content ([4232746](423274657c))
* 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))
* include column posts in favicon bounding box ([0b2f481](0b2f48106a))
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](d7b35d9544))
* **nav:** show full navigation on /games page ([d3fe6ac](d3fe6acbb0))
* reduce padding to minimize gap below last bead ([0e529be](0e529be789))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](ed9a050d64))
* separate horizontal and vertical bounding box logic ([83090df](83090df4df))
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](88993f3662))
* 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))

### Features

* add 3D printing support for abacus models ([dafdfdd](dafdfdd233))
* 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 unified trophy abacus with hero mode integration ([6620418](6620418a70))
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](4d0795a9df))
* dynamically crop favicon to active beads for maximum size ([5670322](567032296a))
* **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:** 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))
* 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)
2025-11-03 19:25:45 +00:00
Thomas Hallock
f03d341314 fix: adjust hero abacus position to avoid covering subtitle
Move hero abacus down from 50vh to 60vh to prevent it from overlapping
the subtitle text ("master the ancient art..." etc.) on the home page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:24:15 -06:00
Thomas Hallock
ed9a050d64 fix: resolve z-index layering and hero abacus visibility issues
Fix two critical issues with the trophy abacus system:

1. Hero abacus appearing on all pages:
   - Root cause: HomeHeroProvider now wraps all pages globally
   - Solution: Use pathname check to detect actual home page routes
   - Only show hero mode on: /, /en, /de, /ja, /hi, /es, /la

2. Z-index conflicts causing layering issues:
   - AppNavBar had hardcoded z-index: 1000 (DROPDOWN layer)
   - Should use Z_INDEX.NAV_BAR (100) for proper layering
   - Tooltip had z-index: 50, should use Z_INDEX.TOOLTIP (1000)

This ensures:
- Hero abacus only appears on home page, not all pages
- Trophy abacus (z-index 30001) appears above ALL content
- Nav bar and tooltips use correct z-index constants
- No stacking context conflicts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:24:15 -06:00
Thomas Hallock
423274657c fix: correct hero abacus scroll direction to flow with page content
The hero abacus was scrolling in the opposite direction of page content due to incorrect math. Fixed by subtracting scroll position instead of adding it.

Change: top: calc(50vh - ${scrollY}px) instead of calc(50vh + ${scrollY}px)

Now the abacus properly scrolls up with the page content when scrolling down.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:24:15 -06:00
Thomas Hallock
6620418a70 feat: add unified trophy abacus with hero mode integration
Implement a single abacus component that seamlessly transitions between three states: hero mode on the home page, compact button in the corner, and full-screen modal display.

Key features:
- Hero mode: Large white abacus at top-center of home page, interactive with beads
- Button mode: Golden mini abacus in bottom-right corner when hero scrolled away or on other pages
- Open mode: Full-screen golden abacus with blur backdrop, accessible from button
- Smooth fly-to-corner animation when scrolling past hero section
- Two-way sync between hero and trophy abacus values via HomeHeroContext
- Highest z-index layer (30000+) to appear above all content

Implementation:
- Create MyAbacus component handling all three states with smooth transitions
- Add MyAbacusContext for global open/close state management
- Move HomeHeroProvider to ClientProviders for global access
- Replace HeroAbacus with HeroSection placeholder on home page
- Fix flashcard z-index by creating proper stacking context (z-index: 1)
- Add MY_ABACUS z-index constants (30000-30001)
- Add calendar page components with correct styled-system imports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:24:15 -06:00
Thomas Hallock
7a4a37ec6d feat: add comprehensive Storybook coverage and migration guide
- Add StandaloneBead.stories.tsx with 11 stories covering all use cases
  (icons, decorations, progress indicators, size/color variations)
- Add AbacusDisplayProvider.stories.tsx with 9 stories demonstrating
  context features, localStorage persistence, and configuration
- Add MIGRATION_GUIDE.md for useAbacusState → useAbacusPlaceStates
  with code examples, API comparison, and BigInt documentation
- Consolidate all test files to src/__tests__/ directory for consistency
- Fix vitest configuration ESM module issue (rename to .mts)

This improves discoverability, documentation, and developer experience
for the abacus-react component library.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:24:15 -06:00
semantic-release-bot
38455e1283 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 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))
* **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))
* **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))
* **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

* **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 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))
* **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 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 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))
2025-11-03 18:20:43 +00:00
Thomas Hallock
88993f3662 fix: tolerate OpenSCAD CGAL warnings if output file is created
OpenSCAD 2021.01 (Debian stable) has CGAL geometry bugs that cause
non-zero exit status when processing complex STL operations, but it
still produces valid output files.

Instead of failing the job when OpenSCAD exits with non-zero status,
we now check if the output file was created. If it exists, we proceed
with the job. Only if the file is missing do we treat it as a failure.

This allows 3D model generation to work on production despite the
older OpenSCAD version's CGAL warnings.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 12:18:00 -06:00
semantic-release-bot
458cc2b918 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 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))
* **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))
* **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))
* **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

* **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 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))
* **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))
* 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 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))
2025-11-03 17:28:27 +00:00
Thomas Hallock
f8fe6e4a41 fix: use Debian base for deps stage to match runner for binary compatibility
CRITICAL REGRESSION: Production container crash-looping with:
"Error: libc.musl-x86_64.so.1: cannot open shared object file"

Root cause identified:
Commit dafdfdd2 (3D printing support) changed runner stage from:
  FROM node:18-alpine  →  FROM node:18-slim
to add OpenSCAD (not available in Alpine repos).

However, the deps stage was NOT updated and remained Alpine-based,
causing a binary incompatibility:
- deps stage (Alpine/musl) compiles better-sqlite3 for musl libc
- runner stage (Debian/glibc) cannot run musl-compiled binaries

Solution:
Changed deps stage from node:18-alpine to node:18-slim to match
runner, ensuring better-sqlite3 is compiled for the correct target.

Both stages must now use Debian because OpenSCAD is required for
3D printing functionality and is not available in Alpine.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 11:25:33 -06:00
semantic-release-bot
af5308bcbe 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 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))
* **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))
* **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))
* **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

* **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 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))
* **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))
* 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 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))
2025-11-03 17:11:56 +00:00
Thomas Hallock
d7b35d9544 fix: mark dynamic routes as force-dynamic to prevent static generation errors
Next.js build was failing because routes using headers() were being
statically generated during build time. Added `export const dynamic = 'force-dynamic'`
to:
- /api/player-stats (uses getViewerId which reads headers)
- /api/debug/active-players (uses getViewerId which reads headers)
- /opengraph-image (reads filesystem during render)

Build errors:
- Route /api/player-stats couldn't be rendered statically because it used `headers`
- Route /api/debug/active-players couldn't be rendered statically because it used `headers`
- Error occurred prerendering page "/opengraph-image"

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 11:09:06 -06:00
semantic-release-bot
8499d90c2e 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 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))
* **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))
* **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))
* **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

* **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 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))
* **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))
* **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))
* 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 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))
2025-11-03 17:04:43 +00:00
Thomas Hallock
f4ffc5b027 fix: use default BOSL2 branch instead of non-existent v2.0.0 tag
The BOSL2 repository has no release tags, causing Docker build to fail
when trying to clone --branch v2.0.0. Removed the branch flag to clone
the default branch instead.

Error:
fatal: Remote branch v2.0.0 not found in upstream origin

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 11:02:02 -06:00
semantic-release-bot
8acfe665c5 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 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))
* **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))
* **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))
* **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

* **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 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))
* **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))
* **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))
* 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 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))
2025-11-03 16:56:46 +00:00
Thomas Hallock
b67cf610c5 fix: various game improvements and UI enhancements
Collection of improvements across multiple games and components:

- Complement Race: Improve sound effects timing and reliability
- Card Sorting: Enhance drag-and-drop physics and visual feedback
- Memory Quiz: Fix input phase keyboard navigation
- Rithmomachia: Update playing guide modal content and layout
- PageWithNav: Add viewport context integration
- ArcadeSession: Improve session state management and cleanup
- Global CSS: Add utility classes for 3D transforms

Documentation:
- Add Google Classroom setup guide
- Update Claude Code settings

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
25880cc7e4 feat: add game preview system with mock arcade environment
Add comprehensive preview system for arcade games:

- GamePreview component: Renders any arcade game in preview mode
- MockArcadeEnvironment: Provides isolated context for previews
- MockArcadeHooks: Mock implementations of useArcadeSession, etc.
- MockGameStates: Pre-defined game states for each arcade game
- ViewportContext: Track and respond to viewport size changes

Enables rendering game components outside of arcade rooms for:
- Documentation and guides
- Marketing/showcase pages
- Testing and development
- Game selection interfaces

Mock states include setup, playing, and results phases for all
five arcade games (matching, complement-race, memory-quiz,
card-sorting, rithmomachia).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
b91b23d95f refactor: restructure /create page into hub with sub-pages
Reorganize create functionality into a central hub page:

- Move flashcard creation to /create/flashcards
- Add new creation hub at /create with options for:
  - Flashcards (existing feature)
  - Abacus 3D models (new feature)
  - Future creation tools

The hub page provides a clean navigation interface for all
content creation features.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
dafdfdd233 feat: add 3D printing support for abacus models
Add comprehensive 3D printing capabilities for generating custom
abacus models in STL format:

- OpenSCAD integration in Docker container
- API endpoint: POST /api/abacus/generate-stl
- Background job processing with status monitoring
- STL file preview with Three.js
- Interactive abacus customization page at /create/abacus
- Configurable parameters: columns, bead shapes, dimensions, colors
- Export formats: STL (for 3D printing), SCAD (for editing)

Components:
- STLPreview: Real-time 3D model viewer
- JobMonitor: Background job status tracking
- AbacusCustomizer: Interactive configuration UI

Docker: Add OpenSCAD and necessary 3D printing tools
Dependencies: Add three, @react-three/fiber, @react-three/drei

Generated models stored in public/3d-models/
Documentation: 3D_PRINTING_DOCKER.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
613301cd13 feat: add per-player stats tracking system
Implement comprehensive per-player statistics tracking across arcade games:

- Database schema: player_stats table with per-player metrics
- Migration: 0013_add_player_stats.sql for schema deployment
- Type system: Universal GameResult types supporting all game modes
  (competitive, cooperative, solo, head-to-head)
- API endpoints:
  - POST /api/player-stats/record-game - Record game results
  - GET /api/player-stats - Fetch all user's players' stats
  - GET /api/player-stats/[playerId] - Fetch specific player stats
- React hooks:
  - useRecordGameResult() - Mutation hook with cache invalidation
  - usePlayerStats() - Query hooks for fetching stats
- Game integration: Matching game now records stats on completion
- UI updates: /games page displays per-player stats in player cards

Stats tracked: games played, wins, losses, best time, accuracy,
per-game breakdowns (JSON), favorite game type, last played date.

Supports cooperative games via metadata.isTeamVictory flag where
all players share win/loss outcome.

Documentation added:
- GAME_STATS_COMPARISON.md - Cross-game analysis
- PER_PLAYER_STATS_ARCHITECTURE.md - System design
- MATCHING_GAME_STATS_INTEGRATION.md - Integration guide

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
9f51edfaa9 feat(games): add autoplay and improve carousel layout
Major improvements to the games hero carousel:
- Added smooth autoplay (4s delay, stops on interaction/hover)
- Made carousel full-width to reduce virtual scrolling artifacts
- Added horizontal padding for better buffer on edges
- Restructured layout: carousel is now full-width, rest of content
  remains constrained to max-width
- Removed containScroll setting to improve infinite loop behavior

The carousel now smoothly rotates through games automatically and
has better visual consistency at the loop edges.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
946e5d1910 feat: install embla-carousel-autoplay for games carousel
Add embla-carousel-autoplay dependency to enable smooth automatic
rotation of games in the hero carousel.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
5a8c98fc10 fix(games): prevent horizontal page scroll from carousel overflow
Added overflowX: hidden to page container to clip carousel at viewport
level. This prevents the infinite carousel from making the entire page
horizontally scrollable while still allowing game cards to visually
extend beyond their container.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
876513c9cc refactor(games): remove wheel scrolling, enable overflow visible carousel
Removed non-functional wheel scrolling code and changed games carousel
to overflow: visible for a full-width effect that extends beyond the
container. Creates a more immersive, cinematic feel where game cards
bleed into the page edges.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
f80a73b35c fix(games): smooth scroll feel for carousel wheel navigation
Changed carousel wheel scrolling from discrete next/prev jumps to smooth
scrolling that follows the wheel delta. Now it feels like actually
scrolling instead of a rocket engine!

- Uses embla's internal scroll container directly
- Scales wheel delta by 0.5 for comfortable scroll speed
- Maintains horizontal scroll and shift+scroll detection

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
a224abb6f6 feat(games): add horizontal scroll support to carousels
Both games and player carousels now support horizontal scrolling with:
- Mouse wheel horizontal scroll (trackpad swipe gestures)
- Shift + vertical scroll for horizontal navigation
- Prevents page scroll when interacting with carousel

Implemented with custom wheel event handlers that detect horizontal
scroll intent and call embla's scrollNext/scrollPrev methods.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
187271e515 fix(games): use specific transition properties for smooth carousel loop
Fixed carousel looping issue by replacing generic 'transition: all' with
specific transition properties. Only transition opacity on carousel items
and transform/box-shadow/border-color on cards for hover effects.

This prevents CSS transitions from interfering with embla's carousel
transform animations at the loop wrap point.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
24231e6b2e feat(games): add rotating games hero carousel
Added a hero section at the top of the games page featuring a rotating
carousel of all available arcade games. Each card shows:
- Game icon and name
- Difficulty and player count
- Description
- Category chips (Multiplayer, Memory, Soroban, etc.)
- Game-specific gradient background

Uses embla-carousel for smooth dragging and navigation dots with game
icons for quick access. Cards link directly to each game's arcade page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
ad5bb87325 refactor(games): remove redundant subtitle below nav
Removed the hero section subtitle since the page title is now in the
nav bar. The subtitle was awkwardly positioned where nobody would see it.
Added top padding to account for the fixed nav bar.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:54 -06:00
Thomas Hallock
d3fe6acbb0 fix(nav): show full navigation on /games page
Fixed AppNavBar auto-detecting /games route as needing minimal mode.
The /games page should use the full navigation bar (like /create and
/guide), not the minimal arcade nav with only a hamburger menu.

Changed route detection to only apply minimal nav to /arcade/* routes,
not /games route.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 10:53:53 -06:00
semantic-release-bot
36bfe9c219 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 comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* **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))
* **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))
* **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-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

* **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 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))
* **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))
* **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))
* **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))
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](440b492e85))

### 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))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* **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 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))
2025-11-03 15:01:56 +00:00
Thomas Hallock
96c760a3a5 chore: remove static icon.svg replaced by dynamic generation
The static icon.svg has been replaced by dynamic day-of-month favicon
generation via src/app/icon/route.tsx which calls scripts/generateDayIcon.tsx

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
944ad6574e feat: switch to royal color theme with transparent background
Update favicon design based on user preference:
- Ones place: Gold (#fbbf24, stroke #f59e0b)
- Tens place: Purple (#a855f7, stroke #7e22ce)
- Remove background circle for clean, transparent appearance

The royal color scheme provides better contrast and a more
sophisticated look while maintaining excellent legibility at small sizes.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
0e529be789 fix: reduce padding to minimize gap below last bead
Use asymmetric padding to crop tighter to the active bead region:
- Top: 8px (some breathing room above)
- Bottom: 2px (minimal gap below last bead)
- Sides: 5px

This eliminates the visible gap of column post structure below the
last active bead while maintaining clean spacing above.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
440b492e85 fix: use nested SVG viewBox for actual cropping, not just scaling
Previous approach used transforms to scale the entire SVG, which didn't
actually crop anything - just scaled everything uniformly.

New approach uses nested SVG with viewBox to truly crop the content:
- Outer SVG: 100x100 canvas with background circle
- Inner SVG: Uses viewBox to show only the cropped region
- viewBox dimensions vary per day based on active bead positions

Results:
- Day 1 (1 bead): viewBox height ~59px (narrow vertical crop)
- Day 15 (mixed): viewBox height ~88px (medium)
- Day 31 (many): viewBox height ~104px (tall vertical crop)
- Horizontal: Always ~110px (both columns with posts)

This actually maximizes bead size by showing only the relevant vertical region.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
83090df4df fix: separate horizontal and vertical bounding box logic
Clarify that horizontal and vertical cropping use different criteria:
- Horizontal (X): Always show full width of both columns (from structural rects)
- Vertical (Y): Crop dynamically to active bead positions + heights

This ensures consistent horizontal scale (0.8) across all days, with only
vertical positioning adjusting based on where active beads are located.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
0b2f48106a fix: include column posts in favicon bounding box
Refined the cropping algorithm to include structural elements (column posts)
in the bounding box calculation, not just active beads. This ensures the
complete abacus structure is visible.

Algorithm changes:
- X range: Full width from leftmost to rightmost post (all columns)
- Y range: From top active bead to bottom active bead (tight vertical crop)

Results:
- Consistent scale of 0.8 across all days (vs original 0.48)
- Shows complete column structure with posts
- Vertical position adjusts based on active bead positions
- Maintains visual context while maximizing bead visibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
567032296a feat: dynamically crop favicon to active beads for maximum size
Implement bounding box calculation to crop the favicon SVG to show only
the active beads, maximizing their size within the 100x100 icon canvas.

Algorithm:
1. Parse rendered SVG to find all active bead positions (via regex)
2. Calculate bounding box with 15px padding
3. Compute optimal scale to fit within 96x96 (leaving border room)
4. Apply transform: translate + scale + translate to crop and center

Results:
- Day 1-9 (few beads): scale ~1.14 (2.4x larger than before)
- Day 31 (many beads): scale ~0.74 (1.5x larger than before)
- Original fixed scale: 0.48

This uses no external dependencies - just regex parsing of the rendered
SVG to extract active bead coordinates.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
e1369fa275 fix: configure favicon metadata and improve bead visibility
Metadata fixes:
- Update icon path from /icon.svg to /icon to match route handler
- Ensure proper MIME type (image/svg+xml) is set in both metadata and response

Favicon visibility improvements:
- Increase AbacusReact scaleFactor from 1.0 to 1.8 for larger beads
- Add hideInactiveBeads prop to hide inactive beads
- Add hide-inactive-mode class to wrapper for CSS to take effect
- Adjust outer scale to 0.48 to fit larger abacus in 100x100 viewBox
- Reposition abacus with translate(28, -2) for proper centering

CSS cleanup:
- Strip !important declarations from generated SVG (production code policy)
- Apply same fix to OG image generator

User needs to restart dev server to clear in-memory cache.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
4d0795a9df feat: dynamic day-of-month favicon using subprocess pattern
- Create scripts/generateDayIcon.tsx for on-demand icon generation
- Route handler calls script via execSync (avoids Next.js react-dom/server restriction)
- Implement in-memory caching to minimize subprocess overhead
- Show current day (01-31) on 2-column abacus in US Central Time
- High-contrast design: blue/green beads, 2px strokes, gold border
- Document SSR pattern in .claude/CLAUDE.md for future reference

Fixes the Next.js limitation where route handlers cannot import react-dom/server
by using subprocess pattern - scripts CAN use react-dom/server, route handlers
call them via execSync and cache the results.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
Thomas Hallock
712ee58e59 refactor(games): move page title to nav bar
Move 'Soroban Arcade' title from page content to navigation bar
for consistency with other app pages (create, guide).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 08:59:14 -06:00
semantic-release-bot
3830e049ec 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 comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* **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))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](83d0ba26f5))
* **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-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))

### Bug Fixes

* **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))
* copy entire packages/core and packages/templates ([0ccada0](0ccada0ca7))
* 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))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](4d4d930bd3))
* **i18n:** use useMessages() for tutorial translations ([95b0105](95b0105ca3))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](633ff12750))
* Integrate threshold input into Point Victory card ([b29bbee](b29bbeefca))
* **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))
* **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))

### 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))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* **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 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))
2025-11-03 13:06:46 +00:00
Thomas Hallock
6333c60352 feat(i18n): update games page hero section copy
Simplified hero section messaging:
- Changed subtitle from 'Level up your mental math powers...' to
  'Classic strategy games and lightning-fast challenges'
- Removed outdated feature pills (xpBadge, streakBadge, features)
- More accurate description of current game offerings

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:04:09 -06:00
Thomas Hallock
82c133f742 refactor(games): implement carousel, fix victories bug, add conditional stats
This commit includes several related improvements to the games page:

1. Fix victories calculation - removed incorrect division by player count
2. Implement player profile carousel using embla-carousel-react
   - Smooth infinite loop carousel with drag support
   - Navigation dots with player emojis
   - Simple opacity-based transitions (no 3D effects)
   - Disabled text selection during drag
   - Fixed gap spacing at carousel wrap point
3. Add conditional rendering - hide stats sections when user has no gameplay data
4. Update hero section - simplified design, removed excessive animations
5. Switch to PageWithNav for consistent navigation across the app

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:04:09 -06:00
Thomas Hallock
642ae95738 feat: install embla-carousel-react for player profile carousel
Add embla-carousel-react dependency to enable smooth carousel
functionality for displaying player profiles on the games page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:04:09 -06:00
Thomas Hallock
71255d3198 Configure UTC timeZone for next-intl to prevent SSR markup mismatches
- Add timeZone: 'UTC' to server-side i18n config in src/i18n/request.ts
- Add timeZone="UTC" prop to NextIntlClientProvider in src/components/ClientProviders.tsx
- Fix TypeScript type error: handle undefined cookie value with nullish coalescing
- Eliminates IntlError: ENVIRONMENT_FALLBACK warnings in development server

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:04:09 -06:00
Thomas Hallock
f2bbd91801 Redesign OG image and favicon using AbacusReact server-side rendering
- Generate icon.svg and og-image.svg from AbacusReact component via scripts/generateAbacusIcons.tsx
- OG image: huge 4-column abacus with place-value colors, dark theme with decorative diamond shapes and math operators at 0.4/0.35 opacity for visibility
- Favicon: single-column abacus showing value 5, dark brown beads, properly centered and scaled (0.7)
- opengraph-image.tsx: read pre-generated og-image.svg instead of using react-dom/server (avoids edge runtime restriction)
- All abacus visualizations now use AbacusReact component consistently

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 07:04:09 -06:00
semantic-release-bot
775d5061e8 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 comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* **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))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](83d0ba26f5))
* **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))
* 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))

### Bug Fixes

* **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))
* copy entire packages/core and packages/templates ([0ccada0](0ccada0ca7))
* 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))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](4d4d930bd3))
* **i18n:** use useMessages() for tutorial translations ([95b0105](95b0105ca3))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](633ff12750))
* Integrate threshold input into Point Victory card ([b29bbee](b29bbeefca))
* **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))
* **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))

### 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))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* **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 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))
2025-11-03 10:32:42 +00:00
Thomas Hallock
9c20f12bac refactor: use AbacusReact for dynamic Open Graph image
Replaced manual HTML/CSS abacus representation in opengraph-image.tsx
with server-side rendered AbacusReact component, using the same SVG
extraction approach as icon.svg and og-image.svg.

Now all three image generation methods use the actual AbacusReact
component from @soroban/abacus-react instead of manual recreations.

Changes:
- Added renderToStaticMarkup and AbacusReact imports
- Added extractSvgContent() function to parse SVG from rendered markup
- Replaced 150+ lines of manual HTML/CSS with AbacusReact render
- Embedded extracted SVG in ImageResponse via dangerouslySetInnerHTML

Benefits:
- Consistent abacus rendering across all images
- Automatic updates when AbacusReact component changes
- Significantly less code to maintain

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 04:30:09 -06:00
semantic-release-bot
bb5083052f 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 comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](0922ea10b7))
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](81ead65680))
* **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))
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](83d0ba26f5))
* **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))
* 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))

### Bug Fixes

* **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))
* copy entire packages/core and packages/templates ([0ccada0](0ccada0ca7))
* 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))
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](4d4d930bd3))
* **i18n:** use useMessages() for tutorial translations ([95b0105](95b0105ca3))
* increase server update debounce to 2000ms for low bandwidth ([633ff12](633ff12750))
* Integrate threshold input into Point Victory card ([b29bbee](b29bbeefca))
* **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))
* **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))

### 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))
* reorganize Harmony and Victory guide sections ([fb629c4](fb629c44ea))
* **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))

### Documentation

* 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))
2025-11-03 10:28:22 +00:00
Thomas Hallock
b07f1c4216 fix: extract pure SVG content from AbacusReact renders
Fixed generateAbacusIcons.tsx to properly extract SVG content from
AbacusReact server-side renders, removing invalid div wrappers that
prevented the abacus from displaying in icon.svg and og-image.svg.

Changes:
- Added extractSvgContent() function to parse rendered markup
- Regenerated icon.svg with proper SVG structure
- Regenerated og-image.svg showing abacus value 123
- Removed unused React import (modern TSX doesn't need it)

The Open Graph image now correctly displays the abacus visualization
alongside the site text, fixing the issue where only text was visible.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 04:25:53 -06:00
116 changed files with 22916 additions and 2663 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -45,11 +45,16 @@ RUN cd apps/web && npx @pandacss/dev
RUN turbo build --filter=@soroban/web
# Production dependencies stage - install only runtime dependencies
FROM node:18-alpine AS deps
# IMPORTANT: Must use same base as runner stage for binary compatibility (better-sqlite3)
FROM node:18-slim AS deps
WORKDIR /app
# Install build tools temporarily for better-sqlite3 installation
RUN apk add --no-cache python3 py3-setuptools make g++
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Install pnpm
RUN npm install -g pnpm@9.15.4
@@ -64,16 +69,68 @@ COPY packages/templates/package.json ./packages/templates/
# Install ONLY production dependencies
RUN pnpm install --frozen-lockfile --prod
# Production image
FROM node:18-alpine AS runner
# Typst builder stage - download and prepare typst binary
FROM node:18-slim AS typst-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
xz-utils \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
TYPST_ARCH="x86_64-unknown-linux-musl"; \
elif [ "$ARCH" = "aarch64" ]; then \
TYPST_ARCH="aarch64-unknown-linux-musl"; \
else \
echo "Unsupported architecture: $ARCH" && exit 1; \
fi && \
TYPST_VERSION="v0.11.1" && \
wget -q "https://github.com/typst/typst/releases/download/${TYPST_VERSION}/typst-${TYPST_ARCH}.tar.xz" && \
tar -xf "typst-${TYPST_ARCH}.tar.xz" && \
mv "typst-${TYPST_ARCH}/typst" /usr/local/bin/typst && \
chmod +x /usr/local/bin/typst
# BOSL2 builder stage - clone and minimize the library
FROM node:18-slim AS bosl2-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /bosl2 && \
cd /bosl2 && \
git clone --depth 1 https://github.com/BelfrySCAD/BOSL2.git . && \
# Remove unnecessary files to minimize size
rm -rf .git .github tests tutorials examples images *.md CONTRIBUTING* LICENSE* && \
# Keep only .scad files and essential directories
find . -type f ! -name "*.scad" -delete && \
find . -type d -empty -delete
# Production image - Using Debian base for OpenSCAD availability
FROM node:18-slim AS runner
WORKDIR /app
# Install ONLY runtime dependencies (no build tools needed)
RUN apk add --no-cache python3 py3-pip typst qpdf
# Install ONLY runtime dependencies (no build tools)
# Using Debian because OpenSCAD is not available in Alpine repos
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
qpdf \
openscad \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy typst binary from typst-builder stage
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
# Copy minimized BOSL2 library from bosl2-builder stage
RUN mkdir -p /usr/share/openscad/libraries
COPY --from=bosl2-builder /bosl2 /usr/share/openscad/libraries/BOSL2
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Copy built Next.js application
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next
@@ -89,6 +146,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/dist ./apps/web/dist
# Copy database migrations
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/drizzle ./apps/web/drizzle
# Copy scripts directory (needed for calendar generation)
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/scripts ./apps/web/scripts
# Copy PRODUCTION node_modules only (no dev dependencies)
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=deps --chown=nextjs:nodejs /app/apps/web/node_modules ./apps/web/node_modules
@@ -99,6 +159,9 @@ COPY --from=builder --chown=nextjs:nodejs /app/packages/core ./packages/core
# Copy templates package (needed for Typst templates)
COPY --from=builder --chown=nextjs:nodejs /app/packages/templates ./packages/templates
# Copy abacus-react package (needed for calendar generation scripts)
COPY --from=builder --chown=nextjs:nodejs /app/packages/abacus-react ./packages/abacus-react
# Install Python dependencies for flashcard generation
RUN pip3 install --no-cache-dir --break-system-packages -r packages/core/requirements.txt
@@ -112,6 +175,9 @@ WORKDIR /app/apps/web
# Create data directory for SQLite database
RUN mkdir -p data && chown nextjs:nodejs data
# Create tmp directory for 3D job outputs
RUN mkdir -p tmp/3d-jobs && chown nextjs:nodejs tmp
USER nextjs
EXPOSE 3000
ENV PORT 3000
@@ -119,4 +185,4 @@ ENV HOSTNAME "0.0.0.0"
ENV NODE_ENV production
# Start the application
CMD ["node", "server.js"]
CMD ["node", "server.js"]

View File

@@ -0,0 +1,325 @@
# 3D Printing Docker Setup
## Summary
The 3D printable abacus customization feature is fully containerized with optimized Docker multi-stage builds.
**Key Technologies:**
- OpenSCAD 2021.01 (for rendering STL/3MF from .scad files)
- BOSL2 v2.0.0 (minimized library, .scad files only)
- Typst v0.11.1 (pre-built binary)
**Image Size:** ~257MB (optimized with multi-stage builds, saved ~38MB)
**Build Stages:** 7 total (base → builder → deps → typst-builder → bosl2-builder → runner)
## Overview
The 3D printable abacus customization feature requires OpenSCAD and the BOSL2 library to be available in the Docker container.
## Size Optimization Strategy
The Dockerfile uses **multi-stage builds** to minimize the final image size:
1. **typst-builder stage** - Downloads and extracts typst, discards wget/xz-utils
2. **bosl2-builder stage** - Clones BOSL2 and removes unnecessary files (tests, docs, examples, images)
3. **runner stage** - Only copies final binaries and minimized libraries
### Size Reductions
- **Removed from runner**: git, wget, curl, xz-utils (~40MB)
- **BOSL2 minimized**: Removed .git, tests, tutorials, examples, images, markdown files (~2-3MB savings)
- **Kept only .scad files** in BOSL2 library
## Dockerfile Changes
### Build Stages Overview
The Dockerfile now has **7 stages**:
1. **base** (Alpine) - Install build tools and dependencies
2. **builder** (Alpine) - Build Next.js application
3. **deps** (Alpine) - Install production node_modules
4. **typst-builder** (Debian) - Download and extract typst binary
5. **bosl2-builder** (Debian) - Clone and minimize BOSL2 library
6. **runner** (Debian) - Final production image
### Stage 1-3: Base, Builder, Deps (unchanged)
Uses Alpine Linux for building the application (smaller and faster builds).
### Stage 4: Typst Builder (lines 68-87)
```dockerfile
FROM node:18-slim AS typst-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
xz-utils \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN ARCH=$(uname -m) && \
... download and install typst from GitHub releases
```
**Purpose:** Download typst binary in isolation, then discard build tools (wget, xz-utils).
**Result:** Only the typst binary is copied to runner stage (line 120).
### Stage 5: BOSL2 Builder (lines 90-103)
```dockerfile
FROM node:18-slim AS bosl2-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /bosl2 && \
cd /bosl2 && \
git clone --depth 1 --branch v2.0.0 https://github.com/BelfrySCAD/BOSL2.git . && \
# Remove unnecessary files to minimize size
rm -rf .git .github tests tutorials examples images *.md CONTRIBUTING* LICENSE* && \
# Keep only .scad files and essential directories
find . -type f ! -name "*.scad" -delete && \
find . -type d -empty -delete
```
**Purpose:** Clone BOSL2 and aggressively minimize by removing:
- `.git` directory
- Tests, tutorials, examples
- Documentation (markdown files)
- Images
- All non-.scad files
**Result:** Minimized BOSL2 library (~1-2MB instead of ~5MB) copied to runner (line 124).
### Stage 6: Runner - Production Image (lines 106-177)
**Base Image:** `node:18-slim` (Debian) - Required for OpenSCAD availability
**Runtime Dependencies (lines 111-117):**
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
python3-pip \
qpdf \
openscad \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
```
**Removed from runner:**
- ❌ git (only needed in bosl2-builder)
- ❌ wget (only needed in typst-builder)
- ❌ curl (not needed at runtime)
- ❌ xz-utils (only needed in typst-builder)
**Artifacts Copied from Other Stages:**
```dockerfile
# From typst-builder (line 120)
COPY --from=typst-builder /usr/local/bin/typst /usr/local/bin/typst
# From bosl2-builder (line 124)
COPY --from=bosl2-builder /bosl2 /usr/share/openscad/libraries/BOSL2
# From builder (lines 131-159)
# Next.js app, styled-system, server files, etc.
# From deps (lines 145-146)
# Production node_modules only
```
BOSL2 v2.0.0 (minimized) is copied to `/usr/share/openscad/libraries/BOSL2/`, which is OpenSCAD's default library search path. This allows `include <BOSL2/std.scad>` to work in the abacus.scad file.
### Temp Directory for Job Outputs (line 168)
```dockerfile
RUN mkdir -p tmp/3d-jobs && chown nextjs:nodejs tmp
```
Creates the directory where JobManager stores generated 3D files.
## Files Included in Docker Image
The following files are automatically included via the `COPY` command at line 132:
```
apps/web/public/3d-models/
├── abacus.scad (parametric OpenSCAD source)
└── simplified.abacus.stl (base model, 4.8MB)
```
These files are NOT excluded by `.dockerignore`.
## Testing the Docker Build
### Local Testing
1. **Build the Docker image:**
```bash
docker build -t soroban-abacus-test .
```
2. **Run the container:**
```bash
docker run -p 3000:3000 soroban-abacus-test
```
3. **Test OpenSCAD inside the container:**
```bash
docker exec -it <container-id> sh
openscad --version
ls /usr/share/openscad/libraries/BOSL2
```
4. **Test the 3D printing endpoint:**
- Visit http://localhost:3000/3d-print
- Adjust parameters and generate a file
- Monitor job progress
- Download the result
### Verify BOSL2 Installation
Inside the running container:
```bash
# Check OpenSCAD version
openscad --version
# Verify BOSL2 library exists
ls -la /usr/share/openscad/libraries/BOSL2/
# Test rendering a simple file
cd /app/apps/web/public/3d-models
openscad -o /tmp/test.stl abacus.scad
```
## Production Deployment
### Environment Variables
No additional environment variables are required for the 3D printing feature.
### Volume Mounts (Optional)
For better performance and to avoid rebuilding the image when updating 3D models:
```bash
docker run -p 3000:3000 \
-v $(pwd)/apps/web/public/3d-models:/app/apps/web/public/3d-models:ro \
soroban-abacus-test
```
### Disk Space Considerations
- **BOSL2 library**: ~5MB (cloned during build)
- **Base STL file**: 4.8MB (in public/3d-models/)
- **Generated files**: Vary by parameters, typically 1-10MB each
- **Job cleanup**: Old jobs are automatically cleaned up after 1 hour
## Image Size
The final image is Debian-based (required for OpenSCAD), but optimized using multi-stage builds:
**Before optimization (original Debian approach):**
- Base runner: ~250MB
- With all build tools (git, wget, curl, xz-utils): ~290MB
- With BOSL2 (full): ~295MB
- **Total: ~295MB**
**After optimization (current multi-stage approach):**
- Base runner: ~250MB
- Runtime deps only (no build tools): ~250MB
- BOSL2 (minimized, .scad only): ~252MB
- 3D models (STL): ~257MB
- **Total: ~257MB**
**Savings: ~38MB (~13% reduction)**
### What Was Removed
- ❌ git (~15MB)
- ❌ wget (~2MB)
- ❌ curl (~5MB)
- ❌ xz-utils (~1MB)
- ❌ BOSL2 .git directory (~1MB)
- ❌ BOSL2 tests, examples, tutorials (~10MB)
- ❌ BOSL2 images and docs (~4MB)
**Total removed: ~38MB**
This trade-off (Debian vs Alpine) is necessary for OpenSCAD availability, but the multi-stage approach minimizes the size impact.
## Troubleshooting
### OpenSCAD Not Found
If you see "openscad: command not found" in logs:
1. Verify OpenSCAD is installed:
```bash
docker exec -it <container-id> which openscad
docker exec -it <container-id> openscad --version
```
2. Check if the Debian package install succeeded:
```bash
docker exec -it <container-id> dpkg -l | grep openscad
```
### BOSL2 Include Error
If OpenSCAD reports "Can't open library 'BOSL2/std.scad'":
1. Check BOSL2 exists:
```bash
docker exec -it <container-id> ls /usr/share/openscad/libraries/BOSL2/std.scad
```
2. Test include path:
```bash
docker exec -it <container-id> sh -c "cd /tmp && echo 'include <BOSL2/std.scad>; cube(10);' > test.scad && openscad -o test.stl test.scad"
```
### Job Fails with "Permission Denied"
Check tmp directory permissions:
```bash
docker exec -it <container-id> ls -la /app/apps/web/tmp
# Should show: drwxr-xr-x ... nextjs nodejs ... 3d-jobs
```
### Large File Generation Timeout
Jobs timeout after 60 seconds. For complex models, increase the timeout in `jobManager.ts:138`:
```typescript
timeout: 120000, // 2 minutes instead of 60 seconds
```
## Performance Notes
- **Cold start**: First generation takes ~5-10 seconds (OpenSCAD initialization)
- **Warm generations**: Subsequent generations take ~3-5 seconds
- **STL size**: Typically 5-15MB depending on scale parameters
- **3MF size**: Similar to STL (no significant compression)
- **SCAD size**: ~1KB (just text parameters)
## Monitoring
Job processing is logged to stdout:
```
Executing: openscad -o /app/apps/web/tmp/3d-jobs/abacus-abc123.stl ...
Job abc123 completed successfully
```
Check logs with:
```bash
docker logs <container-id> | grep "Job"
```

View File

@@ -194,6 +194,50 @@ When creating ANY new HTML/JSX element (div, button, section, etc.), add appropr
- ✅ Use `useAbacusConfig` for abacus configuration
- ✅ Use `useAbacusDisplay` for reading abacus state
**Server-Side Rendering (CRITICAL):**
`AbacusReact` already supports server-side rendering - it detects SSR and disables animations automatically.
**✅ CORRECT - Use in build scripts:**
```typescript
// scripts/generateAbacusIcons.tsx
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusReact } from '@soroban/abacus-react'
const svg = renderToStaticMarkup(<AbacusReact value={5} columns={2} />)
// This works! Scripts can use react-dom/server
```
**❌ WRONG - Do NOT use in Next.js route handlers:**
```typescript
// src/app/icon/route.tsx - DON'T DO THIS!
import { renderToStaticMarkup } from 'react-dom/server' // ❌ Next.js forbids this!
import { AbacusReact } from '@soroban/abacus-react'
export async function GET() {
const svg = renderToStaticMarkup(<AbacusReact ... />) // ❌ Will fail!
}
```
**✅ CORRECT - Pre-generate and read in route handlers:**
```typescript
// src/app/icon/route.tsx
import { readFileSync } from 'fs'
export async function GET() {
// Read pre-generated SVG from scripts/generateAbacusIcons.tsx
const svg = readFileSync('public/icons/day-01.svg', 'utf-8')
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } })
}
```
**Pattern to follow:**
1. Generate static SVGs using `scripts/generateAbacusIcons.tsx` (uses renderToStaticMarkup)
2. Commit generated SVGs to `public/icons/` or `public/`
3. Route handlers read and serve the pre-generated files
4. Regenerate icons when abacus styling changes
**MANDATORY: Read the Docs Before Customizing**
**ALWAYS read the full README documentation before customizing or styling AbacusReact:**

View File

@@ -0,0 +1,584 @@
# Cross-Game Stats Analysis & Universal Data Model
## Overview
This document analyzes ALL arcade games to ensure our `GameResult` type works universally.
## Games Analyzed
1.**Matching** (Memory Pairs)
2.**Complement Race** (Math race game)
3.**Memory Quiz** (Number memory game)
4.**Card Sorting** (Sort abacus cards)
5.**Rithmomachia** (Strategic board game)
6. 🔍 **YJS Demo** (Multiplayer demo - skipping for now)
---
## Per-Game Analysis
### 1. Matching (Memory Pairs)
**Game Type**: Memory/Pattern Matching
**Players**: 1-N (competitive multiplayer)
**How to Win**: Most pairs matched (multiplayer) OR complete all pairs (solo)
**Data Tracked**:
```typescript
{
scores: { [playerId]: matchCount }
moves: number
matchedPairs: number
totalPairs: number
gameTime: milliseconds
accuracy: percentage (matchedPairs / moves * 100)
grade: 'A+' | 'A' | 'B+' | ...
starRating: 1-5
}
```
**Winner Determination**:
- Solo: completed = won
- Multiplayer: highest score wins
**Fits GameResult?**
```typescript
{
gameType: 'matching',
duration: gameTime,
playerResults: [{
playerId,
won: isWinner,
score: matchCount,
accuracy: 0.0-1.0,
metrics: { moves, matchedPairs, difficulty }
}]
}
```
---
### 2. Complement Race
**Game Type**: Racing/Quiz hybrid
**Players**: 1-N (competitive race)
**How to Win**: Highest score OR reach finish line first (depending on mode)
**Data Tracked**:
```typescript
{
players: {
[playerId]: {
score: number
streak: number
bestStreak: number
correctAnswers: number
totalQuestions: number
position: 0-100% (for practice/survival)
deliveredPassengers: number (sprint mode)
}
}
gameTime: milliseconds
winner: playerId | null
leaderboard: [{ playerId, score, rank }]
}
```
**Winner Determination**:
- Practice/Survival: reach 100% position
- Sprint: highest score (delivered passengers)
**Fits GameResult?**
```typescript
{
gameType: 'complement-race',
duration: gameTime,
playerResults: [{
playerId,
won: winnerId === playerId,
score: player.score,
accuracy: player.correctAnswers / player.totalQuestions,
placement: leaderboard rank,
metrics: {
streak: player.bestStreak,
correctAnswers: player.correctAnswers,
totalQuestions: player.totalQuestions
}
}]
}
```
---
### 3. Memory Quiz
**Game Type**: Memory/Recall
**Players**: 1-N (cooperative OR competitive)
**How to Win**:
- Cooperative: team finds all numbers
- Competitive: most correct answers
**Data Tracked**:
```typescript
{
playerScores: {
[playerId]: { correct: number, incorrect: number }
}
foundNumbers: number[]
correctAnswers: number[]
selectedCount: 2 | 5 | 8 | 12 | 15
playMode: 'cooperative' | 'competitive'
gameTime: milliseconds
}
```
**Winner Determination**:
- Cooperative: ALL found = team wins
- Competitive: highest correct count wins
**Fits GameResult?****BUT needs special handling for cooperative**
```typescript
{
gameType: 'memory-quiz',
duration: gameTime,
playerResults: [{
playerId,
won: playMode === 'cooperative'
? foundAll // All players win or lose together
: hasHighestScore, // Individual winner
score: playerScores[playerId].correct,
accuracy: correct / (correct + incorrect),
metrics: {
correct: playerScores[playerId].correct,
incorrect: playerScores[playerId].incorrect,
difficulty: selectedCount
}
}],
metadata: {
playMode: 'cooperative' | 'competitive',
isTeamVictory: boolean // ← IMPORTANT for cooperative games
}
}
```
**NEW INSIGHT**: Cooperative games need special handling - all players share win/loss!
---
### 4. Card Sorting
**Game Type**: Sorting/Puzzle
**Players**: 1-N (solo, collaborative, competitive, relay)
**How to Win**:
- Solo: achieve high score (0-100)
- Collaborative: team achieves score
- Competitive: highest individual score
- Relay: TBD (not fully implemented)
**Data Tracked**:
```typescript
{
scoreBreakdown: {
finalScore: 0-100
exactMatches: number
lcsLength: number // Longest common subsequence
inversions: number // Out-of-order pairs
relativeOrderScore: 0-100
exactPositionScore: 0-100
inversionScore: 0-100
elapsedTime: seconds
}
gameMode: 'solo' | 'collaborative' | 'competitive' | 'relay'
}
```
**Winner Determination**:
- Solo/Collaborative: score > threshold (e.g., 70+)
- Competitive: highest score
**Fits GameResult?****Similar to Memory Quiz**
```typescript
{
gameType: 'card-sorting',
duration: elapsedTime * 1000,
playerResults: [{
playerId,
won: gameMode === 'collaborative'
? scoreBreakdown.finalScore >= 70 // Team threshold
: hasHighestScore,
score: scoreBreakdown.finalScore,
accuracy: scoreBreakdown.exactMatches / cardCount,
metrics: {
exactMatches: scoreBreakdown.exactMatches,
inversions: scoreBreakdown.inversions,
lcsLength: scoreBreakdown.lcsLength
}
}],
metadata: {
gameMode,
isTeamVictory: gameMode === 'collaborative'
}
}
```
---
### 5. Rithmomachia
**Game Type**: Strategic board game (2-player only)
**Players**: Exactly 2 (White vs Black)
**How to Win**: Multiple victory conditions (harmony, points, exhaustion, resignation)
**Data Tracked**:
```typescript
{
winner: 'W' | 'B' | null
winCondition: 'HARMONY' | 'EXHAUSTION' | 'RESIGNATION' | 'POINTS' | ...
capturedPieces: { W: Piece[], B: Piece[] }
pointsCaptured: { W: number, B: number }
history: MoveRecord[]
gameTime: milliseconds (computed from history)
}
```
**Winner Determination**:
- Specific win condition triggered
- No draws (or rare)
**Fits GameResult?****Needs win condition metadata**
```typescript
{
gameType: 'rithmomachia',
duration: gameTime,
playerResults: [
{
playerId: whitePlayerId,
won: winner === 'W',
score: capturedPieces.W.length, // or pointsCaptured.W
metrics: {
capturedPieces: capturedPieces.W.length,
points: pointsCaptured?.W || 0,
moves: history.filter(m => m.color === 'W').length
}
},
{
playerId: blackPlayerId,
won: winner === 'B',
score: capturedPieces.B.length,
metrics: {
capturedPieces: capturedPieces.B.length,
points: pointsCaptured?.B || 0,
moves: history.filter(m => m.color === 'B').length
}
}
],
metadata: {
winCondition: 'HARMONY' | 'POINTS' | ...
}
}
```
---
## Cross-Game Patterns Identified
### Pattern 1: Competitive (Most Common)
**Games**: Matching (multiplayer), Complement Race, Memory Quiz (competitive), Card Sorting (competitive)
**Characteristics**:
- Each player has their own score
- Winner = highest score
- Players track individually
**Stats to track per player**:
- games_played ++
- wins ++ (if winner)
- losses ++ (if not winner)
- best_time (if faster)
- highest_accuracy (if better)
---
### Pattern 2: Cooperative (Team-Based)
**Games**: Memory Quiz (cooperative), Card Sorting (collaborative)
**Characteristics**:
- All players share outcome
- Team wins or loses together
- Individual contributions still tracked
**Stats to track per player**:
- games_played ++
- wins ++ (if TEAM won) ← Key difference
- losses ++ (if TEAM lost)
- Individual metrics still tracked (correct answers, etc.)
**CRITICAL**: Check `metadata.isTeamVictory` to determine if all players get same win/loss!
---
### Pattern 3: Head-to-Head (Exactly 2 Players)
**Games**: Rithmomachia
**Characteristics**:
- Always 2 players
- One wins, one loses (rare draws)
- Different win conditions
**Stats to track per player**:
- games_played ++
- wins ++ (winner only)
- losses ++ (loser only)
- Game-specific metrics (captures, harmonies)
---
### Pattern 4: Solo Completion
**Games**: Matching (solo), Complement Race (practice), Memory Quiz (solo), Card Sorting (solo)
**Characteristics**:
- Single player
- Win = completion or threshold
- Compete against self/time
**Stats to track**:
- games_played ++
- wins ++ (if completed/threshold met)
- losses ++ (if failed/gave up)
- best_time, highest_accuracy
---
## Refined Universal Data Model
### GameResult Type (UPDATED)
```typescript
export interface GameResult {
// Game identification
gameType: string // e.g., "matching", "complement-race", etc.
// Player results (supports 1-N players)
playerResults: PlayerGameResult[]
// Timing
completedAt: number // timestamp
duration: number // milliseconds
// Optional game-specific data
metadata?: {
// For cooperative games
isTeamVictory?: boolean // ← NEW: all players share win/loss
// For specific win conditions
winCondition?: string // e.g., "HARMONY", "POINTS", "TIMEOUT"
// For game modes
gameMode?: string // e.g., "solo", "competitive", "cooperative"
// Any other game-specific info
[key: string]: unknown
}
}
export interface PlayerGameResult {
playerId: string
// Outcome
won: boolean // For cooperative: all players same value
placement?: number // 1st, 2nd, 3rd (for competitive with >2 players)
// Performance
score?: number
accuracy?: number // 0.0 - 1.0
completionTime?: number // milliseconds (player-specific time)
// Game-specific metrics (optional, stored as JSON in DB)
metrics?: {
// Matching
moves?: number
matchedPairs?: number
difficulty?: number
// Complement Race
streak?: number
correctAnswers?: number
totalQuestions?: number
// Memory Quiz
correct?: number
incorrect?: number
// Card Sorting
exactMatches?: number
inversions?: number
lcsLength?: number
// Rithmomachia
capturedPieces?: number
points?: number
// Extensible for future games
[key: string]: unknown
}
}
```
---
## Stats Recording Logic (UPDATED)
### For Each Player in GameResult
```typescript
// Fetch player stats
const stats = await getPlayerStats(playerId)
// Always increment
stats.gamesPlayed++
// Handle wins/losses based on game type
if (gameResult.metadata?.isTeamVictory !== undefined) {
// COOPERATIVE: All players share outcome
if (playerResult.won) {
stats.totalWins++
} else {
stats.totalLosses++
}
} else {
// COMPETITIVE/SOLO: Individual outcome
if (playerResult.won) {
stats.totalWins++
} else {
stats.totalLosses++
}
}
// Update performance metrics
if (playerResult.completionTime && (
!stats.bestTime || playerResult.completionTime < stats.bestTime
)) {
stats.bestTime = playerResult.completionTime
}
if (playerResult.accuracy && playerResult.accuracy > stats.highestAccuracy) {
stats.highestAccuracy = playerResult.accuracy
}
// Update per-game stats (JSON)
stats.gameStats[gameResult.gameType] = {
gamesPlayed: (stats.gameStats[gameResult.gameType]?.gamesPlayed || 0) + 1,
wins: (stats.gameStats[gameResult.gameType]?.wins || 0) + (playerResult.won ? 1 : 0),
// ... other game-specific aggregates
}
// Update favorite game type (most played)
stats.favoriteGameType = getMostPlayedGame(stats.gameStats)
// Update timestamps
stats.lastPlayedAt = gameResult.completedAt
stats.updatedAt = Date.now()
```
---
## Database Schema (CONFIRMED)
No changes needed from original design! The `metrics` JSON field handles game-specific data perfectly.
```sql
CREATE TABLE player_stats (
player_id TEXT PRIMARY KEY,
-- Aggregates
games_played INTEGER NOT NULL DEFAULT 0,
total_wins INTEGER NOT NULL DEFAULT 0,
total_losses INTEGER NOT NULL DEFAULT 0,
-- Performance
best_time INTEGER,
highest_accuracy REAL NOT NULL DEFAULT 0,
-- Per-game breakdown (JSON)
game_stats TEXT NOT NULL DEFAULT '{}',
-- Meta
favorite_game_type TEXT,
last_played_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
---
## Key Insights & Design Decisions
### 1. Cooperative Games Need Special Flag
**Problem**: Memory Quiz (cooperative) and Card Sorting (collaborative) - all players share win/loss.
**Solution**: Add `metadata.isTeamVictory: boolean` to `GameResult`. When `true`, recording logic gives ALL players the same win/loss.
### 2. Flexible Metrics Field
**Problem**: Each game tracks different metrics (moves, streak, inversions, etc.).
**Solution**: `PlayerGameResult.metrics` is an open object. Store game-specific data here, saved as JSON in DB.
### 3. Placement for Tournaments
**Problem**: 3+ player games need to track ranking (1st, 2nd, 3rd).
**Solution**: `PlayerGameResult.placement` field. Useful for leaderboards.
### 4. Win Conditions Matter
**Problem**: Rithmomachia has multiple win conditions (harmony, points, etc.).
**Solution**: `metadata.winCondition` stores how the game was won. Useful for achievements/stats breakdown.
### 5. Score is Optional
**Problem**: Not all games have scores (e.g., Rithmomachia can win by harmony without points enabled).
**Solution**: Make `score` optional. Use `won` as primary outcome indicator.
---
## Testing Matrix
### Scenarios to Test
| Game | Mode | Players | Expected Outcome |
|------|------|---------|------------------|
| Matching | Solo | 1 | Player wins if completed |
| Matching | Competitive | 2+ | Winner = highest score, others lose |
| Complement Race | Sprint | 2+ | Winner = highest score |
| Memory Quiz | Cooperative | 2+ | ALL win or ALL lose (team) |
| Memory Quiz | Competitive | 2+ | Winner = most correct |
| Card Sorting | Solo | 1 | Win if score >= 70 |
| Card Sorting | Collaborative | 2+ | ALL win or ALL lose (team) |
| Card Sorting | Competitive | 2+ | Winner = highest score |
| Rithmomachia | PvP | 2 | One wins (by condition), one loses |
---
## Conclusion
**Universal `GameResult` type CONFIRMED to work for all games**
**Key Requirements**:
1. Support 1-N players (flexible array)
2. Support cooperative games (isTeamVictory flag)
3. Support game-specific metrics (open metrics object)
4. Support multiple win conditions (winCondition metadata)
5. Track both individual AND team performance
**Next Steps**:
1. Update `.claude/PER_PLAYER_STATS_ARCHITECTURE.md` with refined types
2. Implement database schema
3. Build API endpoints
4. Create React hooks
5. Integrate with each game (starting with Matching)
---
**Status**: ✅ Complete cross-game analysis
**Result**: GameResult type is universal and robust
**Date**: 2025-01-03

View File

@@ -0,0 +1,468 @@
# Google Classroom Integration Setup Guide
**Goal:** Set up Google Classroom API integration using mostly CLI commands, minimizing web console interaction.
**Time Required:** 15-20 minutes
**Cost:** $0 (free for educational use)
---
## Prerequisites
**gcloud CLI installed** (already installed at `/opt/homebrew/bin/gcloud`)
**Valid Google account**
- **Billing account** (required by Google, but FREE for Classroom API)
---
## Quick Start (TL;DR)
```bash
# Run the automated setup script
./scripts/setup-google-classroom.sh
```
The script will:
1. Authenticate with your Google account
2. Create a GCP project
3. Enable Classroom & People APIs
4. Guide you through OAuth setup (2 web console steps)
5. Configure your `.env.local` file
**Note:** Steps 6 & 7 still require web console (Google doesn't provide CLI for OAuth consent screen), but the script opens the pages for you and provides exact instructions.
---
## What the Script Does (Step by Step)
### 1. Authentication ✅ Fully Automated
```bash
gcloud auth login
```
Opens browser, you log in with Google, done.
### 2. Create GCP Project ✅ Fully Automated
```bash
PROJECT_ID="soroban-abacus-$(date +%s)" # Unique ID with timestamp
gcloud projects create "$PROJECT_ID" --name="Soroban Abacus Flashcards"
gcloud config set project "$PROJECT_ID"
```
### 3. Link Billing Account ✅ Mostly Automated
```bash
# List your billing accounts
gcloud billing accounts list
# Link to project
gcloud billing projects link "$PROJECT_ID" --billing-account="BILLING_ACCOUNT_ID"
```
**Why billing is required:**
- Google requires billing for API access (even free APIs!)
- Classroom API is **FREE** with no usage charges
- You won't be charged unless you enable paid services
**If you don't have a billing account:**
- Script will prompt you to create one at: https://console.cloud.google.com/billing
- It's quick: just add payment method (won't be charged)
- Press Enter in terminal after creation
### 4. Enable APIs ✅ Fully Automated
```bash
gcloud services enable classroom.googleapis.com
gcloud services enable people.googleapis.com
```
Takes 1-2 minutes to propagate.
### 5. Create OAuth Credentials ⚠️ Requires Web Console
**Why CLI doesn't work:**
Google doesn't provide `gcloud` commands for creating OAuth clients. You need the web console.
**What the script does:**
- Opens: https://console.cloud.google.com/apis/credentials?project=YOUR_PROJECT
- Provides exact instructions (copy-paste ready)
**Manual steps (takes 2 minutes):**
1. Click "**Create Credentials**" → "**OAuth client ID**"
2. Application type: **"Web application"**
3. Name: **"Soroban Abacus Web"**
4. **Authorized JavaScript origins:**
```
http://localhost:3000
https://abaci.one
```
5. **Authorized redirect URIs:**
```
http://localhost:3000/api/auth/callback/google
https://abaci.one/api/auth/callback/google
```
6. Click **"Create"**
7. **Copy** the Client ID and Client Secret (you'll paste into terminal)
### 6. Configure OAuth Consent Screen ⚠️ Requires Web Console
**Why CLI doesn't work:**
OAuth consent screen configuration is web-only.
**What the script does:**
- Opens: https://console.cloud.google.com/apis/credentials/consent?project=YOUR_PROJECT
- Provides step-by-step instructions
**Manual steps (takes 3 minutes):**
**Screen 1: OAuth consent screen**
- User Type: **"External"** (unless you have Google Workspace)
- Click "**Create**"
**Screen 2: App information**
- App name: **"Soroban Abacus Flashcards"**
- User support email: **Your email**
- App logo: (optional)
- App domain: (optional, can add later)
- Developer contact: **Your email**
- Click "**Save and Continue**"
**Screen 3: Scopes**
- Click "**Add or Remove Scopes**"
- Filter/search for these scopes and check them:
- ✅ `.../auth/userinfo.email` (See your primary Google Account email)
- ✅ `.../auth/userinfo.profile` (See your personal info)
- ✅ `.../auth/classroom.courses.readonly` (View courses)
- ✅ `.../auth/classroom.rosters.readonly` (View class rosters)
- Click "**Update**"
- Click "**Save and Continue**"
**Screen 4: Test users**
- Click "**Add Users**"
- Add your email address (for testing)
- Click "**Save and Continue**"
**Screen 5: Summary**
- Review and click "**Back to Dashboard**"
Done! ✅
### 7. Save Credentials to .env.local ✅ Fully Automated
Script prompts you for:
- Client ID (paste from step 5)
- Client Secret (paste from step 5)
Then automatically adds to `.env.local`:
```bash
# Google OAuth (Generated by setup-google-classroom.sh)
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="GOCSPX-your-secret"
```
---
## After Running the Script
### Verify Setup
```bash
# Check project configuration
gcloud config get-value project
# List enabled APIs
gcloud services list --enabled
# Check Classroom API is enabled
gcloud services list --enabled | grep classroom
```
Expected output:
```
classroom.googleapis.com Google Classroom API
```
### Test API Access
```bash
# Get an access token
gcloud auth application-default login
gcloud auth application-default print-access-token
# Test Classroom API (replace TOKEN)
curl -H "Authorization: Bearer YOUR_TOKEN" \
https://classroom.googleapis.com/v1/courses
```
Expected response (if you have no courses yet):
```json
{}
```
---
## NextAuth Configuration
Now that you have credentials, add Google provider to NextAuth:
### 1. Check Current NextAuth Config
```bash
cat src/app/api/auth/[...nextauth]/route.ts
```
### 2. Add Google Provider
Add to your NextAuth providers array:
```typescript
import GoogleProvider from "next-auth/providers/google"
export const authOptions: NextAuthOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
scope: [
'openid',
'email',
'profile',
'https://www.googleapis.com/auth/classroom.courses.readonly',
'https://www.googleapis.com/auth/classroom.rosters.readonly',
].join(' '),
prompt: 'consent',
access_type: 'offline',
response_type: 'code'
}
}
}),
// ... your existing providers
],
// ... rest of config
}
```
### 3. Test Login
```bash
# Start dev server
npm run dev
# Open browser
open http://localhost:3000
```
Click "Sign in with Google" and verify:
- ✅ OAuth consent screen appears
- ✅ Shows requested permissions
- ✅ Successfully logs in
- ✅ User profile is created
---
## Troubleshooting
### "Billing account required"
**Problem:** Can't enable APIs without billing
**Solution:** Create billing account at https://console.cloud.google.com/billing
- Won't be charged for Classroom API (it's free)
- Just need payment method on file
### "Error 401: deleted_client"
**Problem:** OAuth client was deleted or not created properly
**Solution:** Re-run OAuth client creation (step 5)
```bash
open "https://console.cloud.google.com/apis/credentials?project=$(gcloud config get-value project)"
```
### "Error 403: Access Not Configured"
**Problem:** APIs not enabled yet (takes 1-2 min to propagate)
**Solution:** Wait 2 minutes, then verify:
```bash
gcloud services list --enabled | grep classroom
```
### "Invalid redirect URI"
**Problem:** Redirect URI doesn't match OAuth client config
**Solution:** Check that these URIs are in your OAuth client:
- http://localhost:3000/api/auth/callback/google
- https://abaci.one/api/auth/callback/google
### "App is not verified"
**Problem:** OAuth consent screen in "Testing" mode
**Solution:** This is **normal** for development!
- Click "Advanced" → "Go to [app name] (unsafe)"
- Only affects external test users
- For production, submit for verification (takes 1-2 weeks)
---
## CLI Reference
### Project Management
```bash
# List all your projects
gcloud projects list
# Switch project
gcloud config set project PROJECT_ID
# Delete project (if needed)
gcloud projects delete PROJECT_ID
```
### API Management
```bash
# List enabled APIs
gcloud services list --enabled
# Enable an API
gcloud services enable APINAME.googleapis.com
# Disable an API
gcloud services disable APINAME.googleapis.com
# Check quota
gcloud services quota describe classroom.googleapis.com
```
### OAuth Management
```bash
# List OAuth clients (requires REST API)
PROJECT_ID=$(gcloud config get-value project)
ACCESS_TOKEN=$(gcloud auth application-default print-access-token)
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://oauth2.googleapis.com/v1/projects/$PROJECT_ID/oauthClients"
```
### Billing
```bash
# List billing accounts
gcloud billing accounts list
# Link billing to project
gcloud billing projects link PROJECT_ID --billing-account=ACCOUNT_ID
# Check project billing status
gcloud billing projects describe PROJECT_ID
```
---
## What You Can Do From CLI (Summary)
✅ **Fully Automated:**
- Authenticate with Google
- Create GCP project
- Enable APIs
- Link billing account
- Configure environment variables
⚠️ **Requires Web Console (2-5 minutes):**
- Create OAuth client (2 min)
- Configure OAuth consent screen (3 min)
**Why web console required:**
Google doesn't provide CLI for these security-sensitive operations. But the script:
- Opens the exact pages for you
- Provides step-by-step instructions
- Makes it as painless as possible
---
## Cost Breakdown
| Item | Cost |
|------|------|
| GCP project | $0 |
| Google Classroom API | $0 (free forever) |
| Google People API | $0 (free forever) |
| Billing account requirement | $0 (no charges) |
| **Total** | **$0** |
**Note:** You need to add a payment method for billing account, but Google Classroom API is completely free with no usage limits.
---
## Next Steps After Setup
1. ✅ Run the setup script: `./scripts/setup-google-classroom.sh`
2. ✅ Add Google provider to NextAuth
3. ✅ Test "Sign in with Google"
4. 📝 Implement class import feature (Phase 2 of roadmap)
5. 📝 Build teacher dashboard
6. 📝 Add assignment integration
Refer to `.claude/PLATFORM_INTEGRATION_ROADMAP.md` for full implementation timeline.
---
## Security Best Practices
### Protect Your Secrets
```bash
# Check .env.local is in .gitignore
cat .gitignore | grep .env.local
```
Should see:
```
.env*.local
```
### Rotate Credentials Periodically
```bash
# Open credentials page
PROJECT_ID=$(gcloud config get-value project)
open "https://console.cloud.google.com/apis/credentials?project=$PROJECT_ID"
# Delete old client, create new one
# Update .env.local with new credentials
```
### Use Different Credentials for Dev/Prod
**Development:**
- OAuth client: `http://localhost:3000/api/auth/callback/google`
- Test users only
**Production:**
- OAuth client: `https://abaci.one/api/auth/callback/google`
- Verified app (submit for review)
---
## Resources
**Official Documentation:**
- GCP CLI: https://cloud.google.com/sdk/gcloud
- Classroom API: https://developers.google.com/classroom
- OAuth 2.0: https://developers.google.com/identity/protocols/oauth2
**Script Location:**
- `scripts/setup-google-classroom.sh`
**Configuration Files:**
- `.env.local` (credentials)
- `src/app/api/auth/[...nextauth]/route.ts` (NextAuth config)
---
**Ready to run?**
```bash
./scripts/setup-google-classroom.sh
```
Good luck! 🚀

View File

@@ -0,0 +1,283 @@
# Matching Game Stats Integration Guide
## Quick Reference
**Files to modify**: `src/arcade-games/matching/components/ResultsPhase.tsx`
**What we're adding**: Call `useRecordGameResult()` when game completes to save per-player stats.
## Current State Analysis
### ResultsPhase.tsx (lines 9-29)
Already has all the data we need:
```typescript
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
const gameTime = state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0
const analysis = getPerformanceAnalysis(state)
const multiplayerResult = gameMode === 'multiplayer'
? getMultiplayerWinner(state, activePlayers)
: null
```
**Available data:**
-`state.scores` - scores by player ID
-`state.gameStartTime`, `state.gameEndTime` - timing
-`state.matchedPairs`, `state.totalPairs` - completion
-`state.moves` - total moves
-`activePlayers` - array of player IDs
-`multiplayerResult.winners` - who won
-`analysis.statistics.accuracy` - accuracy percentage
## Implementation Steps
### Step 1: Add state flag to prevent duplicate recording
Add `recorded: boolean` to `MatchingState` type:
```typescript
// src/arcade-games/matching/types.ts (add to MatchingState interface)
export interface MatchingState extends GameState {
// ... existing fields ...
// Stats recording
recorded?: boolean // ← ADD THIS
}
```
### Step 2: Import the hook in ResultsPhase.tsx
```typescript
// At top of src/arcade-games/matching/components/ResultsPhase.tsx
import { useEffect } from 'react' // ← ADD if not present
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
import type { GameResult } from '@/lib/arcade/stats/types'
```
### Step 3: Call the hook
```typescript
// Inside ResultsPhase component, after existing hooks
export function ResultsPhase() {
const router = useRouter()
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// ← ADD THIS
const { mutate: recordGame, isPending: isRecording } = useRecordGameResult()
// ... existing code ...
```
### Step 4: Record game result on mount
Add this useEffect after the hook declarations:
```typescript
// Record game result once when entering results phase
useEffect(() => {
// Only record if we haven't already
if (state.phase === 'results' && !state.recorded && !isRecording) {
const gameTime = state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0
const analysis = getPerformanceAnalysis(state)
const multiplayerResult = gameMode === 'multiplayer'
? getMultiplayerWinner(state, activePlayers)
: null
// Build GameResult
const gameResult: GameResult = {
gameType: state.gameType === 'abacus-numeral'
? 'matching-abacus'
: 'matching-complements',
completedAt: state.gameEndTime || Date.now(),
duration: gameTime,
playerResults: activePlayers.map(playerId => {
const score = state.scores[playerId] || 0
const won = multiplayerResult
? multiplayerResult.winners.includes(playerId)
: state.matchedPairs === state.totalPairs // Solo = completed
// In multiplayer, calculate per-player accuracy from their score
// In single player, use overall accuracy
const playerAccuracy = gameMode === 'multiplayer'
? score / state.totalPairs // Their score as fraction of total pairs
: analysis.statistics.accuracy / 100 // Convert percentage to 0-1
return {
playerId,
won,
score,
accuracy: playerAccuracy,
completionTime: gameTime,
metrics: {
moves: state.moves,
matchedPairs: state.matchedPairs,
difficulty: state.difficulty,
}
}
}),
metadata: {
gameType: state.gameType,
difficulty: state.difficulty,
grade: analysis.grade,
starRating: analysis.starRating,
}
}
// Record to database
recordGame(gameResult, {
onSuccess: (updates) => {
console.log('✅ Stats recorded:', updates)
// Mark as recorded to prevent duplicate saves
// Note: This assumes Provider has a way to update state.recorded
// We'll need to add an action for this
},
onError: (error) => {
console.error('❌ Failed to record stats:', error)
}
})
}
}, [state.phase, state.recorded, isRecording, /* ... deps */])
```
### Step 5: Add loading state UI (optional)
Show a subtle loading indicator while recording:
```typescript
// At the top of the return statement in ResultsPhase
if (isRecording) {
return (
<div className={css({
textAlign: 'center',
padding: '20px',
})}>
<p>Saving results...</p>
</div>
)
}
```
Or keep it subtle and just disable buttons:
```typescript
// On the "Play Again" button
<button
disabled={isRecording}
className={css({
// ... styles ...
opacity: isRecording ? 0.5 : 1,
cursor: isRecording ? 'not-allowed' : 'pointer',
})}
onClick={resetGame}
>
{isRecording ? '💾 Saving...' : '🎮 Play Again'}
</button>
```
## Provider Changes Needed
The Provider needs an action to mark the game as recorded:
```typescript
// src/arcade-games/matching/Provider.tsx
// Add to the context type
export interface MatchingContextType {
// ... existing ...
markAsRecorded: () => void // ← ADD THIS
}
// Add to the reducer or state update logic
const markAsRecorded = useCallback(() => {
setState(prev => ({ ...prev, recorded: true }))
}, [])
// Add to the context value
const contextValue: MatchingContextType = {
// ... existing ...
markAsRecorded,
}
```
Then in ResultsPhase useEffect:
```typescript
onSuccess: (updates) => {
console.log('✅ Stats recorded:', updates)
markAsRecorded() // ← Use this instead
}
```
## Testing Checklist
### Solo Game
- [ ] Play a game to completion
- [ ] Check console for "✅ Stats recorded"
- [ ] Refresh page
- [ ] Go to `/games` page
- [ ] Verify player's gamesPlayed incremented
- [ ] Verify player's totalWins incremented (if completed)
### Multiplayer Game
- [ ] Activate 2+ players
- [ ] Play a game to completion
- [ ] Check console for stats for ALL players
- [ ] Go to `/games` page
- [ ] Verify each player's stats updated independently
- [ ] Winner should have +1 win
- [ ] All players should have +1 games played
### Edge Cases
- [ ] Incomplete game (exit early) - should NOT record
- [ ] Play again from results - should NOT duplicate record
- [ ] Network error during save - should show error, not mark as recorded
## Common Issues
### Issue: Stats recorded multiple times
**Cause**: useEffect dependency array missing or incorrect
**Fix**: Ensure `state.recorded` is in deps and checked in condition
### Issue: Can't read property 'id' of undefined
**Cause**: Player not found in playerMap
**Fix**: Add null checks when mapping activePlayers
### Issue: Accuracy is always 100% or 0%
**Cause**: Wrong calculation or unit (percentage vs decimal)
**Fix**: Ensure accuracy is 0.0 - 1.0, not 0-100
### Issue: Single player never "wins"
**Cause**: Wrong win condition for solo mode
**Fix**: Solo player wins if they complete all pairs (`state.matchedPairs === state.totalPairs`)
## Next Steps After Integration
1. ✅ Verify stats save correctly
2. ✅ Update `/games` page to fetch and display per-player stats
3. ✅ Test with different game modes and difficulties
4. 🔄 Repeat this pattern for other arcade games
5. 📊 Add stats visualization/charts (future)
---
**Status**: Ready for implementation
**Blocked by**:
- Database schema (player_stats table)
- API endpoints (/api/player-stats/record-game)
- React hooks (useRecordGameResult)

View File

@@ -0,0 +1,594 @@
# Per-Player Stats Architecture & Implementation Plan
## Executive Summary
This document outlines the architecture for tracking game statistics per-player (not per-user). Each local player profile will maintain their own game history, wins, losses, and performance metrics. We'll build a universal framework that any arcade game can use to record results.
**Starting point**: Matching/Memory Lightning game
## Current State Problems
1. ❌ Global `user_stats` table exists but games never update it
2.`/games` page shows same global stats for all players
3. ❌ No framework for games to save results
4. ❌ Players table has no stats fields
## Architecture Design
### 1. Database Schema
#### New Table: `player_stats`
```sql
CREATE TABLE player_stats (
player_id TEXT PRIMARY KEY REFERENCES players(id) ON DELETE CASCADE,
-- Aggregate stats
games_played INTEGER NOT NULL DEFAULT 0,
total_wins INTEGER NOT NULL DEFAULT 0,
total_losses INTEGER NOT NULL DEFAULT 0,
-- Performance metrics
best_time INTEGER, -- Best completion time (ms)
highest_accuracy REAL NOT NULL DEFAULT 0, -- 0.0 - 1.0
-- Game preferences
favorite_game_type TEXT, -- Most played game
-- Per-game stats (JSON)
game_stats TEXT NOT NULL DEFAULT '{}', -- { "matching": { wins: 5, played: 10 }, ... }
-- Timestamps
last_played_at INTEGER, -- timestamp
created_at INTEGER NOT NULL, -- timestamp
updated_at INTEGER NOT NULL -- timestamp
);
CREATE INDEX player_stats_last_played_idx ON player_stats(last_played_at);
```
#### Per-Game Stats Structure (JSON)
```typescript
type PerGameStats = {
[gameName: string]: {
gamesPlayed: number
wins: number
losses: number
bestTime: number | null
highestAccuracy: number
averageScore: number
lastPlayed: number // timestamp
}
}
```
#### Keep `user_stats`?
**Decision**: Deprecate `user_stats` table. All stats are now per-player.
**Reasoning**:
- Users can have multiple players
- Aggregate "user level" stats can be computed by summing player stats
- Simpler mental model: players compete, players have stats
- `/games` page displays players, so showing player stats makes sense
### 2. Universal Game Result Types
**Analysis**: Examined 5 arcade games (Matching, Complement Race, Memory Quiz, Card Sorting, Rithmomachia)
**Key Finding**: Cooperative games need special handling - all players share win/loss!
**See**: `.claude/GAME_STATS_COMPARISON.md` for detailed cross-game analysis
```typescript
// src/lib/arcade/stats/types.ts
/**
* Standard game result that all arcade games must provide
*
* Supports:
* - 1-N players
* - Competitive (individual winners)
* - Cooperative (team wins/losses)
* - Solo completion
* - Head-to-head (2-player)
*/
export interface GameResult {
// Game identification
gameType: string // e.g., "matching", "complement-race", "memory-quiz"
// Player results (for multiplayer, array of results)
playerResults: PlayerGameResult[]
// Game metadata
completedAt: number // timestamp
duration: number // milliseconds
// Optional game-specific data
metadata?: {
// For cooperative games (Memory Quiz, Card Sorting collaborative)
isTeamVictory?: boolean // All players share win/loss
// For specific win conditions (Rithmomachia)
winCondition?: string // e.g., "HARMONY", "POINTS", "TIMEOUT"
// For game modes
gameMode?: string // e.g., "solo", "competitive", "cooperative"
// Extensible for other game-specific info
[key: string]: unknown
}
}
export interface PlayerGameResult {
playerId: string
// Outcome
won: boolean // For cooperative: all players have same value
placement?: number // 1st, 2nd, 3rd place (for tournaments with 3+ players)
// Performance
score?: number
accuracy?: number // 0.0 - 1.0
completionTime?: number // milliseconds (player-specific)
// Game-specific metrics (stored as JSON in DB)
metrics?: {
// Matching
moves?: number
matchedPairs?: number
difficulty?: number
// Complement Race
streak?: number
correctAnswers?: number
totalQuestions?: number
// Memory Quiz
correct?: number
incorrect?: number
// Card Sorting
exactMatches?: number
inversions?: number
lcsLength?: number
// Rithmomachia
capturedPieces?: number
points?: number
// Extensible for future games
[key: string]: unknown
}
}
/**
* Stats update returned from API
*/
export interface StatsUpdate {
playerId: string
previousStats: PlayerStats
newStats: PlayerStats
changes: {
gamesPlayed: number
wins: number
losses: number
}
}
export interface PlayerStats {
playerId: string
gamesPlayed: number
totalWins: number
totalLosses: number
bestTime: number | null
highestAccuracy: number
favoriteGameType: string | null
gameStats: PerGameStats
lastPlayedAt: number | null
createdAt: number
updatedAt: number
}
```
### 3. API Endpoints
#### POST `/api/player-stats/record-game`
Records a game result and updates player stats.
**Request:**
```typescript
{
gameResult: GameResult
}
```
**Response:**
```typescript
{
success: true,
updates: StatsUpdate[] // One per player
}
```
**Logic:**
1. Validate game result structure
2. For each player result:
- Fetch or create player_stats record
- Increment games_played
- Increment wins/losses based on outcome
- **Special case**: If `metadata.isTeamVictory === true`, all players share win/loss
- Cooperative games: all win or all lose together
- Competitive games: individual outcomes
- Update best_time if improved
- Update highest_accuracy if improved
- Update game-specific stats in JSON
- Update favorite_game_type based on most played
- Set last_played_at
3. Return updates for all players
**Example pseudo-code**:
```typescript
for (const playerResult of gameResult.playerResults) {
const stats = await getPlayerStats(playerResult.playerId)
stats.gamesPlayed++
// Handle cooperative games specially
if (gameResult.metadata?.isTeamVictory !== undefined) {
// Cooperative: all players share outcome
if (playerResult.won) {
stats.totalWins++
} else {
stats.totalLosses++
}
} else {
// Competitive/Solo: individual outcome
if (playerResult.won) {
stats.totalWins++
} else {
stats.totalLosses++
}
}
// ... rest of stats update
}
```
#### GET `/api/player-stats/:playerId`
Fetch stats for a specific player.
**Response:**
```typescript
{
stats: PlayerStats
}
```
#### GET `/api/player-stats`
Fetch stats for all current user's players.
**Response:**
```typescript
{
playerStats: PlayerStats[]
}
```
### 4. React Hooks
#### `useRecordGameResult()`
Main hook that games use to record results.
```typescript
// src/hooks/useRecordGameResult.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { GameResult, StatsUpdate } from '@/lib/arcade/stats/types'
export function useRecordGameResult() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (gameResult: GameResult): Promise<StatsUpdate[]> => {
const res = await fetch('/api/player-stats/record-game', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gameResult }),
})
if (!res.ok) throw new Error('Failed to record game result')
const data = await res.json()
return data.updates
},
onSuccess: (updates) => {
// Invalidate player stats queries to trigger refetch
queryClient.invalidateQueries({ queryKey: ['player-stats'] })
// Show success feedback (optional)
console.log('✅ Game result recorded:', updates)
},
onError: (error) => {
console.error('❌ Failed to record game result:', error)
},
})
}
```
#### `usePlayerStats(playerId?)`
Fetch stats for a player (or all players if no ID).
```typescript
// src/hooks/usePlayerStats.ts
import { useQuery } from '@tanstack/react-query'
import type { PlayerStats } from '@/lib/arcade/stats/types'
export function usePlayerStats(playerId?: string) {
return useQuery({
queryKey: playerId ? ['player-stats', playerId] : ['player-stats'],
queryFn: async (): Promise<PlayerStats | PlayerStats[]> => {
const url = playerId
? `/api/player-stats/${playerId}`
: '/api/player-stats'
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch player stats')
const data = await res.json()
return playerId ? data.stats : data.playerStats
},
})
}
```
### 5. Game Integration Pattern
Every arcade game should follow this pattern when completing:
```typescript
// In results phase component (e.g., ResultsPhase.tsx)
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
import type { GameResult } from '@/lib/arcade/stats/types'
export function ResultsPhase() {
const { state, activePlayers } = useGameContext()
const { mutate: recordGame, isPending } = useRecordGameResult()
// Record game result on mount (once)
useEffect(() => {
if (state.phase === 'results' && !state.recorded) {
const gameResult: GameResult = {
gameType: 'matching',
completedAt: Date.now(),
duration: state.gameEndTime - state.gameStartTime,
playerResults: activePlayers.map(player => ({
playerId: player.id,
won: player.id === winnerId,
score: player.matchCount,
accuracy: player.matchCount / state.totalPairs,
completionTime: player.completionTime,
})),
}
recordGame(gameResult, {
onSuccess: () => {
// Mark as recorded to prevent duplicates
setState({ recorded: true })
}
})
}
}, [state.phase, state.recorded])
// Show loading state while recording
if (isPending) {
return <div>Saving results...</div>
}
// Show results UI
return <div>...</div>
}
```
## Implementation Plan
### Phase 1: Foundation (Database & API)
1. **Create database schema**
- File: `src/db/schema/player-stats.ts`
- Define `player_stats` table with Drizzle ORM
- Add type exports
2. **Generate migration**
```bash
npx drizzle-kit generate:sqlite
```
3. **Create type definitions**
- File: `src/lib/arcade/stats/types.ts`
- Define `GameResult`, `PlayerGameResult`, `StatsUpdate`, `PlayerStats`
4. **Build API endpoint**
- File: `src/app/api/player-stats/record-game/route.ts`
- Implement POST handler with validation
- Handle per-player stat updates
- Transaction safety
5. **Build query endpoints**
- File: `src/app/api/player-stats/route.ts` (GET all)
- File: `src/app/api/player-stats/[playerId]/route.ts` (GET one)
### Phase 2: React Hooks & Integration
6. **Create React hooks**
- File: `src/hooks/useRecordGameResult.ts`
- File: `src/hooks/usePlayerStats.ts`
7. **Update GameModeContext**
- Expose helper to get player stats map
- Integrate with usePlayerStats hook
### Phase 3: Matching Game Integration
8. **Analyze matching game completion flow**
- Find where game completes
- Identify winner calculation
- Map state to GameResult format
9. **Integrate stats recording**
- Add useRecordGameResult to ResultsPhase
- Build GameResult from game state
- Handle recording state to prevent duplicates
10. **Test matching game stats**
- Play solo game, verify stats update
- Play multiplayer game, verify all players update
- Check accuracy calculations
- Check time tracking
### Phase 4: UI Updates
11. **Update /games page**
- Fetch per-player stats with usePlayerStats
- Display correct stats for each player card
- Remove dependency on global user profile
12. **Add stats visualization**
- Per-game breakdown
- Win/loss ratio
- Performance trends
### Phase 5: Documentation & Rollout
13. **Document integration pattern**
- Create guide for adding stats to other games
- Code examples
- Common pitfalls
14. **Roll out to other games**
- Complement Race
- Memory Quiz
- Card Sorting
- (Future games)
## Data Migration Strategy
### Handling Existing `user_stats`
**Option A: Drop the table**
- Simple, clean break
- No historical data
**Option B: Migrate to player stats**
- For each user with stats, assign to their first/active player
- More complex but preserves history
**Recommendation**: Option A (drop it) since:
- Very new feature, unlikely much data exists
- Cleaner architecture
- Users can rebuild stats by playing
### Migration SQL
```sql
-- Drop old user_stats table
DROP TABLE IF EXISTS user_stats;
-- Create new player_stats table
-- (Drizzle migration will handle this)
```
## Testing Strategy
### Unit Tests
- `GameResult` validation
- Stats calculation logic
- JSON merge for per-game stats
- Favorite game detection
### Integration Tests
- API endpoint: record game, verify DB update
- API endpoint: fetch stats, verify response
- React hook: record game, verify cache invalidation
### E2E Tests
- Play matching game solo, check stats on /games page
- Play matching game multiplayer, verify each player's stats
- Verify stats persist across sessions
## Success Criteria
✅ Player stats save correctly after game completion
✅ Each player maintains separate stats
✅ /games page displays correct per-player stats
✅ Stats survive page refresh
✅ Multiplayer games update all participants
✅ Framework is reusable for other games
✅ No duplicate recordings
✅ Performance acceptable (< 200ms to record)
## Open Questions
1. **Leaderboards?** - Future consideration, need global rankings
2. **Historical games?** - Store individual game records or just aggregates?
3. **Stats reset?** - Should users be able to reset player stats?
4. **Achievements?** - Track milestones? (100 games, 50 wins, etc.)
## File Structure
```
src/
├── db/
│ └── schema/
│ └── player-stats.ts # NEW: Drizzle schema
├── lib/
│ └── arcade/
│ └── stats/
│ ├── types.ts # NEW: Type definitions
│ └── utils.ts # NEW: Helper functions
├── hooks/
│ ├── useRecordGameResult.ts # NEW: Record game hook
│ └── usePlayerStats.ts # NEW: Fetch stats hook
├── app/
│ └── api/
│ └── player-stats/
│ ├── route.ts # NEW: GET all
│ ├── record-game/
│ │ └── route.ts # NEW: POST record
│ └── [playerId]/
│ └── route.ts # NEW: GET one
└── arcade-games/
└── matching/
└── components/
└── ResultsPhase.tsx # MODIFY: Add stats recording
.claude/
└── PER_PLAYER_STATS_ARCHITECTURE.md # THIS FILE
```
## Next Steps
1. Review this plan with user
2. Create database schema and types
3. Build API endpoints
4. Create React hooks
5. Integrate with matching game
6. Test thoroughly
7. Roll out to other games
---
**Document Status**: Draft for review
**Last Updated**: 2025-01-03
**Owner**: Claude Code

View File

@@ -145,11 +145,30 @@
"Bash(gcloud config list:*)",
"WebFetch(domain:www.boardspace.net)",
"WebFetch(domain:www.gamecabinet.com)",
"WebFetch(domain:en.wikipedia.org)"
"WebFetch(domain:en.wikipedia.org)",
"Bash(pnpm search:*)",
"Bash(mkdir:*)",
"Bash(timeout 10 npx drizzle-kit generate:sqlite:*)",
"Bash(brew install:*)",
"Bash(sudo ln:*)",
"Bash(cd:*)",
"Bash(git clone:*)",
"Bash(git ls-remote:*)",
"Bash(openscad:*)",
"Bash(npx eslint:*)",
"Bash(env)",
"Bash(security find-generic-password -s 'Anthropic API Key' -w)",
"Bash(printenv:*)",
"Bash(typst:*)",
"Bash(npx tsx:*)",
"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

@@ -0,0 +1,17 @@
-- Migration: Add player_stats table
-- Per-player game statistics tracking
CREATE TABLE `player_stats` (
`player_id` text PRIMARY KEY NOT NULL,
`games_played` integer DEFAULT 0 NOT NULL,
`total_wins` integer DEFAULT 0 NOT NULL,
`total_losses` integer DEFAULT 0 NOT NULL,
`best_time` integer,
`highest_accuracy` real DEFAULT 0 NOT NULL,
`favorite_game_type` text,
`game_stats` text DEFAULT '{}' NOT NULL,
`last_played_at` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON UPDATE no action ON DELETE cascade
);

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && concurrently \"node server.js\" \"npx @pandacss/dev --watch\"",
"build": "node scripts/generate-build-info.js && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
"build": "node scripts/generate-build-info.js && npx @pandacss/dev && tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json && next build",
"start": "NODE_ENV=production node server.js",
"lint": "npx @biomejs/biome lint . && npx eslint .",
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
@@ -47,6 +47,8 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-spring/web": "^10.0.3",
"@react-three/drei": "^9.117.0",
"@react-three/fiber": "^8.17.0",
"@soroban/abacus-react": "workspace:*",
"@soroban/core": "workspace:*",
"@soroban/templates": "workspace:*",
@@ -57,6 +59,8 @@
"bcryptjs": "^2.4.3",
"better-sqlite3": "^12.4.1",
"drizzle-orm": "^0.44.6",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"emojibase-data": "^16.0.3",
"jose": "^6.1.0",
"js-yaml": "^4.1.0",
@@ -76,6 +80,7 @@
"react-textfit": "^1.1.1",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"three": "^0.169.0",
"y-protocols": "^1.0.6",
"y-websocket": "^3.0.0",
"yjs": "^13.6.27",

View File

@@ -0,0 +1,39 @@
include <BOSL2/std.scad>; // BOSL2 v2.0 or newer
// ---- USER CUSTOMIZABLE PARAMETERS ----
// These can be overridden via command line: -D 'columns=7' etc.
columns = 13; // Total number of columns (1-13, mirrored book design)
scale_factor = 1.5; // Overall size scale (preserves aspect ratio)
// -----------------------------------------
stl_path = "./simplified.abacus.stl";
// Calculate parameters based on column count
// The full STL has 13 columns. We want columns/2 per side (mirrored).
// The original bounding box intersection: scale([35/186, 1, 1])
// 35/186 ≈ 0.188 = ~2.44 columns, so 186 units ≈ 13 columns, ~14.3 units per column
total_columns_in_stl = 13;
columns_per_side = columns / 2;
width_scale = columns_per_side / total_columns_in_stl;
// Column spacing: distance between mirrored halves
// Original spacing of 69 for ~2.4 columns/side
// Calculate proportional spacing based on columns
units_per_column = 186 / total_columns_in_stl; // ~14.3 units per column
column_spacing = columns_per_side * units_per_column;
// --- actual model ---
module imported()
import(stl_path, convexity = 10);
module half_abacus() {
intersection() {
scale([width_scale, 1, 1]) bounding_box() imported();
imported();
}
}
scale([scale_factor, scale_factor, scale_factor]) {
translate([column_spacing, 0, 0]) mirror([1,0,0]) half_abacus();
half_abacus();
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -7,14 +7,25 @@
* SVG output as the interactive client-side version (without animations).
*/
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { writeFileSync } from 'fs'
import { join } from 'path'
import { AbacusReact } from '@soroban/abacus-react'
// Extract just the SVG element content from rendered output
function extractSvgContent(markup: string): string {
// Find the opening <svg and closing </svg> tags
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[1] // Return just the inner content
}
// Generate the favicon (icon.svg) - single column showing value 5
function generateFavicon(): string {
const iconSvg = renderToStaticMarkup(
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
value={5}
columns={1}
@@ -23,30 +34,38 @@ function generateFavicon(): string {
interactive={false}
showNumbers={false}
customStyles={{
heavenBeads: { fill: '#fbbf24' },
earthBeads: { fill: '#fbbf24' },
heavenBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
earthBeads: { fill: '#7c2d12', stroke: '#451a03', strokeWidth: 1 },
columnPosts: {
fill: '#7c2d12',
stroke: '#92400e',
fill: '#451a03',
stroke: '#292524',
strokeWidth: 2,
},
reckoningBar: {
fill: '#92400e',
stroke: '#92400e',
fill: '#292524',
stroke: '#292524',
strokeWidth: 3,
},
}}
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
// Wrap in SVG with proper viewBox for favicon sizing
// AbacusReact with 1 column + scaleFactor 1.0 = ~25×120px
// Scale 0.7 = ~17.5×84px, centered in 100×100
return `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle for better visibility -->
<circle cx="50" cy="50" r="48" fill="#fef3c7"/>
<!-- Abacus from @soroban/abacus-react -->
<g transform="translate(32, 8) scale(0.36)">
${iconSvg}
<g transform="translate(41, 8) scale(0.7)">
${svgContent}
</g>
</svg>
`
@@ -54,74 +73,131 @@ function generateFavicon(): string {
// Generate the Open Graph image (og-image.svg)
function generateOGImage(): string {
const abacusSvg = renderToStaticMarkup(
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
value={123}
columns={3}
scaleFactor={1.8}
value={1234}
columns={4}
scaleFactor={3.5}
animated={false}
interactive={false}
showNumbers={false}
customStyles={{
heavenBeads: { fill: '#fbbf24' },
earthBeads: { fill: '#fbbf24' },
columnPosts: {
fill: '#7c2d12',
stroke: '#92400e',
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: '#92400e',
stroke: '#92400e',
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
columns: {
0: {
// Ones place (rightmost) - Blue
heavenBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
earthBeads: { fill: '#60a5fa', stroke: '#3b82f6', strokeWidth: 1 },
},
1: {
// Tens place - Green
heavenBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
earthBeads: { fill: '#4ade80', stroke: '#22c55e', strokeWidth: 1 },
},
2: {
// Hundreds place - Yellow/Gold
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 1 },
},
3: {
// Thousands place (leftmost) - Purple
heavenBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
earthBeads: { fill: '#c084fc', stroke: '#a855f7', strokeWidth: 1 },
},
},
}}
/>
)
// Extract just the SVG content (without div wrapper)
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
return `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
<!-- Gradient background -->
<!-- Dark background like homepage -->
<rect width="1200" height="630" fill="#111827"/>
<!-- Subtle dot pattern background -->
<defs>
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#fef3c7;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fcd34d;stop-opacity:1" />
<pattern id="dots" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="1" fill="rgba(255, 255, 255, 0.15)" />
</pattern>
</defs>
<rect width="1200" height="630" fill="url(#dots)" opacity="0.1"/>
<!-- Left decorative elements - Diamond shapes and math operators -->
<g opacity="0.4">
<!-- Purple diamond (thousands) -->
<polygon points="150,120 180,150 150,180 120,150" fill="#c084fc" />
<!-- Gold diamond (hundreds) -->
<polygon points="150,220 180,250 150,280 120,250" fill="#fbbf24" />
<!-- Green diamond (tens) -->
<polygon points="150,320 180,350 150,380 120,350" fill="#4ade80" />
<!-- Blue diamond (ones) -->
<polygon points="150,420 180,450 150,480 120,450" fill="#60a5fa" />
</g>
<!-- Left math operators -->
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
<text x="80" y="100" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
<text x="240" y="190" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
<text x="70" y="290" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
<text x="250" y="390" font-family="Arial, sans-serif" font-size="42" font-weight="300"></text>
</g>
<!-- Right decorative elements - Diamond shapes and math operators -->
<g opacity="0.4">
<!-- Purple diamond (thousands) -->
<polygon points="1050,120 1080,150 1050,180 1020,150" fill="#c084fc" />
<!-- Gold diamond (hundreds) -->
<polygon points="1050,220 1080,250 1050,280 1020,250" fill="#fbbf24" />
<!-- Green diamond (tens) -->
<polygon points="1050,320 1080,350 1050,380 1020,350" fill="#4ade80" />
<!-- Blue diamond (ones) -->
<polygon points="1050,420 1080,450 1050,480 1020,450" fill="#60a5fa" />
</g>
<!-- Right math operators -->
<g opacity="0.35" fill="rgba(255, 255, 255, 0.8)">
<text x="940" y="160" font-family="Arial, sans-serif" font-size="42" font-weight="300">÷</text>
<text x="1110" y="270" font-family="Arial, sans-serif" font-size="42" font-weight="300">+</text>
<text x="920" y="360" font-family="Arial, sans-serif" font-size="42" font-weight="300">×</text>
<text x="1120" y="480" font-family="Arial, sans-serif" font-size="42" font-weight="300">=</text>
</g>
<!-- Huge centered abacus from @soroban/abacus-react -->
<!-- AbacusReact 4 columns @ scale 3.5: width ~350px, height ~420px -->
<!-- Center horizontally: (1200-350)/2 = 425px -->
<!-- Center vertically in upper portion: abacus middle at ~225px, so start at 225-210 = 15px -->
<g transform="translate(425, 15)">
${svgContent}
</g>
<!-- Title at bottom, horizontally and vertically centered in lower portion -->
<!-- Position at y=520 for vertical centering in bottom half -->
<text x="600" y="520" font-family="Arial, sans-serif" font-size="72" font-weight="bold" fill="url(#title-gradient)" text-anchor="middle">
Abaci One
</text>
<!-- Gold gradient for title -->
<defs>
<linearGradient id="title-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#fbbf24;stop-opacity:1" />
<stop offset="50%" style="stop-color:#f59e0b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#fbbf24;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bg-gradient)"/>
<!-- Left side - Abacus from @soroban/abacus-react -->
<g transform="translate(80, 100) scale(0.9)">
${abacusSvg}
</g>
<!-- Right side - Text content -->
<g transform="translate(550, 180)">
<!-- Main title -->
<text x="0" y="0" font-family="Arial, sans-serif" font-size="80" font-weight="bold" fill="#7c2d12">
Abaci.One
</text>
<!-- Subtitle -->
<text x="0" y="80" font-family="Arial, sans-serif" font-size="36" font-weight="600" fill="#92400e">
Learn Soroban Through Play
</text>
<!-- Features -->
<text x="0" y="150" font-family="Arial, sans-serif" font-size="28" fill="#78350f">
• Interactive Games
</text>
<text x="0" y="190" font-family="Arial, sans-serif" font-size="28" fill="#78350f">
• Tutorials
</text>
<text x="0" y="230" font-family="Arial, sans-serif" font-size="28" fill="#78350f">
• Practice Tools
</text>
</g>
<!-- Bottom accent line -->
<rect x="0" y="610" width="1200" height="20" fill="#92400e" opacity="0.3"/>
</svg>
`
}
@@ -130,17 +206,14 @@ function generateOGImage(): string {
const appDir = __dirname.replace('/scripts', '')
try {
console.log('Generating favicon from AbacusReact...')
const faviconSvg = generateFavicon()
writeFileSync(join(appDir, 'src', 'app', 'icon.svg'), faviconSvg)
console.log('✓ Generated src/app/icon.svg')
console.log('Generating Open Graph image from AbacusReact...')
const ogImageSvg = generateOGImage()
writeFileSync(join(appDir, 'public', 'og-image.svg'), ogImageSvg)
console.log('✓ Generated public/og-image.svg')
console.log('\n✅ All icons generated successfully!')
console.log('\n✅ Icon generated successfully!')
console.log('\nNote: Day-of-month favicons are generated on-demand by src/app/icon/route.tsx')
console.log('which calls scripts/generateDayIcon.tsx as a subprocess.')
} catch (error) {
console.error('❌ Error generating icons:', error)
process.exit(1)

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env tsx
/**
* Generate a simple abacus SVG (no customization for now - just get it working)
* Usage: npx tsx scripts/generateCalendarAbacus.tsx <value> <columns>
* Example: npx tsx scripts/generateCalendarAbacus.tsx 15 2
*
* Uses AbacusStatic for server-side rendering (no client hooks)
*/
import React from 'react'
import { AbacusStatic } from '@soroban/abacus-react/static'
export function generateAbacusElement(value: number, columns: number) {
return (
<AbacusStatic
value={value}
columns={columns}
scaleFactor={1}
showNumbers={false}
frameVisible={true}
/>
)
}
// CLI interface (if run directly)
if (require.main === module) {
// Only import react-dom/server for CLI usage
const { renderToStaticMarkup } = require('react-dom/server')
const value = parseInt(process.argv[2], 10)
const columns = parseInt(process.argv[3], 10)
if (isNaN(value) || isNaN(columns)) {
console.error('Usage: npx tsx scripts/generateCalendarAbacus.tsx <value> <columns>')
process.exit(1)
}
process.stdout.write(renderToStaticMarkup(generateAbacusElement(value, columns)))
}

View File

@@ -0,0 +1,208 @@
#!/usr/bin/env tsx
/**
* Generate a complete monthly calendar as a single SVG
* This prevents multi-page overflow - one image scales to fit
*
* Usage: npx tsx scripts/generateCalendarComposite.tsx <month> <year>
* Example: npx tsx scripts/generateCalendarComposite.tsx 12 2025
*/
import React from 'react'
import { AbacusStatic, calculateAbacusDimensions } from '@soroban/abacus-react/static'
interface CalendarCompositeOptions {
month: number
year: number
renderToString: (element: React.ReactElement) => string
}
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
]
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
function getDaysInMonth(year: number, month: number): number {
return new Date(year, month, 0).getDate()
}
function getFirstDayOfWeek(year: number, month: number): number {
return new Date(year, month - 1, 1).getDay()
}
export function generateCalendarComposite(options: CalendarCompositeOptions): string {
const { month, year, renderToString } = options
const daysInMonth = getDaysInMonth(year, month)
const firstDayOfWeek = getFirstDayOfWeek(year, month)
const monthName = MONTH_NAMES[month - 1]
// Layout constants for US Letter aspect ratio (8.5 x 11)
const WIDTH = 850
const HEIGHT = 1100
const MARGIN = 50
const CONTENT_WIDTH = WIDTH - MARGIN * 2
const CONTENT_HEIGHT = HEIGHT - MARGIN * 2
// Abacus natural size is 120x230 at scale=1
const ABACUS_NATURAL_WIDTH = 120
const ABACUS_NATURAL_HEIGHT = 230
// Calculate how many columns needed for year
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
// Year abacus dimensions (calculate first to determine header height)
// Use the shared dimension calculator so we stay in sync with AbacusStatic
const { width: yearAbacusActualWidth, height: yearAbacusActualHeight } = calculateAbacusDimensions({
columns: yearColumns,
showNumbers: false,
columnLabels: [],
})
const yearAbacusDisplayWidth = WIDTH * 0.15 // Display size on page
const yearAbacusDisplayHeight = (yearAbacusActualHeight / yearAbacusActualWidth) * yearAbacusDisplayWidth
// Header - sized to fit month name + year abacus
const MONTH_NAME_HEIGHT = 40
const HEADER_HEIGHT = MONTH_NAME_HEIGHT + yearAbacusDisplayHeight + 20 // 20px spacing
const TITLE_Y = MARGIN + 35
const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2
const yearAbacusY = TITLE_Y + 10
// Calendar grid
const GRID_START_Y = MARGIN + HEADER_HEIGHT
const GRID_HEIGHT = CONTENT_HEIGHT - HEADER_HEIGHT
const WEEKDAY_ROW_HEIGHT = 25
const DAY_GRID_HEIGHT = GRID_HEIGHT - WEEKDAY_ROW_HEIGHT
// 7 columns, up to 6 rows (35 cells max = 5 empty + 30 days worst case)
const CELL_WIDTH = CONTENT_WIDTH / 7
const DAY_CELL_HEIGHT = DAY_GRID_HEIGHT / 6
// Day abacus sizing - fit in cell with padding
const CELL_PADDING = 5
// Calculate max scale to fit in cell
const MAX_SCALE_X = (CELL_WIDTH - CELL_PADDING * 2) / ABACUS_NATURAL_WIDTH
const MAX_SCALE_Y = (DAY_CELL_HEIGHT - CELL_PADDING * 2) / ABACUS_NATURAL_HEIGHT
const ABACUS_SCALE = Math.min(MAX_SCALE_X, MAX_SCALE_Y) * 0.9 // 90% to leave breathing room
const SCALED_ABACUS_WIDTH = ABACUS_NATURAL_WIDTH * ABACUS_SCALE
const SCALED_ABACUS_HEIGHT = ABACUS_NATURAL_HEIGHT * ABACUS_SCALE
// Generate calendar grid
const calendarCells: (number | null)[] = []
for (let i = 0; i < firstDayOfWeek; i++) {
calendarCells.push(null)
}
for (let day = 1; day <= daysInMonth; day++) {
calendarCells.push(day)
}
// Render individual abacus SVGs as complete SVG elements
function renderAbacusSVG(value: number, columns: number, scale: number): string {
return renderToString(
<AbacusStatic
value={value}
columns={columns}
scaleFactor={scale}
showNumbers={false}
frameVisible={true}
compact={false}
/>
)
}
// Main composite SVG
const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}" viewBox="0 0 ${WIDTH} ${HEIGHT}">
<!-- Background -->
<rect width="${WIDTH}" height="${HEIGHT}" fill="white"/>
<!-- Title: Month Name -->
<text x="${WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a">
${monthName}
</text>
<!-- Year Abacus (centered below month name) -->
${(() => {
const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1)
const yearAbacusContent = yearAbacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
return `<svg x="${yearAbacusX}" y="${yearAbacusY}" width="${yearAbacusDisplayWidth}" height="${yearAbacusDisplayHeight}"
viewBox="0 0 ${yearAbacusActualWidth} ${yearAbacusActualHeight}">
${yearAbacusContent}
</svg>`
})()}
<!-- Weekday Headers -->
${WEEKDAYS.map((day, i) => `
<text x="${MARGIN + i * CELL_WIDTH + CELL_WIDTH / 2}" y="${GRID_START_Y + 18}"
text-anchor="middle" font-family="Arial" font-size="14" font-weight="bold" fill="#555">
${day}
</text>`).join('')}
<!-- Separator line under weekdays -->
<line x1="${MARGIN}" y1="${GRID_START_Y + WEEKDAY_ROW_HEIGHT}"
x2="${WIDTH - MARGIN}" y2="${GRID_START_Y + WEEKDAY_ROW_HEIGHT}"
stroke="#333" stroke-width="2"/>
<!-- Calendar Grid Cells -->
${calendarCells.map((day, index) => {
const row = Math.floor(index / 7)
const col = index % 7
const cellX = MARGIN + col * CELL_WIDTH
const cellY = GRID_START_Y + WEEKDAY_ROW_HEIGHT + row * DAY_CELL_HEIGHT
return `
<rect x="${cellX}" y="${cellY}" width="${CELL_WIDTH}" height="${DAY_CELL_HEIGHT}"
fill="none" stroke="#333" stroke-width="2"/>`
}).join('')}
<!-- Calendar Day Abaci -->
${calendarCells.map((day, index) => {
if (day === null) return ''
const row = Math.floor(index / 7)
const col = index % 7
const cellX = MARGIN + col * CELL_WIDTH
const cellY = GRID_START_Y + WEEKDAY_ROW_HEIGHT + row * DAY_CELL_HEIGHT
// Center abacus in cell
const abacusCenterX = cellX + CELL_WIDTH / 2
const abacusCenterY = cellY + DAY_CELL_HEIGHT / 2
// Offset to top-left corner of abacus (accounting for scaled size)
const abacusX = abacusCenterX - SCALED_ABACUS_WIDTH / 2
const abacusY = abacusCenterY - SCALED_ABACUS_HEIGHT / 2
// Render at scale=1 and let the nested SVG handle scaling via viewBox
const abacusSVG = renderAbacusSVG(day, 2, 1)
const svgContent = abacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
return `
<!-- Day ${day} (row ${row}, col ${col}) -->
<svg x="${abacusX}" y="${abacusY}" width="${SCALED_ABACUS_WIDTH}" height="${SCALED_ABACUS_HEIGHT}"
viewBox="0 0 ${ABACUS_NATURAL_WIDTH} ${ABACUS_NATURAL_HEIGHT}">
${svgContent}
</svg>`
}).join('')}
</svg>`
return compositeSVG
}
// CLI interface (if run directly)
if (require.main === module) {
// Only import react-dom/server for CLI usage
const { renderToStaticMarkup } = require('react-dom/server')
const month = parseInt(process.argv[2], 10)
const year = parseInt(process.argv[3], 10)
if (isNaN(month) || isNaN(year) || month < 1 || month > 12) {
console.error('Usage: npx tsx scripts/generateCalendarComposite.tsx <month> <year>')
process.exit(1)
}
process.stdout.write(generateCalendarComposite({ month, year, renderToString: renderToStaticMarkup }))
}

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env tsx
/**
* Generate a single day-of-month favicon
* Usage: npx tsx scripts/generateDayIcon.tsx <day>
* Example: npx tsx scripts/generateDayIcon.tsx 15
*/
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusReact } from '@soroban/abacus-react'
// Extract just the SVG element content from rendered output
function extractSvgContent(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[1]
}
// Calculate bounding box that includes active beads AND structural elements (posts, bar)
interface BoundingBox {
minX: number
minY: number
maxX: number
maxY: number
}
function getAbacusBoundingBox(
svgContent: string,
scaleFactor: number,
columns: number
): BoundingBox {
// Parse column posts: <rect x="..." y="..." width="..." height="..." ... >
const postRegex = /<rect\s+x="([^"]+)"\s+y="([^"]+)"\s+width="([^"]+)"\s+height="([^"]+)"/g
const postMatches = [...svgContent.matchAll(postRegex)]
// Parse active bead transforms: <g class="abacus-bead active" transform="translate(x, y)">
const activeBeadRegex =
/<g\s+class="abacus-bead active[^"]*"\s+transform="translate\(([^,]+),\s*([^)]+)\)"/g
const beadMatches = [...svgContent.matchAll(activeBeadRegex)]
if (beadMatches.length === 0) {
// Fallback if no active beads found - show full abacus
return { minX: 0, minY: 0, maxX: 50 * scaleFactor, maxY: 120 * scaleFactor }
}
// Bead dimensions (diamond): width ≈ 30px * scaleFactor, height ≈ 21px * scaleFactor
const beadHeight = 21.6 * scaleFactor
// HORIZONTAL BOUNDS: Always show full width of both columns (fixed for all days)
let minX = Infinity
let maxX = -Infinity
for (const match of postMatches) {
const x = parseFloat(match[1])
const width = parseFloat(match[3])
minX = Math.min(minX, x)
maxX = Math.max(maxX, x + width)
}
// VERTICAL BOUNDS: Crop to active beads (dynamic based on which beads are active)
let minY = Infinity
let maxY = -Infinity
for (const match of beadMatches) {
const y = parseFloat(match[2])
// Top of topmost active bead to bottom of bottommost active bead
minY = Math.min(minY, y)
maxY = Math.max(maxY, y + beadHeight)
}
return { minX, minY, maxX, maxY }
}
// Get day from command line argument
const day = parseInt(process.argv[2], 10)
if (!day || day < 1 || day > 31) {
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
process.exit(1)
}
// Render 2-column abacus showing day of month
const abacusMarkup = renderToStaticMarkup(
<AbacusReact
value={day}
columns={2}
scaleFactor={1.8}
animated={false}
interactive={false}
showNumbers={false}
hideInactiveBeads={true}
customStyles={{
columnPosts: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 2,
},
reckoningBar: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 3,
},
columns: {
0: {
// Ones place - Gold (royal theme)
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
},
1: {
// Tens place - Purple (royal theme)
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
},
},
}}
/>
)
let svgContent = extractSvgContent(abacusMarkup)
// Remove !important from CSS (production code policy)
svgContent = svgContent.replace(/\s*!important/g, '')
// Calculate bounding box including posts, bar, and active beads
const bbox = getAbacusBoundingBox(svgContent, 1.8, 2)
// Add minimal padding around active beads (in abacus coordinates)
// Less padding below since we want to cut tight to the last bead
const paddingTop = 8
const paddingBottom = 2
const paddingSide = 5
const cropX = bbox.minX - paddingSide
const cropY = bbox.minY - paddingTop
const cropWidth = bbox.maxX - bbox.minX + paddingSide * 2
const cropHeight = bbox.maxY - bbox.minY + paddingTop + paddingBottom
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
const targetSize = 96
const scale = Math.min(targetSize / cropWidth, targetSize / cropHeight)
// Center in 100x100 canvas
const scaledWidth = cropWidth * scale
const scaledHeight = cropHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in SVG with proper viewBox for favicon sizing
// Use nested SVG with viewBox to actually CROP the content, not just scale it
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
<!-- Nested SVG with viewBox does the actual cropping -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${cropX} ${cropY} ${cropWidth} ${cropHeight}">
<g class="hide-inactive-mode">
${svgContent}
</g>
</svg>
</svg>
`
// Output to stdout so parent process can capture it
process.stdout.write(svg)

View File

@@ -0,0 +1,46 @@
import { JobManager } from '@/lib/3d-printing/jobManager'
import { NextResponse } from 'next/server'
export async function GET(request: Request, { params }: { params: Promise<{ jobId: string }> }) {
try {
const { jobId } = await params
const job = JobManager.getJob(jobId)
if (!job) {
return NextResponse.json({ error: 'Job not found' }, { status: 404 })
}
if (job.status !== 'completed') {
return NextResponse.json(
{ error: `Job is ${job.status}, not ready for download` },
{ status: 400 }
)
}
const fileBuffer = await JobManager.getJobOutput(jobId)
// Determine content type and filename
const contentTypes = {
stl: 'model/stl',
'3mf': 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml',
scad: 'text/plain',
}
const contentType = contentTypes[job.params.format]
const filename = `abacus.${job.params.format}`
// Convert Buffer to Uint8Array for NextResponse
const uint8Array = new Uint8Array(fileBuffer)
return new NextResponse(uint8Array, {
headers: {
'Content-Type': contentType,
'Content-Disposition': `attachment; filename="${filename}"`,
'Content-Length': fileBuffer.length.toString(),
},
})
} catch (error) {
console.error('Error downloading job:', error)
return NextResponse.json({ error: 'Failed to download file' }, { status: 500 })
}
}

View File

@@ -0,0 +1,57 @@
import { JobManager } from '@/lib/3d-printing/jobManager'
import type { AbacusParams } from '@/lib/3d-printing/jobManager'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
try {
const body = await request.json()
// Validate parameters
const columns = Number.parseInt(body.columns, 10)
const scaleFactor = Number.parseFloat(body.scaleFactor)
const widthMm = body.widthMm ? Number.parseFloat(body.widthMm) : undefined
const format = body.format
// Validation
if (Number.isNaN(columns) || columns < 1 || columns > 13) {
return NextResponse.json({ error: 'columns must be between 1 and 13' }, { status: 400 })
}
if (Number.isNaN(scaleFactor) || scaleFactor < 0.5 || scaleFactor > 3) {
return NextResponse.json({ error: 'scaleFactor must be between 0.5 and 3' }, { status: 400 })
}
if (widthMm !== undefined && (Number.isNaN(widthMm) || widthMm < 50 || widthMm > 500)) {
return NextResponse.json({ error: 'widthMm must be between 50 and 500' }, { status: 400 })
}
if (!['stl', '3mf', 'scad'].includes(format)) {
return NextResponse.json({ error: 'format must be stl, 3mf, or scad' }, { status: 400 })
}
const params: AbacusParams = {
columns,
scaleFactor,
widthMm,
format,
// 3MF colors (optional)
frameColor: body.frameColor,
heavenBeadColor: body.heavenBeadColor,
earthBeadColor: body.earthBeadColor,
decorationColor: body.decorationColor,
}
const jobId = await JobManager.createJob(params)
return NextResponse.json(
{
jobId,
message: 'Job created successfully',
},
{ status: 202 }
)
} catch (error) {
console.error('Error creating job:', error)
return NextResponse.json({ error: 'Failed to create job' }, { status: 500 })
}
}

View File

@@ -0,0 +1,109 @@
import { JobManager } from '@/lib/3d-printing/jobManager'
import type { AbacusParams } from '@/lib/3d-printing/jobManager'
import { NextResponse } from 'next/server'
// Allow up to 90 seconds for OpenSCAD rendering
export const maxDuration = 90
// Cache for preview STLs to avoid regenerating on every request
const previewCache = new Map<string, { buffer: Buffer; timestamp: number }>()
const CACHE_TTL = 300000 // 5 minutes
function getCacheKey(params: AbacusParams): string {
return `${params.columns}-${params.scaleFactor}`
}
export async function POST(request: Request) {
try {
const body = await request.json()
// Validate parameters
const columns = Number.parseInt(body.columns, 10)
const scaleFactor = Number.parseFloat(body.scaleFactor)
// Validation
if (Number.isNaN(columns) || columns < 1 || columns > 13) {
return NextResponse.json({ error: 'columns must be between 1 and 13' }, { status: 400 })
}
if (Number.isNaN(scaleFactor) || scaleFactor < 0.5 || scaleFactor > 3) {
return NextResponse.json({ error: 'scaleFactor must be between 0.5 and 3' }, { status: 400 })
}
const params: AbacusParams = {
columns,
scaleFactor,
format: 'stl', // Always STL for preview
}
// Check cache first
const cacheKey = getCacheKey(params)
const cached = previewCache.get(cacheKey)
const now = Date.now()
if (cached && now - cached.timestamp < CACHE_TTL) {
// Return cached preview
const uint8Array = new Uint8Array(cached.buffer)
return new NextResponse(uint8Array, {
headers: {
'Content-Type': 'model/stl',
'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
},
})
}
// Generate new preview
const jobId = await JobManager.createJob(params)
// Wait for job to complete (with timeout)
const startTime = Date.now()
const timeout = 90000 // 90 seconds max wait (OpenSCAD can take 40-60s)
while (Date.now() - startTime < timeout) {
const job = JobManager.getJob(jobId)
if (!job) {
return NextResponse.json({ error: 'Job not found' }, { status: 404 })
}
if (job.status === 'completed') {
const buffer = await JobManager.getJobOutput(jobId)
// Cache the result
previewCache.set(cacheKey, { buffer, timestamp: now })
// Clean up old cache entries
for (const [key, value] of previewCache.entries()) {
if (now - value.timestamp > CACHE_TTL) {
previewCache.delete(key)
}
}
// Clean up the job
await JobManager.cleanupJob(jobId)
const uint8Array = new Uint8Array(buffer)
return new NextResponse(uint8Array, {
headers: {
'Content-Type': 'model/stl',
'Cache-Control': 'public, max-age=300',
},
})
}
if (job.status === 'failed') {
return NextResponse.json(
{ error: job.error || 'Preview generation failed' },
{ status: 500 }
)
}
// Wait 500ms before checking again
await new Promise((resolve) => setTimeout(resolve, 500))
}
return NextResponse.json({ error: 'Preview generation timeout' }, { status: 408 })
} catch (error) {
console.error('Error generating preview:', error)
return NextResponse.json({ error: 'Failed to generate preview' }, { status: 500 })
}
}

View File

@@ -0,0 +1,25 @@
import { JobManager } from '@/lib/3d-printing/jobManager'
import { NextResponse } from 'next/server'
export async function GET(request: Request, { params }: { params: Promise<{ jobId: string }> }) {
try {
const { jobId } = await params
const job = JobManager.getJob(jobId)
if (!job) {
return NextResponse.json({ error: 'Job not found' }, { status: 404 })
}
return NextResponse.json({
id: job.id,
status: job.status,
progress: job.progress,
error: job.error,
createdAt: job.createdAt,
completedAt: job.completedAt,
})
} catch (error) {
console.error('Error fetching job status:', error)
return NextResponse.json({ error: 'Failed to fetch job status' }, { status: 500 })
}
}

View File

@@ -0,0 +1,138 @@
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, generateDailyTypst, getDaysInMonth } from '../utils/typstGenerator'
import type { AbacusConfig } from '@soroban/abacus-react'
import { generateCalendarComposite } from '@/../../scripts/generateCalendarComposite'
import { generateAbacusElement } from '@/../../scripts/generateCalendarAbacus'
interface CalendarRequest {
month: number
year: number
format: 'monthly' | 'daily'
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
abacusConfig?: AbacusConfig
}
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
// Dynamic import to avoid Next.js bundler issues with react-dom/server
const { renderToStaticMarkup } = await import('react-dom/server')
const body: CalendarRequest = await request.json()
const { month, year, format, paperSize, abacusConfig } = body
// Validate inputs
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
}
// Create temp directory for SVG files
tempDir = join(tmpdir(), `calendar-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Generate and write SVG files
const daysInMonth = getDaysInMonth(year, month)
let typstContent: string
if (format === 'monthly') {
// Generate single composite SVG for monthly calendar
const calendarSvg = generateCalendarComposite({
month,
year,
renderToString: renderToStaticMarkup
})
if (!calendarSvg || calendarSvg.trim().length === 0) {
throw new Error('Generated empty composite calendar SVG')
}
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
// Generate Typst document
typstContent = generateMonthlyTypst({
month,
year,
paperSize,
daysInMonth,
})
} else {
// Daily format: generate individual SVGs for each day
for (let day = 1; day <= daysInMonth; day++) {
const svg = renderToStaticMarkup(generateAbacusElement(day, 2))
if (!svg || svg.trim().length === 0) {
throw new Error(`Generated empty SVG for day ${day}`)
}
writeFileSync(join(tempDir, `day-${day}.svg`), svg)
}
// Generate year SVG
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
const yearSvg = renderToStaticMarkup(generateAbacusElement(year, yearColumns))
if (!yearSvg || yearSvg.trim().length === 0) {
throw new Error(`Generated empty SVG for year ${year}`)
}
writeFileSync(join(tempDir, 'year.svg'), yearSvg)
// Generate Typst document
typstContent = generateDailyTypst({
month,
year,
paperSize,
daysInMonth,
})
}
// Compile with Typst: stdin for .typ content, stdout for PDF output
let pdfBuffer: Buffer
try {
pdfBuffer = execSync('typst compile - -', {
input: typstContent,
cwd: tempDir, // Run in temp dir so relative paths work
maxBuffer: 50 * 1024 * 1024, // 50MB limit for large calendars
})
} catch (error) {
console.error('Typst compilation error:', error)
return NextResponse.json(
{ error: 'Failed to compile PDF. Is Typst installed?' },
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
// Return JSON with PDF
return NextResponse.json({
pdf: pdfBuffer.toString('base64'),
filename: `calendar-${year}-${String(month).padStart(2, '0')}.pdf`,
})
} catch (error) {
console.error('Error generating calendar:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
// Surface the actual error for debugging
const errorMessage = error instanceof Error ? error.message : String(error)
const errorStack = error instanceof Error ? error.stack : undefined
return NextResponse.json(
{
error: 'Failed to generate calendar',
message: errorMessage,
...(process.env.NODE_ENV === 'development' && { stack: errorStack })
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,97 @@
import { type NextRequest, NextResponse } from 'next/server'
import { writeFileSync, mkdirSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { execSync } from 'child_process'
import { generateMonthlyTypst, getDaysInMonth } from '../utils/typstGenerator'
import { generateCalendarComposite } from '@/../../scripts/generateCalendarComposite'
interface PreviewRequest {
month: number
year: number
format: 'monthly' | 'daily'
}
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest) {
let tempDir: string | null = null
try {
const body: PreviewRequest = await request.json()
const { month, year, format } = body
// Validate inputs
if (!month || month < 1 || month > 12 || !year || year < 1 || year > 9999) {
return NextResponse.json({ error: 'Invalid month or year' }, { status: 400 })
}
// Only generate preview for monthly format
if (format !== 'monthly') {
return NextResponse.json({ svg: null })
}
// Dynamic import to avoid Next.js bundler issues
const { renderToStaticMarkup } = await import('react-dom/server')
// Create temp directory for SVG file
tempDir = join(tmpdir(), `calendar-preview-${Date.now()}-${Math.random()}`)
mkdirSync(tempDir, { recursive: true })
// Generate and write composite SVG
const calendarSvg = generateCalendarComposite({
month,
year,
renderToString: renderToStaticMarkup
})
writeFileSync(join(tempDir, 'calendar.svg'), calendarSvg)
// Generate Typst document content
const daysInMonth = getDaysInMonth(year, month)
const typstContent = generateMonthlyTypst({
month,
year,
paperSize: 'us-letter',
daysInMonth,
})
// Compile with Typst: stdin for .typ content, stdout for SVG output
let svg: string
try {
svg = execSync('typst compile --format svg - -', {
input: typstContent,
encoding: 'utf8',
cwd: tempDir, // Run in temp dir so relative paths work
})
} catch (error) {
console.error('Typst compilation error:', error)
return NextResponse.json(
{ error: 'Failed to compile preview. Is Typst installed?' },
{ status: 500 }
)
}
// Clean up temp directory
rmSync(tempDir, { recursive: true, force: true })
tempDir = null
return NextResponse.json({ svg })
} catch (error) {
console.error('Error generating preview:', error)
// Clean up temp directory if it exists
if (tempDir) {
try {
rmSync(tempDir, { recursive: true, force: true })
} catch (cleanupError) {
console.error('Failed to clean up temp directory:', cleanupError)
}
}
const errorMessage = error instanceof Error ? error.message : String(error)
return NextResponse.json(
{ error: 'Failed to generate preview', message: errorMessage },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,147 @@
interface TypstMonthlyConfig {
month: number
year: number
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
daysInMonth: number
}
interface TypstDailyConfig {
month: number
year: number
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
daysInMonth: number
}
const MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
export function getDaysInMonth(year: number, month: number): number {
return new Date(year, month, 0).getDate()
}
function getFirstDayOfWeek(year: number, month: number): number {
return new Date(year, month - 1, 1).getDay() // 0 = Sunday
}
function getDayOfWeek(year: number, month: number, day: number): string {
const date = new Date(year, month - 1, day)
return date.toLocaleDateString('en-US', { weekday: 'long' })
}
type PaperSize = 'us-letter' | 'a4' | 'a3' | 'tabloid'
interface PaperConfig {
typstName: string
marginX: string
marginY: string
}
function getPaperConfig(size: string): PaperConfig {
const configs: Record<PaperSize, PaperConfig> = {
// Tight margins to maximize space for calendar grid
'us-letter': { typstName: 'us-letter', marginX: '0.5in', marginY: '0.5in' },
// A4 is slightly taller/narrower than US Letter - adjust margins proportionally
a4: { typstName: 'a4', marginX: '1.3cm', marginY: '1.3cm' },
// A3 is 2x area of A4 - can use same margins but will scale content larger
a3: { typstName: 'a3', marginX: '1.5cm', marginY: '1.5cm' },
// Tabloid (11" × 17") is larger - can use more margin
tabloid: { typstName: 'us-tabloid', marginX: '0.75in', marginY: '0.75in' },
}
return configs[size as PaperSize] || configs['us-letter']
}
export function generateMonthlyTypst(config: TypstMonthlyConfig): string {
const { paperSize } = config
const paperConfig = getPaperConfig(paperSize)
// Single-page design: use one composite SVG that scales to fit
// This prevents overflow - Typst will scale the image to fit available space
return `#set page(
paper: "${paperConfig.typstName}",
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
)
// Composite calendar SVG - scales to fit page (prevents multi-page overflow)
#align(center + horizon)[
#image("calendar.svg", width: 100%, fit: "contain")
]
`
}
export function generateDailyTypst(config: TypstDailyConfig): string {
const { month, year, paperSize, daysInMonth } = config
const paperConfig = getPaperConfig(paperSize)
const monthName = MONTH_NAMES[month - 1]
let pages = ''
for (let day = 1; day <= daysInMonth; day++) {
const dayOfWeek = getDayOfWeek(year, month, day)
pages += `
#page(
paper: "${paperConfig.typstName}",
margin: (x: ${paperConfig.marginX}, y: ${paperConfig.marginY}),
)[
// Header: Year
#align(center)[
#v(1em)
#image("year.svg", width: 30%)
]
#v(2em)
// Main: Day number as large abacus
#align(center + horizon)[
#image("day-${day}.svg", width: 50%)
]
#v(2em)
// Footer: Day of week and date
#align(center)[
#text(size: 18pt, weight: "bold")[${dayOfWeek}]
#v(0.5em)
#text(size: 14pt)[${monthName} ${day}, ${year}]
]
// Notes section
#v(3em)
#line(length: 100%, stroke: 0.5pt)
#v(0.5em)
#text(size: 10pt, fill: gray)[Notes:]
#v(0.5em)
#line(length: 100%, stroke: 0.5pt)
#v(1em)
#line(length: 100%, stroke: 0.5pt)
#v(1em)
#line(length: 100%, stroke: 0.5pt)
#v(1em)
#line(length: 100%, stroke: 0.5pt)
]
${day < daysInMonth ? '' : ''}`
if (day < daysInMonth) {
pages += '\n'
}
}
return `#set text(font: "Arial")
${pages}
`
}

View File

@@ -4,6 +4,9 @@ import { getActivePlayers } from '@/lib/arcade/player-manager'
import { db, schema } from '@/db'
import { eq } from 'drizzle-orm'
// Force dynamic rendering - this route uses headers()
export const dynamic = 'force-dynamic'
/**
* GET /api/debug/active-players
* Debug endpoint to check active players for current user

View File

@@ -0,0 +1,111 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import { players } from '@/db/schema/players'
import type { GetPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
/**
* GET /api/player-stats/[playerId]
*
* Fetches stats for a specific player (must be owned by current user).
*/
export async function GET(_request: Request, { params }: { params: { playerId: string } }) {
try {
const { playerId } = params
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Verify player belongs to user
const player = await db
.select()
.from(players)
.where(eq(players.id, playerId))
.limit(1)
.then((rows) => rows[0])
if (!player) {
return NextResponse.json({ error: 'Player not found' }, { status: 404 })
}
if (player.userId !== viewerId) {
return NextResponse.json(
{ error: 'Forbidden: player belongs to another user' },
{ status: 403 }
)
}
// 3. Fetch player stats
const stats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
const playerStatsData: PlayerStatsData = stats
? convertToPlayerStatsData(stats)
: createDefaultPlayerStats(playerId)
// 4. Return response
const response: GetPlayerStatsResponse = {
stats: playerStatsData,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to fetch player stats:', error)
return NextResponse.json(
{
error: 'Failed to fetch player stats',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}

View File

@@ -0,0 +1,277 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import type {
GameResult,
PlayerGameResult,
PlayerStatsData,
RecordGameRequest,
RecordGameResponse,
StatsUpdate,
} from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
/**
* POST /api/player-stats/record-game
*
* Records a game result and updates player stats for all participants.
* Supports cooperative games (team wins/losses) and competitive games.
*/
export async function POST(request: Request) {
try {
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Parse and validate request
const body: RecordGameRequest = await request.json()
const { gameResult } = body
if (!gameResult || !gameResult.playerResults || gameResult.playerResults.length === 0) {
return NextResponse.json(
{ error: 'Invalid game result: playerResults required' },
{ status: 400 }
)
}
if (!gameResult.gameType) {
return NextResponse.json({ error: 'Invalid game result: gameType required' }, { status: 400 })
}
// 3. Process each player's result
const updates: StatsUpdate[] = []
for (const playerResult of gameResult.playerResults) {
const update = await recordPlayerResult(gameResult, playerResult)
updates.push(update)
}
// 4. Return success response
const response: RecordGameResponse = {
success: true,
updates,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to record game result:', error)
return NextResponse.json(
{
error: 'Failed to record game result',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Records stats for a single player's game result
*/
async function recordPlayerResult(
gameResult: GameResult,
playerResult: PlayerGameResult
): Promise<StatsUpdate> {
const { playerId } = playerResult
// 1. Fetch or create player stats
const existingStats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
const previousStats: PlayerStatsData = existingStats
? convertToPlayerStatsData(existingStats)
: createDefaultPlayerStats(playerId)
// 2. Calculate new stats
const newStats: PlayerStatsData = { ...previousStats }
// Always increment games played
newStats.gamesPlayed++
// Handle wins/losses (cooperative vs competitive)
if (gameResult.metadata?.isTeamVictory !== undefined) {
// Cooperative game: all players share outcome
if (playerResult.won) {
newStats.totalWins++
} else {
newStats.totalLosses++
}
} else {
// Competitive/Solo: individual outcome
if (playerResult.won) {
newStats.totalWins++
} else {
newStats.totalLosses++
}
}
// Update best time (if provided and improved)
if (playerResult.completionTime) {
if (!newStats.bestTime || playerResult.completionTime < newStats.bestTime) {
newStats.bestTime = playerResult.completionTime
}
}
// Update highest accuracy (if provided and improved)
if (playerResult.accuracy !== undefined && playerResult.accuracy > newStats.highestAccuracy) {
newStats.highestAccuracy = playerResult.accuracy
}
// Update per-game stats (JSON)
const gameType = gameResult.gameType
const currentGameStats: GameStatsBreakdown = newStats.gameStats[gameType] || {
gamesPlayed: 0,
wins: 0,
losses: 0,
bestTime: null,
highestAccuracy: 0,
averageScore: 0,
lastPlayed: 0,
}
currentGameStats.gamesPlayed++
if (playerResult.won) {
currentGameStats.wins++
} else {
currentGameStats.losses++
}
// Update game-specific best time
if (playerResult.completionTime) {
if (!currentGameStats.bestTime || playerResult.completionTime < currentGameStats.bestTime) {
currentGameStats.bestTime = playerResult.completionTime
}
}
// Update game-specific highest accuracy
if (
playerResult.accuracy !== undefined &&
playerResult.accuracy > currentGameStats.highestAccuracy
) {
currentGameStats.highestAccuracy = playerResult.accuracy
}
// Update average score
if (playerResult.score !== undefined) {
const previousTotal = currentGameStats.averageScore * (currentGameStats.gamesPlayed - 1)
currentGameStats.averageScore =
(previousTotal + playerResult.score) / currentGameStats.gamesPlayed
}
currentGameStats.lastPlayed = gameResult.completedAt
newStats.gameStats[gameType] = currentGameStats
// Update favorite game type (most played)
newStats.favoriteGameType = getMostPlayedGame(newStats.gameStats)
// Update timestamps
newStats.lastPlayedAt = new Date(gameResult.completedAt)
newStats.updatedAt = new Date()
// 3. Save to database
if (existingStats) {
// Update existing record
await db
.update(playerStats)
.set({
gamesPlayed: newStats.gamesPlayed,
totalWins: newStats.totalWins,
totalLosses: newStats.totalLosses,
bestTime: newStats.bestTime,
highestAccuracy: newStats.highestAccuracy,
favoriteGameType: newStats.favoriteGameType,
gameStats: newStats.gameStats as any, // Drizzle JSON type
lastPlayedAt: newStats.lastPlayedAt,
updatedAt: newStats.updatedAt,
})
.where(eq(playerStats.playerId, playerId))
} else {
// Insert new record
await db.insert(playerStats).values({
playerId: newStats.playerId,
gamesPlayed: newStats.gamesPlayed,
totalWins: newStats.totalWins,
totalLosses: newStats.totalLosses,
bestTime: newStats.bestTime,
highestAccuracy: newStats.highestAccuracy,
favoriteGameType: newStats.favoriteGameType,
gameStats: newStats.gameStats as any,
lastPlayedAt: newStats.lastPlayedAt,
createdAt: newStats.createdAt,
updatedAt: newStats.updatedAt,
})
}
// 4. Return update summary
return {
playerId,
previousStats,
newStats,
changes: {
gamesPlayed: newStats.gamesPlayed - previousStats.gamesPlayed,
wins: newStats.totalWins - previousStats.totalWins,
losses: newStats.totalLosses - previousStats.totalLosses,
},
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}
/**
* Determine most-played game from game stats
*/
function getMostPlayedGame(gameStats: Record<string, GameStatsBreakdown>): string | null {
const games = Object.entries(gameStats)
if (games.length === 0) return null
return games.reduce((mostPlayed, [gameType, stats]) => {
const mostPlayedStats = gameStats[mostPlayed]
return stats.gamesPlayed > (mostPlayedStats?.gamesPlayed || 0) ? gameType : mostPlayed
}, games[0][0])
}

View File

@@ -0,0 +1,105 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
import { playerStats } from '@/db/schema/player-stats'
import { players } from '@/db/schema/players'
import type { GetAllPlayerStatsResponse, PlayerStatsData } from '@/lib/arcade/stats/types'
import { getViewerId } from '@/lib/viewer'
// Force dynamic rendering - this route uses headers()
export const dynamic = 'force-dynamic'
/**
* GET /api/player-stats
*
* Fetches stats for all of the current user's players.
*/
export async function GET() {
try {
// 1. Authenticate user
const viewerId = await getViewerId()
if (!viewerId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// 2. Fetch all user's players
const userPlayers = await db.select().from(players).where(eq(players.userId, viewerId))
const playerIds = userPlayers.map((p) => p.id)
// 3. Fetch stats for all players
const allStats: PlayerStatsData[] = []
for (const playerId of playerIds) {
const stats = await db
.select()
.from(playerStats)
.where(eq(playerStats.playerId, playerId))
.limit(1)
.then((rows) => rows[0])
if (stats) {
allStats.push(convertToPlayerStatsData(stats))
} else {
// Player exists but has no stats yet
allStats.push(createDefaultPlayerStats(playerId))
}
}
// 4. Return response
const response: GetAllPlayerStatsResponse = {
playerStats: allStats,
}
return NextResponse.json(response)
} catch (error) {
console.error('❌ Failed to fetch player stats:', error)
return NextResponse.json(
{
error: 'Failed to fetch player stats',
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 }
)
}
}
/**
* Convert DB record to PlayerStatsData
*/
function convertToPlayerStatsData(dbStats: typeof playerStats.$inferSelect): PlayerStatsData {
return {
playerId: dbStats.playerId,
gamesPlayed: dbStats.gamesPlayed,
totalWins: dbStats.totalWins,
totalLosses: dbStats.totalLosses,
bestTime: dbStats.bestTime,
highestAccuracy: dbStats.highestAccuracy,
favoriteGameType: dbStats.favoriteGameType,
gameStats: (dbStats.gameStats as Record<string, GameStatsBreakdown>) || {},
lastPlayedAt: dbStats.lastPlayedAt,
createdAt: dbStats.createdAt,
updatedAt: dbStats.updatedAt,
}
}
/**
* Create default player stats for new player
*/
function createDefaultPlayerStats(playerId: string): PlayerStatsData {
const now = new Date()
return {
playerId,
gamesPlayed: 0,
totalWins: 0,
totalLosses: 0,
bestTime: null,
highestAccuracy: 0,
favoriteGameType: null,
gameStats: {},
lastPlayedAt: null,
createdAt: now,
updatedAt: now,
}
}

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

@@ -1,4 +1,5 @@
import { useCallback, useRef } from 'react'
import { useCallback, useContext, useRef } from 'react'
import { PreviewModeContext } from '@/components/GamePreview'
/**
* Web Audio API sound effects system
@@ -15,6 +16,7 @@ interface Note {
export function useSoundEffects() {
const audioContextsRef = useRef<AudioContext[]>([])
const previewMode = useContext(PreviewModeContext)
/**
* Helper function to play multi-note 90s arcade sounds
@@ -107,6 +109,11 @@ export function useSoundEffects() {
| 'steam_hiss',
volume: number = 0.15
) => {
// Disable all audio in preview mode
if (previewMode?.isPreview) {
return
}
try {
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
@@ -438,7 +445,7 @@ export function useSoundEffects() {
console.log('🎵 Web Audio not supported - missing out on rad 90s sounds!')
}
},
[play90sSound]
[play90sSound, previewMode]
)
/**

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

@@ -2,6 +2,9 @@
import { rithmomachiaGame } from '@/arcade-games/rithmomachia'
// Force dynamic rendering to avoid build-time initialization errors
export const dynamic = 'force-dynamic'
const { Provider, GameComponent } = rithmomachiaGame
export default function RithmomachiaPage() {

View File

@@ -0,0 +1,574 @@
'use client'
import { JobMonitor } from '@/components/3d-print/JobMonitor'
import { STLPreview } from '@/components/3d-print/STLPreview'
import { useState } from 'react'
import { css } from '../../../../styled-system/css'
export default function ThreeDPrintPage() {
// New unified parameter system
const [columns, setColumns] = useState(13)
const [scaleFactor, setScaleFactor] = useState(1.5)
const [widthMm, setWidthMm] = useState<number | undefined>(undefined)
const [format, setFormat] = useState<'stl' | '3mf' | 'scad'>('stl')
// 3MF color options
const [frameColor, setFrameColor] = useState('#8b7355')
const [heavenBeadColor, setHeavenBeadColor] = useState('#e8d5c4')
const [earthBeadColor, setEarthBeadColor] = useState('#6b5444')
const [decorationColor, setDecorationColor] = useState('#d4af37')
const [jobId, setJobId] = useState<string | null>(null)
const [isGenerating, setIsGenerating] = useState(false)
const [isComplete, setIsComplete] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleGenerate = async () => {
setIsGenerating(true)
setError(null)
setIsComplete(false)
try {
const response = await fetch('/api/abacus/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
columns,
scaleFactor,
widthMm,
format,
// Include 3MF colors if format is 3mf
...(format === '3mf' && {
frameColor,
heavenBeadColor,
earthBeadColor,
decorationColor,
}),
}),
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.error || 'Failed to generate file')
}
const data = await response.json()
setJobId(data.jobId)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
setIsGenerating(false)
}
}
const handleJobComplete = () => {
setIsComplete(true)
setIsGenerating(false)
}
const handleDownload = () => {
if (!jobId) return
window.location.href = `/api/abacus/download/${jobId}`
}
return (
<div
data-component="3d-print-page"
className={css({
maxWidth: '1200px',
mx: 'auto',
p: 6,
})}
>
<h1
className={css({
fontSize: '3xl',
fontWeight: 'bold',
mb: 2,
})}
>
Customize Your 3D Printable Abacus
</h1>
<p className={css({ mb: 6, color: 'gray.600' })}>
Adjust the parameters below to customize your abacus, then generate and download the file
for 3D printing.
</p>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: '1fr 1fr' },
gap: 8,
})}
>
{/* Left column: Controls */}
<div data-section="controls">
<div
className={css({
bg: 'white',
p: 6,
borderRadius: '8px',
boxShadow: 'md',
})}
>
<h2
className={css({
fontSize: 'xl',
fontWeight: 'bold',
mb: 4,
})}
>
Customization Parameters
</h2>
{/* Number of Columns */}
<div data-setting="columns" className={css({ mb: 4 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 2,
})}
>
Number of Columns: {columns}
</label>
<input
type="range"
min="1"
max="13"
step="1"
value={columns}
onChange={(e) => setColumns(Number.parseInt(e.target.value, 10))}
className={css({ width: '100%' })}
/>
<div
className={css({
fontSize: 'sm',
color: 'gray.500',
mt: 1,
})}
>
Total number of columns in the abacus (1-13)
</div>
</div>
{/* Scale Factor */}
<div data-setting="scale-factor" className={css({ mb: 4 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 2,
})}
>
Scale Factor: {scaleFactor.toFixed(1)}x
</label>
<input
type="range"
min="0.5"
max="3"
step="0.1"
value={scaleFactor}
onChange={(e) => setScaleFactor(Number.parseFloat(e.target.value))}
className={css({ width: '100%' })}
/>
<div
className={css({
fontSize: 'sm',
color: 'gray.500',
mt: 1,
})}
>
Overall size multiplier (preserves aspect ratio, larger values = bigger file size)
</div>
</div>
{/* Optional Width in mm */}
<div data-setting="width-mm" className={css({ mb: 4 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 2,
})}
>
Width in mm (optional)
</label>
<input
type="number"
min="50"
max="500"
step="1"
value={widthMm ?? ''}
onChange={(e) => {
const value = e.target.value
setWidthMm(value ? Number.parseFloat(value) : undefined)
}}
placeholder="Leave empty to use scale factor"
className={css({
width: '100%',
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: '4px',
_focus: {
outline: 'none',
borderColor: 'blue.500',
},
})}
/>
<div
className={css({
fontSize: 'sm',
color: 'gray.500',
mt: 1,
})}
>
Specify exact width in millimeters (overrides scale factor)
</div>
</div>
{/* Format Selection */}
<div data-setting="format" className={css({ mb: format === '3mf' ? 4 : 6 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 2,
})}
>
Output Format
</label>
<div className={css({ display: 'flex', gap: 2, flexWrap: 'wrap' })}>
<button
type="button"
onClick={() => setFormat('stl')}
className={css({
px: 4,
py: 2,
borderRadius: '4px',
border: '2px solid',
borderColor: format === 'stl' ? 'blue.600' : 'gray.300',
bg: format === 'stl' ? 'blue.50' : 'white',
color: format === 'stl' ? 'blue.700' : 'gray.700',
cursor: 'pointer',
fontWeight: format === 'stl' ? 'bold' : 'normal',
_hover: { bg: format === 'stl' ? 'blue.100' : 'gray.50' },
})}
>
STL
</button>
<button
type="button"
onClick={() => setFormat('3mf')}
className={css({
px: 4,
py: 2,
borderRadius: '4px',
border: '2px solid',
borderColor: format === '3mf' ? 'blue.600' : 'gray.300',
bg: format === '3mf' ? 'blue.50' : 'white',
color: format === '3mf' ? 'blue.700' : 'gray.700',
cursor: 'pointer',
fontWeight: format === '3mf' ? 'bold' : 'normal',
_hover: { bg: format === '3mf' ? 'blue.100' : 'gray.50' },
})}
>
3MF
</button>
<button
type="button"
onClick={() => setFormat('scad')}
className={css({
px: 4,
py: 2,
borderRadius: '4px',
border: '2px solid',
borderColor: format === 'scad' ? 'blue.600' : 'gray.300',
bg: format === 'scad' ? 'blue.50' : 'white',
color: format === 'scad' ? 'blue.700' : 'gray.700',
cursor: 'pointer',
fontWeight: format === 'scad' ? 'bold' : 'normal',
_hover: { bg: format === 'scad' ? 'blue.100' : 'gray.50' },
})}
>
OpenSCAD
</button>
</div>
</div>
{/* 3MF Color Options */}
{format === '3mf' && (
<div data-section="3mf-colors" className={css({ mb: 6 })}>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
mb: 3,
})}
>
3MF Color Customization
</h3>
{/* Frame Color */}
<div data-setting="frame-color" className={css({ mb: 3 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 1,
})}
>
Frame Color
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
type="color"
value={frameColor}
onChange={(e) => setFrameColor(e.target.value)}
className={css({ width: '60px', height: '40px', cursor: 'pointer' })}
/>
<input
type="text"
value={frameColor}
onChange={(e) => setFrameColor(e.target.value)}
placeholder="#8b7355"
className={css({
flex: 1,
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: '4px',
fontFamily: 'monospace',
})}
/>
</div>
</div>
{/* Heaven Bead Color */}
<div data-setting="heaven-bead-color" className={css({ mb: 3 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 1,
})}
>
Heaven Bead Color
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
type="color"
value={heavenBeadColor}
onChange={(e) => setHeavenBeadColor(e.target.value)}
className={css({ width: '60px', height: '40px', cursor: 'pointer' })}
/>
<input
type="text"
value={heavenBeadColor}
onChange={(e) => setHeavenBeadColor(e.target.value)}
placeholder="#e8d5c4"
className={css({
flex: 1,
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: '4px',
fontFamily: 'monospace',
})}
/>
</div>
</div>
{/* Earth Bead Color */}
<div data-setting="earth-bead-color" className={css({ mb: 3 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 1,
})}
>
Earth Bead Color
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
type="color"
value={earthBeadColor}
onChange={(e) => setEarthBeadColor(e.target.value)}
className={css({ width: '60px', height: '40px', cursor: 'pointer' })}
/>
<input
type="text"
value={earthBeadColor}
onChange={(e) => setEarthBeadColor(e.target.value)}
placeholder="#6b5444"
className={css({
flex: 1,
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: '4px',
fontFamily: 'monospace',
})}
/>
</div>
</div>
{/* Decoration Color */}
<div data-setting="decoration-color" className={css({ mb: 0 })}>
<label
className={css({
display: 'block',
fontWeight: 'medium',
mb: 1,
})}
>
Decoration Color
</label>
<div className={css({ display: 'flex', gap: 2, alignItems: 'center' })}>
<input
type="color"
value={decorationColor}
onChange={(e) => setDecorationColor(e.target.value)}
className={css({ width: '60px', height: '40px', cursor: 'pointer' })}
/>
<input
type="text"
value={decorationColor}
onChange={(e) => setDecorationColor(e.target.value)}
placeholder="#d4af37"
className={css({
flex: 1,
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: '4px',
fontFamily: 'monospace',
})}
/>
</div>
</div>
</div>
)}
{/* Generate Button */}
<button
type="button"
onClick={handleGenerate}
disabled={isGenerating}
data-action="generate"
className={css({
width: '100%',
px: 6,
py: 3,
bg: 'blue.600',
color: 'white',
borderRadius: '4px',
fontWeight: 'bold',
cursor: isGenerating ? 'not-allowed' : 'pointer',
opacity: isGenerating ? 0.6 : 1,
_hover: { bg: isGenerating ? 'blue.600' : 'blue.700' },
})}
>
{isGenerating ? 'Generating...' : 'Generate File'}
</button>
{/* Job Status */}
{jobId && !isComplete && (
<div className={css({ mt: 4 })}>
<JobMonitor jobId={jobId} onComplete={handleJobComplete} />
</div>
)}
{/* Download Button */}
{isComplete && (
<button
type="button"
onClick={handleDownload}
data-action="download"
className={css({
width: '100%',
mt: 4,
px: 6,
py: 3,
bg: 'green.600',
color: 'white',
borderRadius: '4px',
fontWeight: 'bold',
cursor: 'pointer',
_hover: { bg: 'green.700' },
})}
>
Download {format.toUpperCase()}
</button>
)}
{/* Error Message */}
{error && (
<div
data-status="error"
className={css({
mt: 4,
p: 4,
bg: 'red.100',
borderRadius: '4px',
color: 'red.700',
})}
>
{error}
</div>
)}
</div>
</div>
{/* Right column: Preview */}
<div data-section="preview">
<div
className={css({
bg: 'white',
p: 6,
borderRadius: '8px',
boxShadow: 'md',
})}
>
<h2
className={css({
fontSize: 'xl',
fontWeight: 'bold',
mb: 4,
})}
>
Preview
</h2>
<STLPreview columns={columns} scaleFactor={scaleFactor} />
<div
className={css({
mt: 4,
fontSize: 'sm',
color: 'gray.600',
})}
>
<p className={css({ mb: 2 })}>
<strong>Live Preview:</strong> The preview updates automatically as you adjust
parameters (with a 1-second delay). This shows the exact mirrored book-fold design
that will be generated.
</p>
<p className={css({ mb: 2 })}>
<strong>Note:</strong> Preview generation requires OpenSCAD. If you see an error,
the preview feature only works in production (Docker). The download functionality
will still work when deployed.
</p>
<p>Use your mouse to rotate and zoom the 3D model.</p>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,319 @@
'use client'
import { css } from '../../../../../styled-system/css'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { AbacusDisplayDropdown } from '@/components/AbacusDisplayDropdown'
interface CalendarConfigPanelProps {
month: number
year: number
format: 'monthly' | 'daily'
paperSize: 'us-letter' | 'a4' | 'a3' | 'tabloid'
isGenerating: boolean
onMonthChange: (month: number) => void
onYearChange: (year: number) => void
onFormatChange: (format: 'monthly' | 'daily') => void
onPaperSizeChange: (size: 'us-letter' | 'a4' | 'a3' | 'tabloid') => void
onGenerate: () => void
}
const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
]
export function CalendarConfigPanel({
month,
year,
format,
paperSize,
isGenerating,
onMonthChange,
onYearChange,
onFormatChange,
onPaperSizeChange,
onGenerate,
}: CalendarConfigPanelProps) {
const abacusConfig = useAbacusConfig()
return (
<div
data-component="calendar-config-panel"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '1.5rem',
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
})}
>
{/* Format Selection */}
<fieldset
data-section="format-selection"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
Calendar Format
</legend>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
})}
>
<label
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
cursor: 'pointer',
padding: '0.5rem',
borderRadius: '6px',
_hover: { bg: 'gray.700' },
})}
>
<input
type="radio"
value="monthly"
checked={format === 'monthly'}
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
className={css({
cursor: 'pointer',
})}
/>
<span>Monthly Calendar (one page per month)</span>
</label>
<label
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
cursor: 'pointer',
padding: '0.5rem',
borderRadius: '6px',
_hover: { bg: 'gray.700' },
})}
>
<input
type="radio"
value="daily"
checked={format === 'daily'}
onChange={(e) => onFormatChange(e.target.value as 'monthly' | 'daily')}
className={css({
cursor: 'pointer',
})}
/>
<span>Daily Calendar (one page per day)</span>
</label>
</div>
</fieldset>
{/* Date Selection */}
<fieldset
data-section="date-selection"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
Date
</legend>
<div
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
<select
data-element="month-select"
value={month}
onChange={(e) => onMonthChange(Number(e.target.value))}
className={css({
flex: '1',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
cursor: 'pointer',
_hover: { borderColor: 'gray.500' },
})}
>
{MONTHS.map((monthName, index) => (
<option key={monthName} value={index + 1}>
{monthName}
</option>
))}
</select>
<input
type="number"
data-element="year-input"
value={year}
onChange={(e) => onYearChange(Number(e.target.value))}
min={1}
max={9999}
className={css({
width: '100px',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
_hover: { borderColor: 'gray.500' },
})}
/>
</div>
</fieldset>
{/* Paper Size */}
<fieldset
data-section="paper-size"
className={css({
border: 'none',
padding: '0',
margin: '0',
})}
>
<legend
className={css({
fontSize: '1.125rem',
fontWeight: '600',
marginBottom: '0.75rem',
color: 'yellow.400',
})}
>
Paper Size
</legend>
<select
data-element="paper-size-select"
value={paperSize}
onChange={(e) =>
onPaperSizeChange(e.target.value as 'us-letter' | 'a4' | 'a3' | 'tabloid')
}
className={css({
width: '100%',
padding: '0.5rem',
borderRadius: '6px',
bg: 'gray.700',
color: 'white',
border: '1px solid',
borderColor: 'gray.600',
cursor: 'pointer',
_hover: { borderColor: 'gray.500' },
})}
>
<option value="us-letter">US Letter (8.5" × 11")</option>
<option value="a4">A4 (210mm × 297mm)</option>
<option value="a3">A3 (297mm × 420mm)</option>
<option value="tabloid">Tabloid (11" × 17")</option>
</select>
</fieldset>
{/* Abacus Styling */}
<div
data-section="styling-info"
className={css({
padding: '1rem',
bg: 'gray.700',
borderRadius: '8px',
})}
>
<p
className={css({
fontSize: '0.875rem',
marginBottom: '0.75rem',
color: 'gray.300',
})}
>
Calendar abacus style preview:
</p>
<div
className={css({
display: 'flex',
justifyContent: 'center',
marginBottom: '0.75rem',
})}
>
<AbacusReact
value={12}
columns={2}
customStyles={abacusConfig.customStyles}
scaleFactor={0.5}
showNumbers={false}
/>
</div>
<div
className={css({
display: 'flex',
justifyContent: 'center',
})}
>
<AbacusDisplayDropdown />
</div>
</div>
{/* Generate Button */}
<button
type="button"
data-action="generate-calendar"
onClick={onGenerate}
disabled={isGenerating}
className={css({
padding: '1rem',
bg: 'yellow.500',
color: 'gray.900',
fontWeight: '600',
fontSize: '1.125rem',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
bg: 'yellow.400',
},
_disabled: {
bg: 'gray.600',
color: 'gray.400',
cursor: 'not-allowed',
},
})}
>
{isGenerating ? 'Generating PDF...' : 'Generate PDF Calendar'}
</button>
</div>
)
}

View File

@@ -0,0 +1,131 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import { css } from '../../../../../styled-system/css'
interface CalendarPreviewProps {
month: number
year: number
format: 'monthly' | 'daily'
previewSvg: string | null
}
async function fetchTypstPreview(month: number, year: number, format: string): Promise<string | null> {
const response = await fetch('/api/create/calendar/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ month, year, format }),
})
if (!response.ok) {
throw new Error('Failed to fetch preview')
}
const data = await response.json()
return data.svg
}
export function CalendarPreview({ month, year, format, previewSvg }: CalendarPreviewProps) {
// Use React Query to fetch Typst-generated preview (client-side only)
const { data: typstPreviewSvg, isLoading } = useQuery({
queryKey: ['calendar-typst-preview', month, year, format],
queryFn: () => fetchTypstPreview(month, year, format),
enabled: typeof window !== 'undefined' && format === 'monthly', // Only run on client and for monthly format
})
// Use generated PDF SVG if available, otherwise use Typst live preview
const displaySvg = previewSvg || typstPreviewSvg
// Show loading state while fetching preview
if (isLoading || (!displaySvg && format === 'monthly')) {
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '600px',
})}
>
<p
className={css({
fontSize: '1.25rem',
color: 'gray.400',
textAlign: 'center',
})}
>
Loading preview...
</p>
</div>
)
}
if (!displaySvg) {
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '600px',
})}
>
<p
className={css({
fontSize: '1.25rem',
color: 'gray.400',
textAlign: 'center',
})}
>
{format === 'daily' ? 'Daily format - preview after generation' : 'No preview available'}
</p>
</div>
)
}
return (
<div
data-component="calendar-preview"
className={css({
bg: 'gray.800',
borderRadius: '12px',
padding: '2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
})}
>
<p
className={css({
fontSize: '1.125rem',
color: 'yellow.400',
marginBottom: '1rem',
textAlign: 'center',
fontWeight: 'bold',
})}
>
{previewSvg ? 'Generated PDF' : 'Live Preview'}
</p>
<div
className={css({
bg: 'white',
borderRadius: '8px',
padding: '1rem',
maxWidth: '100%',
overflow: 'auto',
})}
dangerouslySetInnerHTML={{ __html: displaySvg }}
/>
</div>
)
}

View File

@@ -0,0 +1,137 @@
'use client'
import { useState } from 'react'
import { css } from '../../../../styled-system/css'
import { useAbacusConfig } from '@soroban/abacus-react'
import { PageWithNav } from '@/components/PageWithNav'
import { CalendarConfigPanel } from './components/CalendarConfigPanel'
import { CalendarPreview } from './components/CalendarPreview'
export default function CalendarCreatorPage() {
const currentDate = new Date()
const abacusConfig = useAbacusConfig()
const [month, setMonth] = useState(currentDate.getMonth() + 1) // 1-12
const [year, setYear] = useState(currentDate.getFullYear())
const [format, setFormat] = useState<'monthly' | 'daily'>('monthly')
const [paperSize, setPaperSize] = useState<'us-letter' | 'a4' | 'a3' | 'tabloid'>('us-letter')
const [isGenerating, setIsGenerating] = useState(false)
const [previewSvg, setPreviewSvg] = useState<string | null>(null)
const handleGenerate = async () => {
setIsGenerating(true)
try {
const response = await fetch('/api/create/calendar/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
month,
year,
format,
paperSize,
abacusConfig,
}),
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.message || 'Failed to generate calendar')
}
const data = await response.json()
// Convert base64 PDF to blob and trigger download
const pdfBytes = Uint8Array.from(atob(data.pdf), c => c.charCodeAt(0))
const blob = new Blob([pdfBytes], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = data.filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (error) {
console.error('Error generating calendar:', error)
alert(`Failed to generate calendar: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setIsGenerating(false)
}
}
return (
<PageWithNav navTitle="Create" navEmoji="📅">
<div
data-component="calendar-creator"
className={css({
minHeight: '100vh',
bg: 'gray.900',
color: 'white',
padding: '2rem',
})}
>
<div
className={css({
maxWidth: '1400px',
margin: '0 auto',
})}
>
{/* Header */}
<header
data-section="page-header"
className={css({
textAlign: 'center',
marginBottom: '3rem',
})}
>
<h1
className={css({
fontSize: '2.5rem',
fontWeight: 'bold',
marginBottom: '0.5rem',
color: 'yellow.400',
})}
>
Create Abacus Calendar
</h1>
<p
className={css({
fontSize: '1.125rem',
color: 'gray.300',
})}
>
Generate printable calendars with abacus date numbers
</p>
</header>
{/* Main Content */}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', lg: '350px 1fr' },
gap: '2rem',
})}
>
{/* Configuration Panel */}
<CalendarConfigPanel
month={month}
year={year}
format={format}
paperSize={paperSize}
isGenerating={isGenerating}
onMonthChange={setMonth}
onYearChange={setYear}
onFormatChange={setFormat}
onPaperSizeChange={setPaperSize}
onGenerate={handleGenerate}
/>
{/* Preview */}
<CalendarPreview month={month} year={year} format={format} previewSvg={previewSvg} />
</div>
</div>
</div>
</PageWithNav>
)
}

View File

@@ -0,0 +1,411 @@
'use client'
import { useAbacusConfig } from '@soroban/abacus-react'
import { useForm } from '@tanstack/react-form'
import { useState } from 'react'
import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationFormWithoutGenerate'
import { GenerationProgress } from '@/components/GenerationProgress'
import { LivePreview } from '@/components/LivePreview'
import { PageWithNav } from '@/components/PageWithNav'
import { StyleControls } from '@/components/StyleControls'
import { css } from '../../../../styled-system/css'
import { container, grid, hstack, stack } from '../../../../styled-system/patterns'
// Complete, validated configuration ready for generation
export interface FlashcardConfig {
range: string
step?: number
cardsPerPage?: number
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
orientation?: 'portrait' | 'landscape'
margins?: {
top?: string
bottom?: string
left?: string
right?: string
}
gutter?: string
shuffle?: boolean
seed?: number
showCutMarks?: boolean
showRegistration?: boolean
fontFamily?: string
fontSize?: string
columns?: string | number
showEmptyColumns?: boolean
hideInactiveBeads?: boolean
beadShape?: 'diamond' | 'circle' | 'square'
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
coloredNumerals?: boolean
scaleFactor?: number
format?: 'pdf' | 'html' | 'png' | 'svg'
}
// Partial form state during editing (may have undefined values)
export interface FlashcardFormState {
range?: string
step?: number
cardsPerPage?: number
paperSize?: 'us-letter' | 'a4' | 'a3' | 'a5'
orientation?: 'portrait' | 'landscape'
margins?: {
top?: string
bottom?: string
left?: string
right?: string
}
gutter?: string
shuffle?: boolean
seed?: number
showCutMarks?: boolean
showRegistration?: boolean
fontFamily?: string
fontSize?: string
columns?: string | number
showEmptyColumns?: boolean
hideInactiveBeads?: boolean
beadShape?: 'diamond' | 'circle' | 'square'
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
coloredNumerals?: boolean
scaleFactor?: number
format?: 'pdf' | 'html' | 'png' | 'svg'
}
// Validation function to convert form state to complete config
function validateAndCompleteConfig(formState: FlashcardFormState): FlashcardConfig {
return {
// Required fields with defaults
range: formState.range || '0-99',
// Optional fields with defaults
step: formState.step ?? 1,
cardsPerPage: formState.cardsPerPage ?? 6,
paperSize: formState.paperSize ?? 'us-letter',
orientation: formState.orientation ?? 'portrait',
gutter: formState.gutter ?? '5mm',
shuffle: formState.shuffle ?? false,
seed: formState.seed,
showCutMarks: formState.showCutMarks ?? false,
showRegistration: formState.showRegistration ?? false,
fontFamily: formState.fontFamily ?? 'DejaVu Sans',
fontSize: formState.fontSize ?? '48pt',
columns: formState.columns ?? 'auto',
showEmptyColumns: formState.showEmptyColumns ?? false,
hideInactiveBeads: formState.hideInactiveBeads ?? false,
beadShape: formState.beadShape ?? 'diamond',
colorScheme: formState.colorScheme ?? 'place-value',
coloredNumerals: formState.coloredNumerals ?? false,
scaleFactor: formState.scaleFactor ?? 0.9,
format: formState.format ?? 'pdf',
margins: formState.margins,
}
}
type GenerationStatus = 'idle' | 'generating' | 'error'
export default function CreatePage() {
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
const [error, setError] = useState<string | null>(null)
const globalConfig = useAbacusConfig()
const form = useForm<FlashcardFormState>({
defaultValues: {
range: '0-99',
step: 1,
cardsPerPage: 6,
paperSize: 'us-letter',
orientation: 'portrait',
gutter: '5mm',
shuffle: false,
showCutMarks: false,
showRegistration: false,
fontFamily: 'DejaVu Sans',
fontSize: '48pt',
columns: 'auto',
showEmptyColumns: false,
// Use global config for abacus display settings
hideInactiveBeads: globalConfig.hideInactiveBeads,
beadShape: globalConfig.beadShape,
colorScheme: globalConfig.colorScheme,
coloredNumerals: globalConfig.coloredNumerals,
scaleFactor: globalConfig.scaleFactor,
format: 'pdf',
},
})
const handleGenerate = async (formState: FlashcardFormState) => {
setGenerationStatus('generating')
setError(null)
try {
// Validate and complete the configuration
const config = validateAndCompleteConfig(formState)
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
})
if (!response.ok) {
// Handle error response (should be JSON)
const errorResult = await response.json()
throw new Error(errorResult.error || 'Generation failed')
}
// Success - response is binary PDF data, trigger download
const blob = await response.blob()
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
// Create download link and trigger download
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
setGenerationStatus('idle') // Reset to idle after successful download
} catch (err) {
console.error('Generation error:', err)
setError(err instanceof Error ? err.message : 'Unknown error occurred')
setGenerationStatus('error')
}
}
const handleNewGeneration = () => {
setGenerationStatus('idle')
setError(null)
}
return (
<PageWithNav navTitle="Create Flashcards" navEmoji="✨">
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
{/* Main Content */}
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
<div className={stack({ gap: '6', mb: '8' })}>
<div className={stack({ gap: '2', textAlign: 'center' })}>
<h1
className={css({
fontSize: '3xl',
fontWeight: 'bold',
color: 'gray.900',
})}
>
Create Your Flashcards
</h1>
<p
className={css({
fontSize: 'lg',
color: 'gray.600',
})}
>
Configure content and style, preview instantly, then generate your flashcards
</p>
</div>
</div>
{/* Configuration Interface */}
<div
className={grid({
columns: { base: 1, lg: 3 },
gap: '8',
alignItems: 'start',
})}
>
{/* Main Configuration Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '8',
})}
>
<ConfigurationFormWithoutGenerate form={form} />
</div>
{/* Style Controls Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
})}
>
<div className={stack({ gap: '4' })}>
<div className={stack({ gap: '1' })}>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
})}
>
🎨 Visual Style
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
See changes instantly in the preview
</p>
</div>
<form.Subscribe
selector={(state) => state}
children={(_state) => <StyleControls form={form} />}
/>
</div>
</div>
{/* Live Preview Panel */}
<div
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
})}
>
<div className={stack({ gap: '6' })}>
<form.Subscribe
selector={(state) => state}
children={(state) => <LivePreview config={state.values} />}
/>
{/* Generate Button within Preview */}
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
pt: '6',
})}
>
{/* Generation Status */}
{generationStatus === 'generating' && (
<div className={css({ mb: '4' })}>
<GenerationProgress config={form.state.values} />
</div>
)}
<button
onClick={() => handleGenerate(form.state.values)}
disabled={generationStatus === 'generating'}
className={css({
w: 'full',
px: '6',
py: '4',
bg: 'brand.600',
color: 'white',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
cursor: generationStatus === 'generating' ? 'not-allowed' : 'pointer',
opacity: generationStatus === 'generating' ? '0.7' : '1',
_hover:
generationStatus === 'generating'
? {}
: {
bg: 'brand.700',
transform: 'translateY(-1px)',
shadow: 'modal',
},
})}
>
<span className={hstack({ gap: '3', justify: 'center' })}>
{generationStatus === 'generating' ? (
<>
<div
className={css({
w: '5',
h: '5',
border: '2px solid',
borderColor: 'white',
borderTopColor: 'transparent',
rounded: 'full',
animation: 'spin 1s linear infinite',
})}
/>
Generating Your Flashcards...
</>
) : (
<>
<div className={css({ fontSize: 'xl' })}></div>
Generate Flashcards
</>
)}
</span>
</button>
</div>
</div>
</div>
</div>
{/* Error Display - moved to global level */}
{generationStatus === 'error' && error && (
<div
className={css({
bg: 'red.50',
border: '1px solid',
borderColor: 'red.200',
rounded: '2xl',
p: '8',
mt: '8',
})}
>
<div className={stack({ gap: '4' })}>
<div className={hstack({ gap: '3', alignItems: 'center' })}>
<div className={css({ fontSize: '2xl' })}></div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'semibold',
color: 'red.800',
})}
>
Generation Failed
</h3>
</div>
<p
className={css({
color: 'red.700',
lineHeight: 'relaxed',
})}
>
{error}
</p>
<button
onClick={handleNewGeneration}
className={css({
alignSelf: 'start',
px: '4',
py: '2',
bg: 'red.600',
color: 'white',
fontWeight: 'medium',
rounded: 'lg',
transition: 'all',
_hover: { bg: 'red.700' },
})}
>
Try Again
</button>
</div>
</div>
)}
</div>
</div>
</PageWithNav>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -45,3 +45,13 @@ body {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}

View File

@@ -1,34 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle for better visibility -->
<circle cx="50" cy="50" r="48" fill="#fef3c7"/>
<!-- Abacus from @soroban/abacus-react -->
<g transform="translate(32, 8) scale(0.36)">
<div class="abacus-container" style="display:inline-block;text-align:center;position:relative"><svg width="25" height="120" viewBox="0 0 25 120" class="abacus-svg " style="overflow:visible;display:block"><defs><style>
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1 !important;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0 !important;
}
</style></defs><rect x="11" y="0" width="3" height="120" fill="#7c2d12" stroke="#92400e" stroke-width="2" opacity="1"></rect><rect x="6.5" y="30" width="12" height="2" fill="#92400e" stroke="#92400e" stroke-width="3" opacity="1"></rect><g class="abacus-bead active " transform="translate(4.100000000000001, 17)" style="cursor:default;touch-action:none;transition:opacity 0.2s ease-in-out"><polygon points="8.399999999999999,0 16.799999999999997,6 8.399999999999999,12 0,6" fill="#fbbf24" stroke="#000" stroke-width="0.5"></polygon></g><g class="abacus-bead inactive " transform="translate(4.100000000000001, 40)" style="cursor:default;touch-action:none;transition:opacity 0.2s ease-in-out"><polygon points="8.399999999999999,0 16.799999999999997,6 8.399999999999999,12 0,6" fill="#fbbf24" stroke="#000" stroke-width="0.5"></polygon></g><g class="abacus-bead inactive " transform="translate(4.100000000000001, 52.5)" style="cursor:default;touch-action:none;transition:opacity 0.2s ease-in-out"><polygon points="8.399999999999999,0 16.799999999999997,6 8.399999999999999,12 0,6" fill="#fbbf24" stroke="#000" stroke-width="0.5"></polygon></g><g class="abacus-bead inactive " transform="translate(4.100000000000001, 65)" style="cursor:default;touch-action:none;transition:opacity 0.2s ease-in-out"><polygon points="8.399999999999999,0 16.799999999999997,6 8.399999999999999,12 0,6" fill="#fbbf24" stroke="#000" stroke-width="0.5"></polygon></g><g class="abacus-bead inactive " transform="translate(4.100000000000001, 77.5)" style="cursor:default;touch-action:none;transition:opacity 0.2s ease-in-out"><polygon points="8.399999999999999,0 16.799999999999997,6 8.399999999999999,12 0,6" fill="#fbbf24" stroke="#000" stroke-width="0.5"></polygon></g><rect x="0" y="0" width="25" height="120" fill="transparent" stroke="none" style="cursor:default;pointer-events:none"></rect></svg></div>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,55 @@
import { execSync } from 'child_process'
import { join } from 'path'
export const runtime = 'nodejs'
// In-memory cache: { day: svg }
const iconCache = new Map<number, string>()
// Get current day of month in US Central Time
function getDayOfMonth(): number {
const now = new Date()
// Get date in America/Chicago timezone
const centralDate = new Date(now.toLocaleString('en-US', { timeZone: 'America/Chicago' }))
return centralDate.getDate()
}
// Generate icon by calling script that uses react-dom/server
function generateDayIcon(day: number): string {
// Call the generation script as a subprocess
// Scripts can use react-dom/server, route handlers cannot
const scriptPath = join(process.cwd(), 'scripts', 'generateDayIcon.tsx')
const svg = execSync(`npx tsx "${scriptPath}" ${day}`, {
encoding: 'utf-8',
cwd: process.cwd(),
})
return svg
}
export async function GET() {
const dayOfMonth = getDayOfMonth()
// Check cache first
let svg = iconCache.get(dayOfMonth)
if (!svg) {
// Generate and cache
svg = generateDayIcon(dayOfMonth)
iconCache.set(dayOfMonth, svg)
// Clear old cache entries (keep only current day)
for (const [cachedDay] of iconCache) {
if (cachedDay !== dayOfMonth) {
iconCache.delete(cachedDay)
}
}
}
return new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
// Cache for 1 hour so it updates throughout the day
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
},
})
}

View File

@@ -52,7 +52,7 @@ export const metadata: Metadata = {
icons: {
icon: [
{ url: '/favicon.ico', sizes: 'any' },
{ url: '/icon.svg', type: 'image/svg+xml' },
{ url: '/icon', type: 'image/svg+xml' },
],
apple: '/apple-touch-icon.png',
},

View File

@@ -1,7 +1,10 @@
import { ImageResponse } from 'next/og'
import { readFileSync } from 'fs'
import { join } from 'path'
// Route segment config
export const runtime = 'edge'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
// Image metadata
export const alt = 'Abaci.One - Interactive Soroban Learning Platform'
@@ -11,10 +14,30 @@ export const size = {
}
export const contentType = 'image/png'
// Extract just the abacus SVG content from the pre-generated og-image.svg
// This SVG is generated by scripts/generateAbacusIcons.tsx using AbacusReact
function getAbacusSVGContent(): string {
const svgPath = join(process.cwd(), 'public', 'og-image.svg')
const svgContent = readFileSync(svgPath, 'utf-8')
// Extract just the abacus <g> element (contains the AbacusReact output)
const abacusMatch = svgContent.match(
/<!-- Left side - Abacus from @soroban\/abacus-react -->\s*<g[^>]*>([\s\S]*?)<\/g>/
)
if (!abacusMatch) {
throw new Error('Could not extract abacus content from og-image.svg')
}
return abacusMatch[0] // Return the full <g>...</g> block with AbacusReact output
}
// Image generation
// Note: Using simplified abacus HTML/CSS representation instead of StaticAbacus
// because ImageResponse has limited JSX support (no custom components)
// Note: Uses pre-generated SVG from og-image.svg which is rendered by AbacusReact
// This avoids importing react-dom/server in this file (Next.js restriction)
export default async function Image() {
const abacusSVG = getAbacusSVGContent()
return new ImageResponse(
<div
style={{
@@ -27,154 +50,16 @@ export default async function Image() {
padding: '80px',
}}
>
{/* Left side - Simplified abacus visualization (HTML/CSS)
Can't use StaticAbacus here because ImageResponse only supports
basic HTML elements, not custom React components */}
{/* Left side - Abacus from pre-generated og-image.svg (AbacusReact output) */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '40%',
}}
>
{/* Simple abacus representation with 3 columns */}
<div
style={{
display: 'flex',
gap: '30px',
}}
>
{/* Column 1 */}
<div
style={{
width: '80px',
height: '400px',
background: '#7c2d12',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around',
padding: '20px 0',
position: 'relative',
}}
>
{/* Reckoning bar */}
<div
style={{
position: 'absolute',
top: '50%',
left: '-10px',
right: '-10px',
height: '12px',
background: '#92400e',
borderRadius: '4px',
}}
/>
{/* Beads - simplified representation */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{[...Array(2)].map((_, i) => (
<div
key={i}
style={{
width: '36px',
height: '36px',
background: '#fbbf24',
borderRadius: '50%',
border: '3px solid #92400e',
}}
/>
))}
</div>
</div>
{/* Column 2 */}
<div
style={{
width: '80px',
height: '400px',
background: '#7c2d12',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around',
padding: '20px 0',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: '50%',
left: '-10px',
right: '-10px',
height: '12px',
background: '#92400e',
borderRadius: '4px',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{[...Array(2)].map((_, i) => (
<div
key={i}
style={{
width: '36px',
height: '36px',
background: '#fbbf24',
borderRadius: '50%',
border: '3px solid #92400e',
}}
/>
))}
</div>
</div>
{/* Column 3 */}
<div
style={{
width: '80px',
height: '400px',
background: '#7c2d12',
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-around',
padding: '20px 0',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: '50%',
left: '-10px',
right: '-10px',
height: '12px',
background: '#92400e',
borderRadius: '4px',
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{[...Array(2)].map((_, i) => (
<div
key={i}
style={{
width: '36px',
height: '36px',
background: '#fbbf24',
borderRadius: '50%',
border: '3px solid #92400e',
}}
/>
))}
</div>
</div>
</div>
</div>
dangerouslySetInnerHTML={{
__html: abacusSVG,
}}
/>
{/* Right side - Text content */}
<div

View File

@@ -1,11 +1,10 @@
'use client'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import { useTranslations, useMessages } from 'next-intl'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { HeroAbacus } from '@/components/HeroAbacus'
import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
import { useHomeHero } from '@/contexts/HomeHeroContext'
import { PageWithNav } from '@/components/PageWithNav'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
@@ -15,6 +14,135 @@ import { LevelSliderDisplay } from '@/components/LevelSliderDisplay'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
// Hero section placeholder - the actual abacus is rendered by MyAbacus component
function HeroSection() {
const { subtitle, setIsHeroVisible, isSubtitleLoaded } = useHomeHero()
const heroRef = useRef<HTMLDivElement>(null)
// Detect when hero scrolls out of view
useEffect(() => {
if (!heroRef.current) return
const observer = new IntersectionObserver(
([entry]) => {
setIsHeroVisible(entry.intersectionRatio > 0.2)
},
{
threshold: [0, 0.2, 0.5, 1],
}
)
observer.observe(heroRef.current)
return () => observer.disconnect()
}, [setIsHeroVisible])
return (
<div
ref={heroRef}
className={css({
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
bg: 'gray.900',
position: 'relative',
overflow: 'hidden',
px: '4',
py: '12',
})}
>
{/* Background pattern */}
<div
className={css({
position: 'absolute',
inset: 0,
opacity: 0.1,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
})}
/>
{/* Title and Subtitle */}
<div
className={css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
zIndex: 10,
})}
>
<h1
className={css({
fontSize: { base: '4xl', md: '6xl', lg: '7xl' },
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Abaci One
</h1>
<p
className={css({
fontSize: { base: 'xl', md: '2xl' },
fontWeight: 'medium',
color: 'purple.300',
fontStyle: 'italic',
marginBottom: '8',
opacity: isSubtitleLoaded ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
})}
>
{subtitle.text}
</p>
</div>
{/* Space for abacus - rendered by MyAbacus component in hero mode */}
<div className={css({ flex: 1 })} />
{/* Scroll hint */}
<div
className={css({
position: 'relative',
fontSize: 'sm',
color: 'gray.400',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
animation: 'bounce 2s ease-in-out infinite',
zIndex: 10,
})}
>
<span>Scroll to explore</span>
<span></span>
</div>
{/* Keyframes for bounce animation */}
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes bounce {
0%, 100% {
transform: translateY(0);
opacity: 0.7;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
}
`,
}}
/>
</div>
)
}
// Mini abacus that cycles through a sequence of values
function MiniAbacus({
values,
@@ -119,14 +247,246 @@ export default function HomePage() {
const selectedTutorial = skillTutorials[selectedSkillIndex]
return (
<HomeHeroProvider>
<PageWithNav>
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero Section with Large Interactive Abacus */}
<HeroAbacus />
<PageWithNav>
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero Section - abacus rendered by MyAbacus in hero mode */}
<HeroSection />
{/* Learn by Doing Section - with inline tutorial demo */}
<section className={stack({ gap: '8', mb: '16', px: '4', py: '12' })}>
{/* Learn by Doing Section - with inline tutorial demo */}
<section className={stack({ gap: '8', mb: '16', px: '4', py: '12' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('learnByDoing.title')}
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
{t('learnByDoing.subtitle')}
</p>
</div>
{/* Live demo and learning objectives */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
minW: { base: '100%', xl: '1400px' },
mx: 'auto',
})}
>
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', xl: 'row' },
gap: '8',
alignItems: { base: 'center', xl: 'flex-start' },
})}
>
{/* Tutorial on the left */}
<div
className={css({
flex: '1',
minW: { base: '100%', xl: '500px' },
maxW: { base: '100%', xl: '500px' },
})}
>
<TutorialPlayer
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
</div>
{/* What you'll learn on the right */}
<div
className={css({
flex: '0 0 auto',
w: { base: '100%', lg: '800px' },
})}
>
<h3
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: 'white',
mb: '6',
})}
>
{t('whatYouLearn.title')}
</h3>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
{[
{
title: t('skills.readNumbers.title'),
desc: t('skills.readNumbers.desc'),
example: t('skills.readNumbers.example'),
badge: t('skills.readNumbers.badge'),
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3,
},
{
title: t('skills.friends.title'),
desc: t('skills.friends.desc'),
example: t('skills.friends.example'),
badge: t('skills.friends.badge'),
values: [2, 5, 3],
columns: 1,
},
{
title: t('skills.multiply.title'),
desc: t('skills.multiply.desc'),
example: t('skills.multiply.example'),
badge: t('skills.multiply.badge'),
values: [12, 24, 36, 48],
columns: 2,
},
{
title: t('skills.mental.title'),
desc: t('skills.mental.desc'),
example: t('skills.mental.example'),
badge: t('skills.mental.badge'),
values: [7, 14, 21, 28, 35],
columns: 2,
},
].map((skill, i) => {
const isSelected = i === selectedSkillIndex
return (
<div
key={i}
onClick={() => setSelectedSkillIndex(i)}
className={css({
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
borderRadius: 'xl',
p: { base: '4', lg: '5' },
border: '1px solid',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.4)'
: 'rgba(255, 255, 255, 0.15)',
boxShadow: isSelected
? '0 6px 16px rgba(250, 204, 21, 0.2)'
: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s',
cursor: 'pointer',
_hover: {
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.5)'
: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: isSelected
? '0 8px 20px rgba(250, 204, 21, 0.3)'
: '0 6px 16px rgba(0, 0, 0, 0.4)',
},
})}
>
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
<div
className={css({
fontSize: '3xl',
width: { base: '120px', lg: '150px' },
minHeight: { base: '115px', lg: '140px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bg: isSelected
? 'rgba(250, 204, 21, 0.15)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
<MiniAbacus values={skill.values} columns={skill.columns} />
</div>
<div className={stack({ gap: '2', flex: '1', minWidth: '0' })}>
<div
className={hstack({
gap: '2',
alignItems: 'center',
flexWrap: 'wrap',
})}
>
<div
className={css({
color: 'white',
fontSize: 'md',
fontWeight: 'bold',
})}
>
{skill.title}
</div>
<div
className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400',
fontSize: '2xs',
fontWeight: 'semibold',
px: '2',
py: '0.5',
borderRadius: 'md',
})}
>
{skill.badge}
</div>
</div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div>
<div
className={css({
color: 'yellow.400',
fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2',
py: '1',
borderRadius: 'md',
w: 'fit-content',
})}
>
{skill.example}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
</section>
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Current Offerings Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
@@ -136,397 +496,161 @@ export default function HomePage() {
mb: '2',
})}
>
{t('learnByDoing.title')}
{t('arcade.title')}
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>{t('arcade.subtitle')}</p>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
{getAvailableGames().map((game) => {
const playersText =
game.manifest.maxPlayers === 1
? t('arcade.soloChallenge')
: t('arcade.playersCount', { min: 1, max: game.manifest.maxPlayers })
return (
<GameCard
key={game.manifest.name}
icon={game.manifest.icon}
title={game.manifest.displayName}
description={game.manifest.description}
players={playersText}
tags={game.manifest.chips}
gradient={game.manifest.gradient}
href="/games"
/>
)
})}
</div>
</section>
{/* Progression Visualization */}
<section className={stack({ gap: '6', mb: '16', overflow: 'hidden' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('journey.title')}
</h2>
<p style={{ color: '#e5e7eb', fontSize: '16px' }}>{t('journey.subtitle')}</p>
</div>
<LevelSliderDisplay />
</section>
{/* Flashcard Generator Section */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('flashcards.title')}
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
{t('learnByDoing.subtitle')}
{t('flashcards.subtitle')}
</p>
</div>
{/* Live demo and learning objectives */}
{/* Combined interactive display and CTA */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
p: { base: '6', md: '8' },
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
minW: { base: '100%', xl: '1400px' },
maxW: '1200px',
mx: 'auto',
})}
>
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', xl: 'row' },
gap: '8',
alignItems: { base: 'center', xl: 'flex-start' },
})}
>
{/* Tutorial on the left */}
<div
className={css({
flex: '1',
minW: { base: '100%', xl: '500px' },
maxW: { base: '100%', xl: '500px' },
})}
>
<TutorialPlayer
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
</div>
{/* Interactive Flashcards Display */}
<div className={css({ mb: '8' })}>
<InteractiveFlashcards />
</div>
{/* What you'll learn on the right */}
<div
className={css({
flex: '0 0 auto',
w: { base: '100%', lg: '800px' },
})}
>
<h3
{/* Features */}
<div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}>
{[
{
icon: t('flashcards.features.formats.icon'),
title: t('flashcards.features.formats.title'),
desc: t('flashcards.features.formats.desc'),
},
{
icon: t('flashcards.features.customizable.icon'),
title: t('flashcards.features.customizable.title'),
desc: t('flashcards.features.customizable.desc'),
},
{
icon: t('flashcards.features.paperSizes.icon'),
title: t('flashcards.features.paperSizes.title'),
desc: t('flashcards.features.paperSizes.desc'),
},
].map((feature, i) => (
<div
key={i}
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: 'white',
mb: '6',
textAlign: 'center',
p: '4',
rounded: 'lg',
bg: 'rgba(255, 255, 255, 0.05)',
})}
>
{t('whatYouLearn.title')}
</h3>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
{[
{
title: t('skills.readNumbers.title'),
desc: t('skills.readNumbers.desc'),
example: t('skills.readNumbers.example'),
badge: t('skills.readNumbers.badge'),
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3,
},
{
title: t('skills.friends.title'),
desc: t('skills.friends.desc'),
example: t('skills.friends.example'),
badge: t('skills.friends.badge'),
values: [2, 5, 3],
columns: 1,
},
{
title: t('skills.multiply.title'),
desc: t('skills.multiply.desc'),
example: t('skills.multiply.example'),
badge: t('skills.multiply.badge'),
values: [12, 24, 36, 48],
columns: 2,
},
{
title: t('skills.mental.title'),
desc: t('skills.mental.desc'),
example: t('skills.mental.example'),
badge: t('skills.mental.badge'),
values: [7, 14, 21, 28, 35],
columns: 2,
},
].map((skill, i) => {
const isSelected = i === selectedSkillIndex
return (
<div
key={i}
onClick={() => setSelectedSkillIndex(i)}
className={css({
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
borderRadius: 'xl',
p: { base: '4', lg: '5' },
border: '1px solid',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.4)'
: 'rgba(255, 255, 255, 0.15)',
boxShadow: isSelected
? '0 6px 16px rgba(250, 204, 21, 0.2)'
: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s',
cursor: 'pointer',
_hover: {
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.5)'
: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: isSelected
? '0 8px 20px rgba(250, 204, 21, 0.3)'
: '0 6px 16px rgba(0, 0, 0, 0.4)',
},
})}
>
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
<div
className={css({
fontSize: '3xl',
width: { base: '120px', lg: '150px' },
minHeight: { base: '115px', lg: '140px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bg: isSelected
? 'rgba(250, 204, 21, 0.15)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
<MiniAbacus values={skill.values} columns={skill.columns} />
</div>
<div className={stack({ gap: '2', flex: '1', minWidth: '0' })}>
<div
className={hstack({
gap: '2',
alignItems: 'center',
flexWrap: 'wrap',
})}
>
<div
className={css({
color: 'white',
fontSize: 'md',
fontWeight: 'bold',
})}
>
{skill.title}
</div>
<div
className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400',
fontSize: '2xs',
fontWeight: 'semibold',
px: '2',
py: '0.5',
borderRadius: 'md',
})}
>
{skill.badge}
</div>
</div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div>
<div
className={css({
color: 'yellow.400',
fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2',
py: '1',
borderRadius: 'md',
w: 'fit-content',
})}
>
{skill.example}
</div>
</div>
</div>
</div>
)
})}
<div className={css({ fontSize: '2xl', mb: '2' })}>{feature.icon}</div>
<div
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'white',
mb: '1',
})}
>
{feature.title}
</div>
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>{feature.desc}</div>
</div>
</div>
))}
</div>
{/* CTA Button */}
<div className={css({ textAlign: 'center' })}>
<Link
href="/create"
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '2',
bg: 'blue.600',
color: 'white',
px: '6',
py: '3',
rounded: 'lg',
fontWeight: 'semibold',
transition: 'all 0.2s',
_hover: {
bg: 'blue.500',
},
})}
>
<span>{t('flashcards.cta')}</span>
<span></span>
</Link>
</div>
</div>
</section>
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Current Offerings Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('arcade.title')}
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>{t('arcade.subtitle')}</p>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
{getAvailableGames().map((game) => {
const playersText =
game.manifest.maxPlayers === 1
? t('arcade.soloChallenge')
: t('arcade.playersCount', { min: 1, max: game.manifest.maxPlayers })
return (
<GameCard
key={game.manifest.name}
icon={game.manifest.icon}
title={game.manifest.displayName}
description={game.manifest.description}
players={playersText}
tags={game.manifest.chips}
gradient={game.manifest.gradient}
href="/games"
/>
)
})}
</div>
</section>
{/* Progression Visualization */}
<section className={stack({ gap: '6', mb: '16', overflow: 'hidden' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('journey.title')}
</h2>
<p style={{ color: '#e5e7eb', fontSize: '16px' }}>{t('journey.subtitle')}</p>
</div>
<LevelSliderDisplay />
</section>
{/* Flashcard Generator Section */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
{t('flashcards.title')}
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
{t('flashcards.subtitle')}
</p>
</div>
{/* Combined interactive display and CTA */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: { base: '6', md: '8' },
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '1200px',
mx: 'auto',
})}
>
{/* Interactive Flashcards Display */}
<div className={css({ mb: '8' })}>
<InteractiveFlashcards />
</div>
{/* Features */}
<div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}>
{[
{
icon: t('flashcards.features.formats.icon'),
title: t('flashcards.features.formats.title'),
desc: t('flashcards.features.formats.desc'),
},
{
icon: t('flashcards.features.customizable.icon'),
title: t('flashcards.features.customizable.title'),
desc: t('flashcards.features.customizable.desc'),
},
{
icon: t('flashcards.features.paperSizes.icon'),
title: t('flashcards.features.paperSizes.title'),
desc: t('flashcards.features.paperSizes.desc'),
},
].map((feature, i) => (
<div
key={i}
className={css({
textAlign: 'center',
p: '4',
rounded: 'lg',
bg: 'rgba(255, 255, 255, 0.05)',
})}
>
<div className={css({ fontSize: '2xl', mb: '2' })}>{feature.icon}</div>
<div
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'white',
mb: '1',
})}
>
{feature.title}
</div>
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>
{feature.desc}
</div>
</div>
))}
</div>
{/* CTA Button */}
<div className={css({ textAlign: 'center' })}>
<Link
href="/create"
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '2',
bg: 'blue.600',
color: 'white',
px: '6',
py: '3',
rounded: 'lg',
fontWeight: 'semibold',
transition: 'all 0.2s',
_hover: {
bg: 'blue.500',
},
})}
>
<span>{t('flashcards.cta')}</span>
<span></span>
</Link>
</div>
</div>
</section>
</div>
</div>
</PageWithNav>
</HomeHeroProvider>
</div>
</PageWithNav>
)
}

View File

@@ -0,0 +1,66 @@
/**
* Test page for AbacusStatic - Server Component
* This demonstrates that AbacusStatic works without "use client"
*
* Note: Uses /static import path to avoid client-side code
*/
import { AbacusStatic } from '@soroban/abacus-react/static'
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 &quot;use client&quot; 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&apos;ll see pure HTML/SVG with
no client-side hydration markers!
</p>
</div>
</div>
)
}

View File

@@ -4,6 +4,7 @@ import { css } from '../../../../styled-system/css'
import { useCardSorting } from '../Provider'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useSpring, animated, to } from '@react-spring/web'
import { useViewport } from '@/contexts/ViewportContext'
import type { SortingCard } from '../types'
// Add celebration animations
@@ -929,10 +930,12 @@ export function PlayingPhaseDrag() {
const [nextZIndex, setNextZIndex] = useState(1)
// Track viewport dimensions for responsive positioning
// Get viewport dimensions (uses mock dimensions in preview mode)
const viewport = useViewport()
// For spectators, reduce dimensions to account for panels
const getEffectiveViewportWidth = () => {
if (typeof window === 'undefined') return 1000
const baseWidth = window.innerWidth
const baseWidth = viewport.width
// Sidebar is hidden on mobile (< 768px), narrower on desktop
if (isSpectating && !spectatorStatsCollapsed && baseWidth >= 768) {
return baseWidth - 240 // Subtract stats sidebar width on desktop
@@ -941,9 +944,8 @@ export function PlayingPhaseDrag() {
}
const getEffectiveViewportHeight = () => {
if (typeof window === 'undefined') return 800
const baseHeight = window.innerHeight
const baseWidth = window.innerWidth
const baseHeight = viewport.height
const baseWidth = viewport.width
if (isSpectating) {
// Banner is 170px on mobile (130px mini nav + 40px spectator banner), 56px on desktop
return baseHeight - (baseWidth < 768 ? 170 : 56)
@@ -1284,8 +1286,8 @@ export function PlayingPhaseDrag() {
const newYPx = e.clientY - offsetY
// Convert to percentages
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
const viewportWidth = viewport.width
const viewportHeight = viewport.height
const newX = (newXPx / viewportWidth) * 100
const newY = (newYPx / viewportHeight) * 100
@@ -1901,22 +1903,15 @@ export function PlayingPhaseDrag() {
})}
style={{
width:
isSpectating &&
!spectatorStatsCollapsed &&
typeof window !== 'undefined' &&
window.innerWidth >= 768
isSpectating && !spectatorStatsCollapsed && viewport.width >= 768
? 'calc(100vw - 240px)'
: '100vw',
height: isSpectating
? typeof window !== 'undefined' && window.innerWidth < 768
? viewport.width < 768
? 'calc(100vh - 170px)'
: 'calc(100vh - 56px)'
: '100vh',
top: isSpectating
? typeof window !== 'undefined' && window.innerWidth < 768
? '170px'
: '56px'
: '0',
top: isSpectating ? (viewport.width < 768 ? '170px' : '56px') : '0',
}}
>
{/* Render continuous curved path through the entire sequence */}

View File

@@ -1,15 +1,19 @@
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { css } from '../../../../styled-system/css'
import { useGameMode } from '@/contexts/GameModeContext'
import { useMatching } from '../Provider'
import { formatGameTime, getMultiplayerWinner, getPerformanceAnalysis } from '../utils/gameScoring'
import { useRecordGameResult } from '@/hooks/useRecordGameResult'
import type { GameResult } from '@/lib/arcade/stats/types'
export function ResultsPhase() {
const router = useRouter()
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
const { mutate: recordGameResult } = useRecordGameResult()
// Get active player data array
const activePlayerData = Array.from(activePlayerIds)
@@ -28,6 +32,45 @@ export function ResultsPhase() {
const multiplayerResult =
gameMode === 'multiplayer' ? getMultiplayerWinner(state, activePlayers) : null
// Record game stats when results are shown
useEffect(() => {
if (!state.gameEndTime || !state.gameStartTime) return
// Build game result
const gameResult: GameResult = {
gameType: 'matching',
playerResults: activePlayerData.map((player) => {
const isWinner = gameMode === 'single' || multiplayerResult?.winners.includes(player.id)
const score =
gameMode === 'multiplayer'
? multiplayerResult?.scores[player.id] || 0
: state.matchedPairs
return {
playerId: player.id,
won: isWinner || false,
score,
accuracy: analysis.statistics.accuracy / 100, // Convert percentage to 0-1
completionTime: gameTime,
metrics: {
moves: state.moves,
matchedPairs: state.matchedPairs,
},
}
}),
completedAt: state.gameEndTime,
duration: gameTime,
metadata: {
gameMode,
starRating: analysis.starRating,
grade: analysis.grade,
},
}
console.log('📊 Recording matching game result:', gameResult)
recordGameResult(gameResult)
}, []) // Empty deps - only record once when component mounts
return (
<div
className={css({

View File

@@ -1,10 +1,12 @@
import { useCallback, useEffect, useState } from 'react'
import { isPrefix } from '@/lib/memory-quiz-utils'
import { useMemoryQuiz } from '../Provider'
import { useViewport } from '@/contexts/ViewportContext'
import { CardGrid } from './CardGrid'
export function InputPhase() {
const { state, dispatch, acceptNumber, rejectNumber, setInput, showResults } = useMemoryQuiz()
const viewport = useViewport()
const [displayFeedback, setDisplayFeedback] = useState<'neutral' | 'correct' | 'incorrect'>(
'neutral'
)
@@ -56,7 +58,7 @@ export function InputPhase() {
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
// Method 3: Check viewport characteristics for mobile devices
const isMobileViewport = window.innerWidth <= 768 && window.innerHeight <= 1024
const isMobileViewport = viewport.width <= 768 && viewport.height <= 1024
// Combined heuristic: assume no physical keyboard if:
// - It's a touch device AND has mobile viewport AND lacks precise pointer

View File

@@ -6,6 +6,7 @@ import { Textfit } from 'react-textfit'
import { css } from '../../../../styled-system/css'
import { Z_INDEX } from '@/constants/zIndex'
import { useAbacusSettings } from '@/hooks/useAbacusSettings'
import { useViewport } from '@/contexts/ViewportContext'
import { OverviewSection } from './guide-sections/OverviewSection'
import { PiecesSection } from './guide-sections/PiecesSection'
import { CaptureSection } from './guide-sections/CaptureSection'
@@ -37,6 +38,7 @@ export function PlayingGuideModal({
const t = useTranslations('rithmomachia.guide')
const { data: abacusSettings } = useAbacusSettings()
const useNativeAbacusNumbers = abacusSettings?.nativeAbacusNumbers ?? false
const viewport = useViewport()
const [activeSection, setActiveSection] = useState<Section>('overview')
@@ -69,7 +71,7 @@ export function PlayingGuideModal({
const [isDragging, setIsDragging] = useState(false)
const [windowWidth, setWindowWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 800
typeof window !== 'undefined' ? viewport.width : 800
)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const [isResizing, setIsResizing] = useState(false)
@@ -98,18 +100,16 @@ export function PlayingGuideModal({
// Track window width for responsive behavior
useEffect(() => {
const handleResize = () => setWindowWidth(window.innerWidth)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
setWindowWidth(viewport.width)
}, [viewport.width])
// Center modal on mount (not in standalone mode)
useEffect(() => {
if (isOpen && modalRef.current && !standalone) {
const rect = modalRef.current.getBoundingClientRect()
setPosition({
x: (window.innerWidth - rect.width) / 2,
y: Math.max(50, (window.innerHeight - rect.height) / 2),
x: (viewport.width - rect.width) / 2,
y: Math.max(50, (viewport.height - rect.height) / 2),
})
}
}, [isOpen, standalone])
@@ -118,13 +118,13 @@ export function PlayingGuideModal({
const handleMouseDown = (e: React.MouseEvent) => {
console.log(
'[GUIDE_DRAG] === MOUSE DOWN === windowWidth: ' +
window.innerWidth +
viewport.width +
', standalone: ' +
standalone +
', docked: ' +
docked
)
if (window.innerWidth < 768 || standalone) {
if (viewport.width < 768 || standalone) {
console.log('[GUIDE_DRAG] Skipping drag - mobile or standalone')
return // No dragging on mobile or standalone
}
@@ -169,7 +169,7 @@ export function PlayingGuideModal({
// Handle resize start
const handleResizeStart = (e: React.MouseEvent, direction: string) => {
if (window.innerWidth < 768 || standalone) return
if (viewport.width < 768 || standalone) return
e.stopPropagation()
setIsResizing(true)
setResizeDirection(direction)
@@ -326,7 +326,7 @@ export function PlayingGuideModal({
if (e.clientX < DOCK_THRESHOLD) {
setDockPreview('left')
onDockPreview('left')
} else if (e.clientX > window.innerWidth - DOCK_THRESHOLD) {
} else if (e.clientX > viewport.width - DOCK_THRESHOLD) {
setDockPreview('right')
onDockPreview('right')
} else {
@@ -351,26 +351,23 @@ export function PlayingGuideModal({
const minHeight = 300
if (resizeDirection.includes('e')) {
newWidth = Math.max(
minWidth,
Math.min(window.innerWidth * 0.9, resizeStart.width + deltaX)
)
newWidth = Math.max(minWidth, Math.min(viewport.width * 0.9, resizeStart.width + deltaX))
}
if (resizeDirection.includes('w')) {
const desiredWidth = resizeStart.width - deltaX
newWidth = Math.max(minWidth, Math.min(window.innerWidth * 0.9, desiredWidth))
newWidth = Math.max(minWidth, Math.min(viewport.width * 0.9, desiredWidth))
// Move left edge by the amount we actually changed width
newX = resizeStart.x + (resizeStart.width - newWidth)
}
if (resizeDirection.includes('s')) {
newHeight = Math.max(
minHeight,
Math.min(window.innerHeight * 0.9, resizeStart.height + deltaY)
Math.min(viewport.height * 0.9, resizeStart.height + deltaY)
)
}
if (resizeDirection.includes('n')) {
const desiredHeight = resizeStart.height - deltaY
newHeight = Math.max(minHeight, Math.min(window.innerHeight * 0.9, desiredHeight))
newHeight = Math.max(minHeight, Math.min(viewport.height * 0.9, desiredHeight))
// Move top edge by the amount we actually changed height
newY = resizeStart.y + (resizeStart.height - newHeight)
}
@@ -403,7 +400,7 @@ export function PlayingGuideModal({
', threshold: ' +
DOCK_THRESHOLD +
', windowWidth: ' +
window.innerWidth
viewport.width
)
if (e.clientX < DOCK_THRESHOLD) {
@@ -420,7 +417,7 @@ export function PlayingGuideModal({
}
console.log('[GUIDE_DRAG] Cleared state after re-dock to left')
return
} else if (e.clientX > window.innerWidth - DOCK_THRESHOLD) {
} else if (e.clientX > viewport.width - DOCK_THRESHOLD) {
console.log('[GUIDE_DRAG] Mouse up - near right edge, calling onDock(right)')
onDock('right')
// Don't call onUndock if we're re-docking
@@ -496,7 +493,7 @@ export function PlayingGuideModal({
const isMedium = effectiveWidth < 600
const renderResizeHandles = () => {
if (!isHovered || window.innerWidth < 768 || standalone) return null
if (!isHovered || viewport.width < 768 || standalone) return null
const handleStyle = {
position: 'absolute' as const,
@@ -662,7 +659,7 @@ export function PlayingGuideModal({
opacity:
dockPreview !== null
? 0.8
: !standalone && !docked && window.innerWidth >= 768 && !isHovered
: !standalone && !docked && viewport.width >= 768 && !isHovered
? 0.8
: 1,
transition: 'opacity 0.2s ease',
@@ -705,7 +702,7 @@ export function PlayingGuideModal({
padding: isVeryNarrow ? '8px' : isNarrow ? '12px' : '24px',
cursor: isDragging
? 'grabbing'
: !standalone && window.innerWidth >= 768
: !standalone && viewport.width >= 768
? 'grab'
: 'default',
}}

View File

@@ -0,0 +1,146 @@
'use client'
import { useEffect, useState } from 'react'
import { css } from '../../../styled-system/css'
type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'
interface Job {
id: string
status: JobStatus
progress?: string
error?: string
createdAt: string
completedAt?: string
}
interface JobMonitorProps {
jobId: string
onComplete: () => void
}
export function JobMonitor({ jobId, onComplete }: JobMonitorProps) {
const [job, setJob] = useState<Job | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let pollInterval: NodeJS.Timeout
const pollStatus = async () => {
try {
const response = await fetch(`/api/abacus/status/${jobId}`)
if (!response.ok) {
throw new Error('Failed to fetch job status')
}
const data = await response.json()
setJob(data)
if (data.status === 'completed') {
onComplete()
clearInterval(pollInterval)
} else if (data.status === 'failed') {
setError(data.error || 'Job failed')
clearInterval(pollInterval)
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
clearInterval(pollInterval)
}
}
// Poll immediately
pollStatus()
// Then poll every 1 second
pollInterval = setInterval(pollStatus, 1000)
return () => clearInterval(pollInterval)
}, [jobId, onComplete])
if (error) {
return (
<div
data-status="error"
className={css({
p: 4,
bg: 'red.100',
borderRadius: '8px',
borderLeft: '4px solid',
borderColor: 'red.600',
})}
>
<div className={css({ fontWeight: 'bold', color: 'red.800', mb: 1 })}>Error</div>
<div className={css({ color: 'red.700' })}>{error}</div>
</div>
)
}
if (!job) {
return (
<div data-status="loading" className={css({ p: 4, textAlign: 'center' })}>
Loading...
</div>
)
}
const statusColors = {
pending: 'blue',
processing: 'yellow',
completed: 'green',
failed: 'red',
}
const statusColor = statusColors[job.status]
return (
<div
data-component="job-monitor"
className={css({
p: 4,
bg: `${statusColor}.50`,
borderRadius: '8px',
borderLeft: '4px solid',
borderColor: `${statusColor}.600`,
})}
>
<div
className={css({
display: 'flex',
alignItems: 'center',
gap: 2,
mb: 2,
})}
>
<div
data-status={job.status}
className={css({
fontWeight: 'bold',
color: `${statusColor}.800`,
textTransform: 'capitalize',
})}
>
{job.status}
</div>
{(job.status === 'pending' || job.status === 'processing') && (
<div
className={css({
width: '16px',
height: '16px',
border: '2px solid',
borderColor: `${statusColor}.600`,
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
})}
/>
)}
</div>
{job.progress && (
<div className={css({ color: `${statusColor}.700`, fontSize: 'sm' })}>{job.progress}</div>
)}
{job.error && (
<div className={css({ color: 'red.700', fontSize: 'sm', mt: 2 })}>Error: {job.error}</div>
)}
</div>
)
}

View File

@@ -0,0 +1,181 @@
'use client'
import { OrbitControls, Stage } from '@react-three/drei'
import { Canvas, useLoader } from '@react-three/fiber'
import { Suspense, useEffect, useState } from 'react'
// @ts-expect-error - STLLoader doesn't have TypeScript declarations
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'
import { css } from '../../../styled-system/css'
interface STLModelProps {
url: string
}
function STLModel({ url }: STLModelProps) {
const geometry = useLoader(STLLoader, url)
return (
<mesh geometry={geometry}>
<meshStandardMaterial color="#8b7355" metalness={0.1} roughness={0.6} />
</mesh>
)
}
interface STLPreviewProps {
columns: number
scaleFactor: number
}
export function STLPreview({ columns, scaleFactor }: STLPreviewProps) {
const [previewUrl, setPreviewUrl] = useState<string>('/3d-models/simplified.abacus.stl')
const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let mounted = true
const generatePreview = async () => {
setIsGenerating(true)
setError(null)
try {
const response = await fetch('/api/abacus/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ columns, scaleFactor }),
})
if (!response.ok) {
throw new Error('Failed to generate preview')
}
// Convert response to blob and create object URL
const blob = await response.blob()
const objectUrl = URL.createObjectURL(blob)
if (mounted) {
// Revoke old URL if it exists
if (previewUrl && previewUrl.startsWith('blob:')) {
URL.revokeObjectURL(previewUrl)
}
setPreviewUrl(objectUrl)
} else {
// Component unmounted, clean up the URL
URL.revokeObjectURL(objectUrl)
}
} catch (err) {
if (mounted) {
const errorMessage = err instanceof Error ? err.message : 'Failed to generate preview'
// Check if this is an OpenSCAD not found error
if (
errorMessage.includes('openscad: command not found') ||
errorMessage.includes('Command failed: openscad')
) {
setError('OpenSCAD not installed (preview only available in production/Docker)')
// Fallback to showing the base STL
setPreviewUrl('/3d-models/simplified.abacus.stl')
} else {
setError(errorMessage)
}
console.error('Preview generation error:', err)
}
} finally {
if (mounted) {
setIsGenerating(false)
}
}
}
// Debounce: Wait 1 second after parameters change before regenerating
const timeoutId = setTimeout(generatePreview, 1000)
return () => {
mounted = false
clearTimeout(timeoutId)
}
}, [columns, scaleFactor])
return (
<div
data-component="stl-preview"
className={css({
position: 'relative',
width: '100%',
height: '500px',
bg: 'gray.900',
borderRadius: '8px',
overflow: 'hidden',
})}
>
{isGenerating && (
<div
className={css({
position: 'absolute',
top: 4,
right: 4,
left: 4,
zIndex: 10,
bg: 'blue.600',
color: 'white',
px: 3,
py: 2,
borderRadius: '4px',
fontSize: 'sm',
fontWeight: 'bold',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: 2 })}>
<div
className={css({
width: '16px',
height: '16px',
border: '2px solid white',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
})}
/>
<span>Rendering preview (may take 30-60 seconds)...</span>
</div>
</div>
)}
{error && (
<div
className={css({
position: 'absolute',
top: 4,
right: 4,
left: 4,
zIndex: 10,
bg: 'red.600',
color: 'white',
px: 3,
py: 2,
borderRadius: '4px',
fontSize: 'sm',
fontWeight: 'bold',
})}
>
<div>Preview Error:</div>
<div className={css({ fontSize: 'xs', mt: 1, opacity: 0.9 })}>{error}</div>
</div>
)}
<Canvas camera={{ position: [0, 0, 100], fov: 50 }}>
<Suspense
fallback={
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
}
>
<Stage environment="city" intensity={0.6}>
<STLModel url={previewUrl} key={previewUrl} />
</Stage>
<OrbitControls makeDefault />
</Suspense>
</Canvas>
</div>
)
}

View File

@@ -567,7 +567,6 @@ function MinimalNav({
export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const pathname = usePathname()
const router = useRouter()
const isGamePage = pathname?.startsWith('/games')
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
@@ -583,7 +582,8 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const showBranding = !homeHero || !homeHero.isHeroVisible
// Auto-detect variant based on context
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
// Only arcade pages (not /games) should use minimal nav
const actualVariant = variant === 'full' && isArcadePage ? 'minimal' : variant
// Mini nav for games/arcade (both fullscreen and non-fullscreen)
if (actualVariant === 'minimal') {
@@ -616,7 +616,7 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
top: 0,
left: 0,
right: 0,
zIndex: 1000,
zIndex: Z_INDEX.NAV_BAR,
transition: 'all 0.3s ease',
})}
>
@@ -675,7 +675,7 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
fontSize: 'sm',
maxW: '250px',
shadow: 'lg',
zIndex: 50,
zIndex: Z_INDEX.TOOLTIP,
})}
>
{subtitle.description}

View File

@@ -13,6 +13,9 @@ import { createQueryClient } from '@/lib/queryClient'
import type { Locale } from '@/i18n/messages'
import { AbacusSettingsSync } from './AbacusSettingsSync'
import { DeploymentInfo } from './DeploymentInfo'
import { MyAbacusProvider } from '@/contexts/MyAbacusContext'
import { MyAbacus } from './MyAbacus'
import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
interface ClientProvidersProps {
children: ReactNode
@@ -24,15 +27,20 @@ function InnerProviders({ children }: { children: ReactNode }) {
const { locale, messages } = useLocaleContext()
return (
<NextIntlClientProvider locale={locale} messages={messages}>
<NextIntlClientProvider locale={locale} messages={messages} timeZone="UTC">
<ToastProvider>
<AbacusDisplayProvider>
<AbacusSettingsSync />
<UserProfileProvider>
<GameModeProvider>
<FullscreenProvider>
{children}
<DeploymentInfo />
<HomeHeroProvider>
<MyAbacusProvider>
{children}
<DeploymentInfo />
<MyAbacus />
</MyAbacusProvider>
</HomeHeroProvider>
</FullscreenProvider>
</GameModeProvider>
</UserProfileProvider>

View File

@@ -0,0 +1,125 @@
'use client'
import { Component, createContext, useEffect, useMemo, useState } from 'react'
import type { ReactNode } from 'react'
import type { GameComponent, GameProviderComponent } from '@/lib/arcade/game-sdk/types'
import { MockArcadeEnvironment } from './MockArcadeEnvironment'
import { GameModeProvider } from '@/contexts/GameModeContext'
import { ViewportProvider } from '@/contexts/ViewportContext'
import { getMockGameState } from './MockGameStates'
// Export context so useArcadeSession can check for preview mode
export const PreviewModeContext = createContext<{
isPreview: boolean
mockState: any
} | null>(null)
interface GamePreviewProps {
GameComponent: GameComponent
Provider: GameProviderComponent
gameName: string
}
/**
* Error boundary to prevent game errors from crashing the page
*/
class GameErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
constructor(props: { children: ReactNode; fallback: ReactNode }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error) {
console.error(`Game preview error (${error.message}):`, error)
}
render() {
if (this.state.hasError) {
return this.props.fallback
}
return this.props.children
}
}
/**
* Wrapper for displaying games in demo/preview mode
* Provides mock arcade contexts so games can render
*/
export function GamePreview({ GameComponent, Provider, gameName }: GamePreviewProps) {
// Don't render on first mount to avoid hydration issues
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
// Get mock state for this game
const mockState = useMemo(() => getMockGameState(gameName), [gameName])
// Preview mode context value
const previewModeValue = useMemo(
() => ({
isPreview: true,
mockState,
}),
[mockState]
)
if (!mounted) {
return null
}
return (
<GameErrorBoundary
fallback={
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: 'rgba(255, 255, 255, 0.4)',
fontSize: '14px',
textAlign: 'center',
padding: '20px',
}}
>
<span style={{ fontSize: '48px', marginBottom: '10px' }}>🎮</span>
Game Demo
</div>
}
>
<PreviewModeContext.Provider value={previewModeValue}>
<MockArcadeEnvironment gameName={gameName}>
<GameModeProvider>
{/*
Mock viewport: Provide 1440x900 dimensions to games via ViewportContext
This prevents layout issues when games check viewport size
*/}
<ViewportProvider width={1440} height={900}>
<div
style={{
width: '1440px',
height: '900px',
position: 'relative',
overflow: 'hidden',
}}
>
<Provider>
<GameComponent />
</Provider>
</div>
</ViewportProvider>
</GameModeProvider>
</MockArcadeEnvironment>
</PreviewModeContext.Provider>
</GameErrorBoundary>
)
}

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

@@ -68,6 +68,7 @@ export function InteractiveFlashcards() {
return (
<div
ref={containerRef}
data-component="interactive-flashcards"
className={css({
position: 'relative',
width: '100%',
@@ -78,6 +79,7 @@ export function InteractiveFlashcards() {
bg: 'rgba(0, 0, 0, 0.3)',
rounded: 'xl',
border: '1px solid rgba(255, 255, 255, 0.1)',
zIndex: 1, // Create stacking context so child z-indexes are relative
})}
>
{cards.map((card) => (
@@ -113,7 +115,7 @@ function DraggableCard({ card, containerRef }: DraggableCardProps) {
const handlePointerDown = (e: React.PointerEvent) => {
setIsDragging(true)
setZIndex(1000) // Bring to front
setZIndex(10) // Bring to front within container stacking context
setDragSpeed(0)
// Capture the pointer

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

@@ -0,0 +1,211 @@
'use client'
import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'
import type { Player } from '@/contexts/GameModeContext'
import type { GameMove } from '@/lib/arcade/validation'
import type { RetryState } from '@/lib/arcade/error-handling'
// ============================================================================
// Mock ViewerId Context
// ============================================================================
const MockViewerIdContext = createContext<string>('demo-viewer-id')
export function useMockViewerId() {
return useContext(MockViewerIdContext)
}
// ============================================================================
// Mock Room Data Context
// ============================================================================
interface MockRoomData {
id: string
name: string
code: string
gameName: string
gameConfig: Record<string, unknown>
}
const MockRoomDataContext = createContext<MockRoomData | null>(null)
export function useMockRoomData() {
const room = useContext(MockRoomDataContext)
if (!room) throw new Error('useMockRoomData must be used within MockRoomDataProvider')
return room
}
export function useMockUpdateGameConfig() {
return useCallback((config: Record<string, unknown>) => {
// Mock: do nothing in preview mode
console.log('Mock updateGameConfig:', config)
}, [])
}
// ============================================================================
// Mock Game Mode Context
// ============================================================================
type GameMode = 'single' | 'battle' | 'tournament'
interface GameModeContextType {
gameMode: GameMode
players: Map<string, Player>
activePlayers: Set<string>
activePlayerCount: number
addPlayer: (player?: Partial<Player>) => void
updatePlayer: (id: string, updates: Partial<Player>) => void
removePlayer: (id: string) => void
setActive: (id: string, active: boolean) => void
getActivePlayers: () => Player[]
getPlayer: (id: string) => Player | undefined
getAllPlayers: () => Player[]
resetPlayers: () => void
isLoading: boolean
}
const MockGameModeContextValue = createContext<GameModeContextType | null>(null)
export function useMockGameMode() {
const ctx = useContext(MockGameModeContextValue)
if (!ctx) throw new Error('useMockGameMode must be used within MockGameModeProvider')
return ctx
}
// ============================================================================
// Mock Arcade Session
// ============================================================================
interface MockArcadeSessionReturn<TState> {
state: TState
version: number
connected: boolean
hasPendingMoves: boolean
lastError: string | null
retryState: RetryState
sendMove: (move: Omit<GameMove, 'timestamp'>) => void
exitSession: () => void
clearError: () => void
refresh: () => void
}
export function createMockArcadeSession<TState>(
initialState: TState
): MockArcadeSessionReturn<TState> {
const mockRetryState: RetryState = {
isRetrying: false,
retryCount: 0,
move: null,
timestamp: null,
}
return {
state: initialState,
version: 1,
connected: true,
hasPendingMoves: false,
lastError: null,
retryState: mockRetryState,
sendMove: () => {
// Mock: do nothing in preview
},
exitSession: () => {
// Mock: do nothing in preview
},
clearError: () => {
// Mock: do nothing in preview
},
refresh: () => {
// Mock: do nothing in preview
},
}
}
// ============================================================================
// Mock Environment Provider
// ============================================================================
interface MockArcadeEnvironmentProps {
children: ReactNode
gameName: string
gameConfig?: Record<string, unknown>
}
export function MockArcadeEnvironment({
children,
gameName,
gameConfig = {},
}: MockArcadeEnvironmentProps) {
const mockPlayers = useMemo(
(): Player[] => [
{
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
color: '#3b82f6',
createdAt: Date.now(),
},
],
[]
)
const playersMap = useMemo(() => {
const map = new Map<string, Player>()
for (const p of mockPlayers) {
map.set(p.id, p)
}
return map
}, [mockPlayers])
const activePlayers = useMemo(() => new Set(mockPlayers.map((p) => p.id)), [mockPlayers])
const mockGameModeCtx: GameModeContextType = useMemo(
() => ({
gameMode: 'single',
players: playersMap,
activePlayers,
activePlayerCount: activePlayers.size,
addPlayer: () => {
// Mock: do nothing
},
updatePlayer: () => {
// Mock: do nothing
},
removePlayer: () => {
// Mock: do nothing
},
setActive: () => {
// Mock: do nothing
},
getActivePlayers: () => mockPlayers,
getPlayer: (id: string) => playersMap.get(id),
getAllPlayers: () => mockPlayers,
resetPlayers: () => {
// Mock: do nothing
},
isLoading: false,
}),
[mockPlayers, playersMap, activePlayers]
)
const mockRoomData: MockRoomData = useMemo(
() => ({
id: `demo-room-${gameName}`,
name: 'Demo Room',
code: 'DEMO',
gameName,
gameConfig,
}),
[gameName, gameConfig]
)
return (
<MockViewerIdContext.Provider value="demo-viewer-id">
<MockRoomDataContext.Provider value={mockRoomData}>
<MockGameModeContextValue.Provider value={mockGameModeCtx}>
{children}
</MockGameModeContextValue.Provider>
</MockRoomDataContext.Provider>
</MockViewerIdContext.Provider>
)
}

View File

@@ -0,0 +1,21 @@
'use client'
/**
* Mock implementations of arcade SDK hooks for game previews
* These are exported with the same names so games can use them transparently
*/
import {
useMockViewerId,
useMockRoomData,
useMockUpdateGameConfig,
useMockGameMode,
} from './MockArcadeEnvironment'
// Re-export with SDK names
export const useViewerId = useMockViewerId
export const useRoomData = useMockRoomData
export const useUpdateGameConfig = useMockUpdateGameConfig
export const useGameMode = useMockGameMode
// Note: useArcadeSession must be handled per-game since it needs type parameters

View File

@@ -0,0 +1,437 @@
/**
* Mock game states for game previews
* Creates proper initial states in "playing" phase for each game type
*/
import { complementRaceValidator } from '@/arcade-games/complement-race/Validator'
import { matchingGameValidator } from '@/arcade-games/matching/Validator'
import { memoryQuizGameValidator } from '@/arcade-games/memory-quiz/Validator'
import { cardSortingValidator } from '@/arcade-games/card-sorting/Validator'
import { rithmomachiaValidator } from '@/arcade-games/rithmomachia/Validator'
import {
DEFAULT_COMPLEMENT_RACE_CONFIG,
DEFAULT_MATCHING_CONFIG,
DEFAULT_MEMORY_QUIZ_CONFIG,
DEFAULT_CARD_SORTING_CONFIG,
DEFAULT_RITHMOMACHIA_CONFIG,
} from '@/lib/arcade/game-configs'
import type { ComplementRaceState } from '@/arcade-games/complement-race/types'
import type { MatchingState } from '@/arcade-games/matching/types'
import type { MemoryQuizState } from '@/arcade-games/memory-quiz/types'
import type { CardSortingState } from '@/arcade-games/card-sorting/types'
import type { RithmomachiaState } from '@/arcade-games/rithmomachia/types'
/**
* Create a mock state for Complement Race in playing phase
* Shows mid-game state with progress and activity
*/
export function createMockComplementRaceState(): ComplementRaceState {
const baseState = complementRaceValidator.getInitialState(DEFAULT_COMPLEMENT_RACE_CONFIG)
// Create some passengers for visual interest
const mockPassengers = [
{
id: 'p1',
name: 'Alice',
avatar: '👩‍💼',
originStationId: 'depot',
destinationStationId: 'canyon',
isUrgent: false,
claimedBy: 'demo-player-1',
deliveredBy: null,
carIndex: 0,
timestamp: Date.now() - 10000,
},
{
id: 'p2',
name: 'Bob',
avatar: '👨‍🎓',
originStationId: 'riverside',
destinationStationId: 'grand-central',
isUrgent: true,
claimedBy: null,
deliveredBy: null,
carIndex: null,
timestamp: Date.now() - 5000,
},
]
// Create stations for sprint mode
const mockStations = [
{ id: 'station-0', name: 'Depot', position: 0, icon: '🏭', emoji: '🏭' },
{ id: 'station-1', name: 'Riverside', position: 20, icon: '🌊', emoji: '🌊' },
{ id: 'station-2', name: 'Hillside', position: 40, icon: '⛰️', emoji: '⛰️' },
{ id: 'station-3', name: 'Canyon View', position: 60, icon: '🏜️', emoji: '🏜️' },
{ id: 'station-4', name: 'Meadows', position: 80, icon: '🌾', emoji: '🌾' },
{ id: 'station-5', name: 'Grand Central', position: 100, icon: '🏛️', emoji: '🏛️' },
]
// Override to playing phase with mid-game action
// IMPORTANT: Set style to 'sprint' for Steam Sprint mode with train visualization
return {
...baseState,
config: {
...baseState.config,
style: 'sprint', // Steam Sprint mode with train and passengers
},
style: 'sprint', // Also set at top level for local context
gamePhase: 'playing',
isGameActive: true,
activePlayers: ['demo-player-1'],
playerMetadata: {
'demo-player-1': {
name: 'Demo Player',
color: '#3b82f6',
},
},
players: {
'demo-player-1': {
id: 'demo-player-1',
name: 'Demo Player',
color: '#3b82f6',
score: 420,
streak: 5,
bestStreak: 8,
correctAnswers: 18,
totalQuestions: 21,
position: 65, // Well into the race
isReady: true,
isActive: true,
currentAnswer: null,
lastAnswerTime: Date.now() - 2000,
passengers: ['p1'],
deliveredPassengers: 5,
},
},
currentQuestions: {
'demo-player-1': {
id: 'demo-q-current',
number: 6,
targetSum: 10,
correctAnswer: 4,
showAsAbacus: true,
timestamp: Date.now() - 1500,
},
},
currentQuestion: {
id: 'demo-q-current',
number: 6,
targetSum: 10,
correctAnswer: 4,
showAsAbacus: true,
timestamp: Date.now() - 1500,
},
// Sprint mode specific fields
momentum: 45, // Mid-level momentum
trainPosition: 65, // 65% along the track
pressure: 30, // Some pressure building up
elapsedTime: 45, // 45 seconds into the game
lastCorrectAnswerTime: Date.now() - 2000,
currentRoute: 1,
stations: mockStations,
passengers: mockPassengers,
deliveredPassengers: 5,
cumulativeDistance: 65,
showRouteCelebration: false,
questionStartTime: Date.now() - 1500,
gameStartTime: Date.now() - 45000, // Game has been running for 45 seconds
raceStartTime: Date.now() - 45000,
// Additional fields for compatibility
score: 420,
streak: 5,
bestStreak: 8,
correctAnswers: 18,
totalQuestions: 21,
currentInput: '',
}
}
/**
* Create a mock state for Matching game in playing phase
* Shows mid-game with some cards matched and one card flipped
*/
export function createMockMatchingState(): MatchingState {
const baseState = matchingGameValidator.getInitialState(DEFAULT_MATCHING_CONFIG)
// Create mock cards showing mid-game progress
// 2 pairs matched, 1 card currently flipped (looking for its match)
const mockGameCards = [
// Matched pair 1
{
id: 'c1',
type: 'number' as const,
number: 5,
matched: true,
matchedBy: 'demo-player-1',
},
{
id: 'c2',
type: 'number' as const,
number: 5,
matched: true,
matchedBy: 'demo-player-1',
},
// Matched pair 2
{
id: 'c3',
type: 'number' as const,
number: 8,
matched: true,
matchedBy: 'demo-player-1',
},
{
id: 'c4',
type: 'number' as const,
number: 8,
matched: true,
matchedBy: 'demo-player-1',
},
// Unmatched cards - player is looking for matches
{ id: 'c5', type: 'number' as const, number: 3, matched: false },
{ id: 'c6', type: 'number' as const, number: 7, matched: false },
{ id: 'c7', type: 'number' as const, number: 3, matched: false },
{ id: 'c8', type: 'number' as const, number: 7, matched: false },
{ id: 'c9', type: 'number' as const, number: 2, matched: false },
{ id: 'c10', type: 'number' as const, number: 2, matched: false },
{ id: 'c11', type: 'number' as const, number: 9, matched: false },
{ id: 'c12', type: 'number' as const, number: 9, matched: false },
]
// One card is currently flipped
const flippedCard = mockGameCards[4] // The first "3"
// Override to playing phase
return {
...baseState,
gamePhase: 'playing',
activePlayers: ['demo-player-1'],
playerMetadata: {
'demo-player-1': {
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
userId: 'demo-viewer-id',
color: '#3b82f6',
},
},
currentPlayer: 'demo-player-1',
gameCards: mockGameCards,
cards: mockGameCards,
flippedCards: [flippedCard],
scores: {
'demo-player-1': 2,
},
consecutiveMatches: {
'demo-player-1': 2,
},
matchedPairs: 2,
totalPairs: 6,
moves: 12,
gameStartTime: Date.now() - 25000, // Game has been running for 25 seconds
currentMoveStartTime: Date.now() - 500,
isProcessingMove: false,
showMismatchFeedback: false,
}
}
/**
* Create a mock state for Memory Quiz in input phase
* Shows mid-game with some numbers already found
*/
export function createMockMemoryQuizState(): MemoryQuizState {
const baseState = memoryQuizGameValidator.getInitialState(DEFAULT_MEMORY_QUIZ_CONFIG)
// Create mock quiz cards
const mockQuizCards = [
{ number: 123, svgComponent: null, element: null },
{ number: 456, svgComponent: null, element: null },
{ number: 789, svgComponent: null, element: null },
{ number: 234, svgComponent: null, element: null },
{ number: 567, svgComponent: null, element: null },
]
// Override to input phase with some numbers found
return {
...baseState,
gamePhase: 'input',
quizCards: mockQuizCards,
correctAnswers: mockQuizCards.map((c) => c.number),
cards: mockQuizCards,
currentCardIndex: mockQuizCards.length, // Display phase complete
foundNumbers: [123, 456], // 2 out of 5 found
guessesRemaining: 3,
currentInput: '',
incorrectGuesses: 1,
activePlayers: ['demo-player-1'],
playerMetadata: {
'demo-player-1': {
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
userId: 'demo-viewer-id',
color: '#3b82f6',
},
},
playerScores: {
'demo-viewer-id': {
correct: 2,
incorrect: 1,
},
},
numberFoundBy: {
123: 'demo-viewer-id',
456: 'demo-viewer-id',
},
playMode: 'cooperative',
selectedCount: 5,
selectedDifficulty: 'medium',
displayTime: 3000,
hasPhysicalKeyboard: true,
testingMode: false,
showOnScreenKeyboard: false,
prefixAcceptanceTimeout: null,
finishButtonsBound: false,
wrongGuessAnimations: [],
}
}
/**
* Create a mock state for Card Sorting in playing phase
* Shows mid-game with some cards placed in sorting area
*/
export function createMockCardSortingState(): CardSortingState {
const baseState = cardSortingValidator.getInitialState(DEFAULT_CARD_SORTING_CONFIG)
// Create mock cards with AbacusReact SVG placeholders
const mockCards = [
{ id: 'c1', number: 23, svgContent: '<svg>23</svg>' },
{ id: 'c2', number: 45, svgContent: '<svg>45</svg>' },
{ id: 'c3', number: 12, svgContent: '<svg>12</svg>' },
{ id: 'c4', number: 78, svgContent: '<svg>78</svg>' },
{ id: 'c5', number: 56, svgContent: '<svg>56</svg>' },
]
// Correct order (sorted)
const correctOrder = [...mockCards].sort((a, b) => a.number - b.number)
// Show 3 cards placed, 2 still available
return {
...baseState,
gamePhase: 'playing',
playerId: 'demo-player-1',
playerMetadata: {
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
userId: 'demo-viewer-id',
},
activePlayers: ['demo-player-1'],
allPlayerMetadata: new Map([
[
'demo-player-1',
{
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
userId: 'demo-viewer-id',
},
],
]),
gameStartTime: Date.now() - 30000, // 30 seconds ago
selectedCards: mockCards,
correctOrder,
availableCards: [mockCards[3], mockCards[4]], // 78 and 56 still available
placedCards: [mockCards[2], mockCards[0], mockCards[1], null, null], // 12, 23, 45, empty, empty
cardPositions: [],
cursorPositions: new Map(),
}
}
/**
* Create a mock state for Rithmomachia in playing phase
* Shows mid-game with some pieces captured
*/
export function createMockRithmomachiaState(): RithmomachiaState {
const baseState = rithmomachiaValidator.getInitialState(DEFAULT_RITHMOMACHIA_CONFIG)
// Start the game (transitions to playing phase)
return {
...baseState,
gamePhase: 'playing',
turn: 'W', // White's turn
// Captured pieces show some progress
capturedPieces: {
W: [
// White has captured 2 black pieces
{ id: 'B_C_01', color: 'B', type: 'C', value: 4, square: 'CAPTURED', captured: true },
{ id: 'B_T_01', color: 'B', type: 'T', value: 9, square: 'CAPTURED', captured: true },
],
B: [
// Black has captured 1 white piece
{ id: 'W_C_02', color: 'W', type: 'C', value: 6, square: 'CAPTURED', captured: true },
],
},
history: [
// Add a few moves to show activity
{
ply: 1,
color: 'W',
from: 'C2',
to: 'C4',
pieceId: 'W_C_01',
capture: null,
ambush: null,
fenLikeHash: 'mock-hash-1',
noProgressCount: 1,
resultAfter: 'ONGOING',
},
{
ply: 2,
color: 'B',
from: 'N7',
to: 'N5',
pieceId: 'B_T_02',
capture: null,
ambush: null,
fenLikeHash: 'mock-hash-2',
noProgressCount: 2,
resultAfter: 'ONGOING',
},
],
noProgressCount: 2,
stateHashes: ['initial-hash', 'mock-hash-1', 'mock-hash-2'],
}
}
/**
* Get mock state for any game by name
*/
export function getMockGameState(gameName: string): any {
switch (gameName) {
case 'complement-race':
return createMockComplementRaceState()
case 'matching':
return createMockMatchingState()
case 'memory-quiz':
return createMockMemoryQuizState()
case 'card-sorting':
return createMockCardSortingState()
case 'rithmomachia':
return createMockRithmomachiaState()
// For games we haven't implemented yet, return a basic "playing" state
default:
return {
gamePhase: 'playing',
activePlayers: ['demo-player-1'],
playerMetadata: {
'demo-player-1': {
id: 'demo-player-1',
name: 'Demo Player',
emoji: '🎮',
color: '#3b82f6',
userId: 'demo-viewer-id',
},
},
}
}
}

View File

@@ -0,0 +1,253 @@
'use client'
import { useContext, useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
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'
export function MyAbacus() {
const { isOpen, close, toggle } = useMyAbacus()
const appConfig = useAbacusConfig()
const pathname = usePathname()
// Sync with hero context if on home page
const homeHeroContext = useContext(HomeHeroContext)
const [localAbacusValue, setLocalAbacusValue] = useState(1234)
const abacusValue = homeHeroContext?.abacusValue ?? localAbacusValue
const setAbacusValue = homeHeroContext?.setAbacusValue ?? setLocalAbacusValue
// Determine display mode - only hero mode on actual home page
const isOnHomePage =
pathname === '/' ||
pathname === '/en' ||
pathname === '/de' ||
pathname === '/ja' ||
pathname === '/hi' ||
pathname === '/es' ||
pathname === '/la'
const isHeroVisible = homeHeroContext?.isHeroVisible ?? false
const isHeroMode = isOnHomePage && isHeroVisible && !isOpen
// Close on Escape key
useEffect(() => {
if (!isOpen) return
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
close()
}
}
window.addEventListener('keydown', handleEscape)
return () => window.removeEventListener('keydown', handleEscape)
}, [isOpen, close])
// Prevent body scroll when open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [isOpen])
// Use theme presets from abacus-react instead of manual definitions
const structuralStyles = ABACUS_THEMES.light
const trophyStyles = ABACUS_THEMES.trophy
return (
<>
{/* Blur backdrop - only visible when open */}
{isOpen && (
<div
data-component="my-abacus-backdrop"
style={{
WebkitBackdropFilter: 'blur(12px)',
}}
className={css({
position: 'fixed',
inset: 0,
bg: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(12px)',
zIndex: 101,
animation: 'backdropFadeIn 0.4s ease-out',
})}
onClick={close}
/>
)}
{/* Close button - only visible when open */}
{isOpen && (
<button
data-action="close-my-abacus"
onClick={close}
className={css({
position: 'fixed',
top: { base: '4', md: '8' },
right: { base: '4', md: '8' },
w: '12',
h: '12',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bg: 'rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(8px)',
border: '2px solid rgba(255, 255, 255, 0.2)',
borderRadius: 'full',
color: 'white',
fontSize: '2xl',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'all 0.2s',
zIndex: 103,
animation: 'fadeIn 0.3s ease-out 0.2s both',
_hover: {
bg: 'rgba(255, 255, 255, 0.2)',
borderColor: 'rgba(255, 255, 255, 0.4)',
transform: 'scale(1.1)',
},
})}
>
×
</button>
)}
{/* Single abacus element that morphs between states */}
<div
data-component="my-abacus"
data-mode={isOpen ? 'open' : isHeroMode ? 'hero' : 'button'}
onClick={isOpen || isHeroMode ? undefined : toggle}
className={css({
position: isHeroMode ? 'absolute' : 'fixed',
zIndex: 102,
cursor: isOpen || isHeroMode ? 'default' : 'pointer',
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: fixed to center of viewport
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: isHeroMode
? {
// Hero mode: absolute positioning - scrolls naturally with document
top: '60vh',
left: '50%',
transform: 'translate(-50%, -50%)',
}
: {
// Button mode: fixed to bottom-right corner
bottom: { base: '4', md: '6' },
right: { base: '4', md: '6' },
transform: 'translate(0, 0)',
}),
})}
>
{/* Container that changes between hero, button, and open states */}
<div
className={css({
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
...(isOpen || isHeroMode
? {
// Open/Hero state: no background, just the abacus
bg: 'transparent',
border: 'none',
boxShadow: 'none',
borderRadius: '0',
}
: {
// Button state: button styling
bg: 'rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(8px)',
border: '3px solid rgba(251, 191, 36, 0.5)',
boxShadow: '0 8px 32px rgba(251, 191, 36, 0.4)',
borderRadius: 'xl',
w: { base: '80px', md: '100px' },
h: { base: '80px', md: '100px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
animation: 'pulse 2s ease-in-out infinite',
_hover: {
transform: 'scale(1.1)',
boxShadow: '0 12px 48px rgba(251, 191, 36, 0.6)',
borderColor: 'rgba(251, 191, 36, 0.8)',
},
}),
})}
>
{/* The abacus itself - same element, scales between hero/button/open */}
<div
data-element="abacus-display"
className={css({
transform: isOpen
? { base: 'scale(2.5)', md: 'scale(3.5)', lg: 'scale(4.5)' }
: isHeroMode
? { base: 'scale(3)', md: 'scale(3.5)', lg: 'scale(4.25)' }
: 'scale(0.35)',
transformOrigin: 'center center',
transition: 'transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), filter 0.6s ease',
filter:
isOpen || isHeroMode
? 'drop-shadow(0 10px 40px rgba(251, 191, 36, 0.3))'
: 'drop-shadow(0 4px 12px rgba(251, 191, 36, 0.2))',
pointerEvents: isOpen || isHeroMode ? 'auto' : 'none',
})}
>
<AbacusReact
key={isHeroMode ? 'hero' : isOpen ? 'open' : 'closed'}
value={abacusValue}
columns={isHeroMode ? 4 : 5}
beadShape={appConfig.beadShape}
showNumbers={isOpen || isHeroMode}
interactive={isOpen || isHeroMode}
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>
</div>
{/* Keyframes for animations */}
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes backdropFadeIn {
from { opacity: 0; backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); }
to { opacity: 1; backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pulse {
0%, 100% { box-shadow: 0 8px 32px rgba(251, 191, 36, 0.4); }
50% { box-shadow: 0 12px 48px rgba(251, 191, 36, 0.6); }
}
`,
}}
/>
</>
)
}

View File

@@ -1,6 +1,6 @@
'use client'
import React from 'react'
import React, { useContext } from 'react'
import { useGameMode } from '../contexts/GameModeContext'
import { useRoomData } from '../hooks/useRoomData'
import { useViewerId } from '../hooks/useViewerId'
@@ -9,6 +9,7 @@ import { GameContextNav, type RosterWarning } from './nav/GameContextNav'
import type { PlayerBadge } from './nav/types'
import { PlayerConfigDialog } from './nav/PlayerConfigDialog'
import { ModerationNotifications } from './nav/ModerationNotifications'
import { PreviewModeContext } from './GamePreview'
interface PageWithNavProps {
navTitle?: string
@@ -57,6 +58,12 @@ export function PageWithNav({
onAssignBlackPlayer,
gamePhase,
}: PageWithNavProps) {
// In preview mode, render just the children without navigation
const previewMode = useContext(PreviewModeContext)
if (previewMode?.isPreview) {
return <>{children}</>
}
const { players, activePlayers, setActive, activePlayerCount } = useGameMode()
const { roomData, isInRoom, moderationEvent, clearModerationEvent } = useRoomData()
const { data: viewerId } = useViewerId()

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

@@ -26,6 +26,10 @@ export const Z_INDEX = {
// Top-level overlays (20000+)
TOAST: 20000,
// My Abacus - Personal trophy overlay (30000+)
MY_ABACUS_BACKDROP: 30000,
MY_ABACUS: 30001,
// Special navigation layers for game pages
GAME_NAV: {
// Hamburger menu and its nested content

View File

@@ -0,0 +1,35 @@
'use client'
import type React from 'react'
import { createContext, useContext, useState, useCallback } from 'react'
interface MyAbacusContextValue {
isOpen: boolean
open: () => void
close: () => void
toggle: () => void
}
const MyAbacusContext = createContext<MyAbacusContextValue | undefined>(undefined)
export function MyAbacusProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false)
const open = useCallback(() => setIsOpen(true), [])
const close = useCallback(() => setIsOpen(false), [])
const toggle = useCallback(() => setIsOpen((prev) => !prev), [])
return (
<MyAbacusContext.Provider value={{ isOpen, open, close, toggle }}>
{children}
</MyAbacusContext.Provider>
)
}
export function useMyAbacus() {
const context = useContext(MyAbacusContext)
if (!context) {
throw new Error('useMyAbacus must be used within MyAbacusProvider')
}
return context
}

View File

@@ -0,0 +1,72 @@
'use client'
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
/**
* Viewport dimensions
*/
export interface ViewportDimensions {
width: number
height: number
}
/**
* Viewport context value
*/
interface ViewportContextValue {
width: number
height: number
}
const ViewportContext = createContext<ViewportContextValue | null>(null)
/**
* Hook to get viewport dimensions
* Returns mock dimensions in preview mode, actual window dimensions otherwise
*/
export function useViewport(): ViewportDimensions {
const context = useContext(ViewportContext)
// If context is provided (preview mode or custom viewport), use it
if (context) {
return context
}
// Otherwise, use actual window dimensions (hook will update on resize)
const [dimensions, setDimensions] = useState<ViewportDimensions>({
width: typeof window !== 'undefined' ? window.innerWidth : 1440,
height: typeof window !== 'undefined' ? window.innerHeight : 900,
})
useEffect(() => {
const handleResize = () => {
setDimensions({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
handleResize() // Set initial value
return () => window.removeEventListener('resize', handleResize)
}, [])
return dimensions
}
/**
* Provider that supplies custom viewport dimensions
* Used in preview mode to provide mock 1440×900 viewport
*/
export function ViewportProvider({
children,
width,
height,
}: {
children: ReactNode
width: number
height: number
}) {
return <ViewportContext.Provider value={{ width, height }}>{children}</ViewportContext.Provider>
}

View File

@@ -9,6 +9,7 @@ export * from './abacus-settings'
export * from './arcade-rooms'
export * from './arcade-sessions'
export * from './players'
export * from './player-stats'
export * from './room-members'
export * from './room-member-history'
export * from './room-invitations'

View File

@@ -0,0 +1,85 @@
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'
import { players } from './players'
/**
* Player stats table - game statistics per player
*
* Tracks aggregate performance and per-game breakdowns for each player.
* One-to-one with players table. Deleted when player is deleted (cascade).
*/
export const playerStats = sqliteTable('player_stats', {
/** Primary key and foreign key to players table */
playerId: text('player_id')
.primaryKey()
.references(() => players.id, { onDelete: 'cascade' }),
/** Total number of games played across all game types */
gamesPlayed: integer('games_played').notNull().default(0),
/** Total number of games won */
totalWins: integer('total_wins').notNull().default(0),
/** Total number of games lost */
totalLosses: integer('total_losses').notNull().default(0),
/** Best completion time in milliseconds (across all games) */
bestTime: integer('best_time'),
/** Highest accuracy percentage (0.0 - 1.0, across all games) */
highestAccuracy: real('highest_accuracy').notNull().default(0),
/** Player's most-played game type */
favoriteGameType: text('favorite_game_type'),
/**
* Per-game statistics breakdown (JSON)
*
* Structure:
* {
* "matching": {
* gamesPlayed: 10,
* wins: 5,
* losses: 5,
* bestTime: 45000,
* highestAccuracy: 0.95,
* averageScore: 12.5,
* lastPlayed: 1704326400000
* },
* "complement-race": { ... },
* ...
* }
*/
gameStats: text('game_stats', { mode: 'json' })
.notNull()
.default('{}')
.$type<Record<string, GameStatsBreakdown>>(),
/** When this player last played any game */
lastPlayedAt: integer('last_played_at', { mode: 'timestamp' }),
/** When this record was created */
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
/** When this record was last updated */
updatedAt: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
})
/**
* Per-game stats breakdown stored in JSON
*/
export interface GameStatsBreakdown {
gamesPlayed: number
wins: number
losses: number
bestTime: number | null
highestAccuracy: number
averageScore: number
lastPlayed: number // timestamp
}
export type PlayerStats = typeof playerStats.$inferSelect
export type NewPlayerStats = typeof playerStats.$inferInsert

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
import type { GameMove } from '@/lib/arcade/validation'
import { useArcadeSocket } from './useArcadeSocket'
import {
@@ -6,6 +6,7 @@ import {
useOptimisticGameState,
} from './useOptimisticGameState'
import type { RetryState } from '@/lib/arcade/error-handling'
import { PreviewModeContext } from '@/components/GamePreview'
export interface UseArcadeSessionOptions<TState> extends UseOptimisticGameStateOptions<TState> {
/**
@@ -101,6 +102,40 @@ export function useArcadeSession<TState>(
): UseArcadeSessionReturn<TState> {
const { userId, roomId, autoJoin = true, ...optimisticOptions } = options
// Check if we're in preview mode
const previewMode = useContext(PreviewModeContext)
// If in preview mode, return mock session immediately
if (previewMode?.isPreview && previewMode?.mockState) {
const mockRetryState: RetryState = {
isRetrying: false,
retryCount: 0,
move: null,
timestamp: null,
}
return {
state: previewMode.mockState as TState,
version: 1,
connected: true,
hasPendingMoves: false,
lastError: null,
retryState: mockRetryState,
sendMove: () => {
// Mock: do nothing in preview
},
exitSession: () => {
// Mock: do nothing in preview
},
clearError: () => {
// Mock: do nothing in preview
},
refresh: () => {
// Mock: do nothing in preview
},
}
}
// Optimistic state management
const optimistic = useOptimisticGameState<TState>(optimisticOptions)

View File

@@ -0,0 +1,87 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import type {
GetAllPlayerStatsResponse,
GetPlayerStatsResponse,
PlayerStatsData,
} from '@/lib/arcade/stats/types'
import { api } from '@/lib/queryClient'
/**
* Hook to fetch stats for a specific player or all user's players
*
* Usage:
* ```tsx
* // Fetch all players' stats
* const { data, isLoading } = usePlayerStats()
* // data is PlayerStatsData[]
*
* // Fetch specific player's stats
* const { data, isLoading } = usePlayerStats('player-id')
* // data is PlayerStatsData
* ```
*/
export function usePlayerStats(playerId?: string) {
return useQuery<PlayerStatsData | PlayerStatsData[]>({
queryKey: playerId ? ['player-stats', playerId] : ['player-stats'],
queryFn: async () => {
const url = playerId ? `player-stats/${playerId}` : 'player-stats'
const res = await api(url)
if (!res.ok) {
throw new Error('Failed to fetch player stats')
}
const data: GetPlayerStatsResponse | GetAllPlayerStatsResponse = await res.json()
// Return single player stats or array of all stats
return 'stats' in data ? data.stats : data.playerStats
},
})
}
/**
* Hook to fetch stats for all user's players (typed as array)
*
* Convenience wrapper around usePlayerStats() with better typing.
*/
export function useAllPlayerStats() {
const query = useQuery<PlayerStatsData[]>({
queryKey: ['player-stats'],
queryFn: async () => {
const res = await api('player-stats')
if (!res.ok) {
throw new Error('Failed to fetch player stats')
}
const data: GetAllPlayerStatsResponse = await res.json()
return data.playerStats
},
})
return query
}
/**
* Hook to fetch stats for a specific player (typed as single object)
*
* Convenience wrapper around usePlayerStats() with better typing.
*/
export function useSinglePlayerStats(playerId: string) {
const query = useQuery<PlayerStatsData>({
queryKey: ['player-stats', playerId],
queryFn: async () => {
const res = await api(`player-stats/${playerId}`)
if (!res.ok) {
throw new Error('Failed to fetch player stats')
}
const data: GetPlayerStatsResponse = await res.json()
return data.stats
},
enabled: !!playerId, // Only run if playerId is provided
})
return query
}

View File

@@ -0,0 +1,51 @@
'use client'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { GameResult, RecordGameResponse } from '@/lib/arcade/stats/types'
import { api } from '@/lib/queryClient'
/**
* Hook to record a game result and update player stats
*
* Usage:
* ```tsx
* const { mutate: recordGame, isPending } = useRecordGameResult()
*
* recordGame(gameResult, {
* onSuccess: (updates) => {
* console.log('Stats recorded:', updates)
* }
* })
* ```
*/
export function useRecordGameResult() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (gameResult: GameResult): Promise<RecordGameResponse> => {
const res = await api('player-stats/record-game', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ gameResult }),
})
if (!res.ok) {
const error = await res.json().catch(() => ({ error: 'Failed to record game result' }))
throw new Error(error.error || 'Failed to record game result')
}
return res.json()
},
onSuccess: (response) => {
// Invalidate player stats queries to trigger refetch
queryClient.invalidateQueries({ queryKey: ['player-stats'] })
console.log('✅ Game result recorded successfully:', response.updates)
},
onError: (error) => {
console.error('❌ Failed to record game result:', error)
},
})
}

View File

@@ -1,15 +1,8 @@
{
"games": {
"hero": {
"title": "🕹️ Soroban Arcade",
"subtitle": "Level up your mental math powers in the most fun way possible!",
"xpBadge": "+100 XP",
"streakBadge": "STREAK!",
"features": {
"challenge": "🎯 Challenge Your Brain",
"speed": "⚡ Build Speed",
"achievements": "🏆 Unlock Achievements"
}
"title": "Soroban Arcade",
"subtitle": "Classic strategy games and lightning-fast challenges"
},
"enterArcade": {
"title": "🏟️ Ready for the Arena?",

View File

@@ -11,7 +11,7 @@ export async function getRequestLocale(): Promise<Locale> {
let locale = headersList.get('x-locale') as Locale | null
if (!locale) {
locale = cookieStore.get(LOCALE_COOKIE_NAME)?.value as Locale | undefined
locale = (cookieStore.get(LOCALE_COOKIE_NAME)?.value as Locale | undefined) ?? null
}
// Validate and fallback to default
@@ -28,5 +28,6 @@ export default getRequestConfig(async () => {
return {
locale,
messages: await getMessages(locale),
timeZone: 'UTC',
}
})

View File

@@ -0,0 +1,215 @@
import { exec } from 'node:child_process'
import { randomBytes } from 'node:crypto'
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { promisify } from 'node:util'
const execAsync = promisify(exec)
export type JobStatus = 'pending' | 'processing' | 'completed' | 'failed'
export interface Job {
id: string
status: JobStatus
params: AbacusParams
error?: string
outputPath?: string
createdAt: Date
completedAt?: Date
progress?: string
}
export interface AbacusParams {
columns: number // Number of columns (1-13)
scaleFactor: number // Overall size multiplier
widthMm?: number // Optional: desired width in mm (overrides scaleFactor)
format: 'stl' | '3mf' | 'scad'
// 3MF color options
frameColor?: string
heavenBeadColor?: string
earthBeadColor?: string
decorationColor?: string
}
// In-memory job storage (can be upgraded to Redis later)
const jobs = new Map<string, Job>()
// Temporary directory for generated files
const TEMP_DIR = join(process.cwd(), 'tmp', '3d-jobs')
export class JobManager {
static generateJobId(): string {
return randomBytes(16).toString('hex')
}
static async createJob(params: AbacusParams): Promise<string> {
const jobId = JobManager.generateJobId()
const job: Job = {
id: jobId,
status: 'pending',
params,
createdAt: new Date(),
}
jobs.set(jobId, job)
// Start processing in background
JobManager.processJob(jobId).catch((error) => {
console.error(`Job ${jobId} failed:`, error)
const job = jobs.get(jobId)
if (job) {
job.status = 'failed'
job.error = error.message
job.completedAt = new Date()
}
})
return jobId
}
static getJob(jobId: string): Job | undefined {
return jobs.get(jobId)
}
static async processJob(jobId: string): Promise<void> {
const job = jobs.get(jobId)
if (!job) throw new Error('Job not found')
job.status = 'processing'
job.progress = 'Preparing workspace...'
// Create temp directory
await mkdir(TEMP_DIR, { recursive: true })
const outputFileName = `abacus-${jobId}.${job.params.format}`
const outputPath = join(TEMP_DIR, outputFileName)
try {
// Build OpenSCAD command
const scadPath = join(process.cwd(), 'public', '3d-models', 'abacus.scad')
const stlPath = join(process.cwd(), 'public', '3d-models', 'simplified.abacus.stl')
// If format is 'scad', just copy the file with custom parameters
if (job.params.format === 'scad') {
job.progress = 'Generating OpenSCAD file...'
const scadContent = await readFile(scadPath, 'utf-8')
const customizedScad = scadContent
.replace(/columns = \d+\.?\d*/, `columns = ${job.params.columns}`)
.replace(/scale_factor = \d+\.?\d*/, `scale_factor = ${job.params.scaleFactor}`)
await writeFile(outputPath, customizedScad)
job.outputPath = outputPath
job.status = 'completed'
job.completedAt = new Date()
job.progress = 'Complete!'
return
}
job.progress = 'Rendering 3D model...'
// Build command with parameters
const cmd = [
'openscad',
'-o',
outputPath,
'-D',
`'columns=${job.params.columns}'`,
'-D',
`'scale_factor=${job.params.scaleFactor}'`,
scadPath,
].join(' ')
console.log(`Executing: ${cmd}`)
// Execute OpenSCAD (with 60s timeout)
// Note: OpenSCAD may exit with non-zero status due to CGAL warnings
// but still produce valid output. We'll check file existence afterward.
try {
await execAsync(cmd, {
timeout: 60000,
cwd: join(process.cwd(), 'public', '3d-models'),
})
} catch (execError) {
// Log the error but don't throw yet - check if output was created
console.warn(`OpenSCAD reported errors, but checking if output was created:`, execError)
// Check if output file exists despite the error
try {
await readFile(outputPath)
console.log(`Output file created despite OpenSCAD warnings - proceeding`)
} catch (readError) {
// File doesn't exist, this is a real failure
console.error(`OpenSCAD execution failed and no output file created:`, execError)
if (execError instanceof Error) {
throw new Error(`OpenSCAD error: ${execError.message}`)
}
throw execError
}
}
job.progress = 'Finalizing...'
// Verify output exists and check file size
const fileBuffer = await readFile(outputPath)
const fileSizeMB = fileBuffer.length / (1024 * 1024)
// Maximum file size: 100MB (to prevent memory issues)
const MAX_FILE_SIZE_MB = 100
if (fileSizeMB > MAX_FILE_SIZE_MB) {
throw new Error(
`Generated file is too large (${fileSizeMB.toFixed(1)}MB). Maximum allowed is ${MAX_FILE_SIZE_MB}MB. Try reducing scale parameters.`
)
}
console.log(`Generated STL file size: ${fileSizeMB.toFixed(2)}MB`)
job.outputPath = outputPath
job.status = 'completed'
job.completedAt = new Date()
job.progress = 'Complete!'
console.log(`Job ${jobId} completed successfully`)
} catch (error) {
console.error(`Job ${jobId} failed:`, error)
job.status = 'failed'
job.error = error instanceof Error ? error.message : 'Unknown error occurred'
job.completedAt = new Date()
throw error
}
}
static async getJobOutput(jobId: string): Promise<Buffer> {
const job = jobs.get(jobId)
if (!job) throw new Error('Job not found')
if (job.status !== 'completed') throw new Error(`Job is ${job.status}, not completed`)
if (!job.outputPath) throw new Error('Output path not set')
return await readFile(job.outputPath)
}
static async cleanupJob(jobId: string): Promise<void> {
const job = jobs.get(jobId)
if (!job) return
if (job.outputPath) {
try {
await rm(job.outputPath)
} catch (error) {
console.error(`Failed to cleanup job ${jobId}:`, error)
}
}
jobs.delete(jobId)
}
// Cleanup old jobs (should be called periodically)
static async cleanupOldJobs(maxAgeMs = 3600000): Promise<void> {
const now = Date.now()
for (const [jobId, job] of jobs.entries()) {
const age = now - job.createdAt.getTime()
if (age > maxAgeMs) {
await JobManager.cleanupJob(jobId)
}
}
}
}

View File

@@ -0,0 +1,153 @@
/**
* Universal game stats types
*
* These types are used across ALL arcade games to record player performance.
* Supports: solo, competitive, cooperative, and head-to-head game modes.
*
* See: .claude/GAME_STATS_COMPARISON.md for detailed cross-game analysis
*/
import type { GameStatsBreakdown } from '@/db/schema/player-stats'
/**
* Standard game result that all arcade games must provide
*
* Supports:
* - 1-N players
* - Competitive (individual winners)
* - Cooperative (team wins/losses)
* - Solo completion
* - Head-to-head (2-player)
*/
export interface GameResult {
// Game identification
gameType: string // e.g., "matching", "complement-race", "memory-quiz"
// Player results (supports 1-N players)
playerResults: PlayerGameResult[]
// Timing
completedAt: number // timestamp
duration: number // milliseconds
// Optional game-specific data
metadata?: {
// For cooperative games (Memory Quiz, Card Sorting collaborative)
// When true: all players share win/loss outcome
isTeamVictory?: boolean
// For specific win conditions (Rithmomachia)
winCondition?: string // e.g., "HARMONY", "POINTS", "TIMEOUT"
// For game modes
gameMode?: string // e.g., "solo", "competitive", "cooperative"
// Extensible for other game-specific info
[key: string]: unknown
}
}
/**
* Individual player result within a game
*/
export interface PlayerGameResult {
playerId: string
// Outcome
won: boolean // For cooperative games: all players have same value
placement?: number // 1st, 2nd, 3rd place (for tournaments with 3+ players)
// Performance
score?: number
accuracy?: number // 0.0 - 1.0
completionTime?: number // milliseconds (player-specific)
// Game-specific metrics (stored as JSON in DB)
metrics?: {
// Matching
moves?: number
matchedPairs?: number
difficulty?: number
// Complement Race
streak?: number
correctAnswers?: number
totalQuestions?: number
// Memory Quiz
correct?: number
incorrect?: number
// Card Sorting
exactMatches?: number
inversions?: number
lcsLength?: number
// Rithmomachia
capturedPieces?: number
points?: number
// Extensible for future games
[key: string]: unknown
}
}
/**
* Stats update returned from API after recording a game
*/
export interface StatsUpdate {
playerId: string
previousStats: PlayerStatsData
newStats: PlayerStatsData
changes: {
gamesPlayed: number
wins: number
losses: number
}
}
/**
* Complete player stats data (from DB)
*/
export interface PlayerStatsData {
playerId: string
gamesPlayed: number
totalWins: number
totalLosses: number
bestTime: number | null
highestAccuracy: number
favoriteGameType: string | null
gameStats: Record<string, GameStatsBreakdown>
lastPlayedAt: Date | null
createdAt: Date
updatedAt: Date
}
/**
* Request body for recording a game result
*/
export interface RecordGameRequest {
gameResult: GameResult
}
/**
* Response from recording a game result
*/
export interface RecordGameResponse {
success: boolean
updates: StatsUpdate[]
}
/**
* Response from fetching player stats
*/
export interface GetPlayerStatsResponse {
stats: PlayerStatsData
}
/**
* Response from fetching all user's player stats
*/
export interface GetAllPlayerStatsResponse {
playerStats: PlayerStatsData[]
}

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

@@ -0,0 +1,27 @@
{
"permissions": {
"allow": [
"Bash(tree:*)",
"Bash(npx tsc:*)",
"Bash(npm test:*)",
"Bash(npm run test:run:*)",
"Bash(timeout 60 npm run test:run)",
"Bash(git add:*)",
"Bash(git rm:*)",
"Bash(git commit:*)",
"Bash(npm run build:*)",
"Bash(git reset:*)",
"Bash(cat:*)",
"Bash(pnpm --filter @soroban/abacus-react build:*)",
"Bash(git show:*)",
"Bash(pnpm build:*)",
"Bash(pnpm --filter @soroban/web build:*)",
"Bash(pnpm tsc:*)",
"Bash(AbacusReact.tsx)"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"sqlite"
]
}

View File

@@ -1,3 +1,137 @@
## [2.8.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.8.0...abacus-react-v2.8.1) (2025-11-04)
### Bug Fixes
* **abacus-react:** fix animations by preventing component remounting ([be7d4c4](https://github.com/antialias/soroban-abacus-flashcards/commit/be7d4c471327534a95c4c75372680c629b5f12c2))
* **abacus-react:** restore original AbacusReact measurements and positioning ([88c0baa](https://github.com/antialias/soroban-abacus-flashcards/commit/88c0baaad9b83b60ab8cdcad92070cc049d61cc7))
# [2.8.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.7.1...abacus-react-v2.8.0) (2025-11-04)
### Bug Fixes
* **docker:** add scripts, abacus-react, and tsx for production calendar generation ([33eb90e](https://github.com/antialias/soroban-abacus-flashcards/commit/33eb90e316f84650ae619f8c6c02c9e77c663d1b))
* **web:** generate styled-system artifacts during build ([293390a](https://github.com/antialias/soroban-abacus-flashcards/commit/293390ae350a6c6aa467410f68c735512104d9dd))
* **web:** move react-dom/server import to API route to satisfy Next.js ([00a8bc3](https://github.com/antialias/soroban-abacus-flashcards/commit/00a8bc3e5e8f044df280c4356d3605a852f82e84))
* **web:** prevent abacus overlap in composite calendar ([448f93c](https://github.com/antialias/soroban-abacus-flashcards/commit/448f93c1e2a7f86bc48e678d4599ca968c6d81d2)), closes [#f0f0f0](https://github.com/antialias/soroban-abacus-flashcards/issues/f0f0f0)
* **web:** use dynamic import for react-dom/server in API route ([4f93c7d](https://github.com/antialias/soroban-abacus-flashcards/commit/4f93c7d996732de4bc19e7acf2d4ce803cba88b6))
* **web:** use nested SVG elements to prevent coordinate space conflicts ([f9cbee8](https://github.com/antialias/soroban-abacus-flashcards/commit/f9cbee8fcdf80641f3b82a65fad6b8a3575525fc))
### Features
* **abacus-react:** add shared dimension calculator for consistent sizing ([e5ba772](https://github.com/antialias/soroban-abacus-flashcards/commit/e5ba772fde9839c22daec92007f052ca125c7695))
* **web:** add Typst-based preview endpoint with React Suspense ([599a758](https://github.com/antialias/soroban-abacus-flashcards/commit/599a758471c43ab0fc87301c5e7eeceed608062e))
* **web:** add year abacus to calendar header and make grid bolder ([867c7ee](https://github.com/antialias/soroban-abacus-flashcards/commit/867c7ee17251b8df13665bee9c0391961975e681)), closes [#333](https://github.com/antialias/soroban-abacus-flashcards/issues/333)
* **web:** optimize monthly calendar for single-page layout ([b277a89](https://github.com/antialias/soroban-abacus-flashcards/commit/b277a89415d1823455376c3e0f641b52f3394e7c))
* **web:** redesign monthly calendar as single composite SVG ([8ce8038](https://github.com/antialias/soroban-abacus-flashcards/commit/8ce8038baeea0b8b0fffe3215746958731bd9d6a))
## [2.7.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.7.0...abacus-react-v2.7.1) (2025-11-04)
### Bug Fixes
* add xmlns to AbacusStatic for Typst SVG parsing ([98cd019](https://github.com/antialias/soroban-abacus-flashcards/commit/98cd019d4af91d7ca4e7a88f700194273476afb7))
* **web:** use AbacusStatic for calendar SVG generation ([08c6a41](https://github.com/antialias/soroban-abacus-flashcards/commit/08c6a419e25d220560eba13d6db437145e6e61b8))
# [2.7.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.6.0...abacus-react-v2.7.0) (2025-11-04)
### Bug Fixes
* **web:** add dynamic export to rithmomachia page ([329e623](https://github.com/antialias/soroban-abacus-flashcards/commit/329e62321245ef62726c986c917f19a909a5b65e))
* **web:** fix Typst PDF generation path resolution ([7ce1287](https://github.com/antialias/soroban-abacus-flashcards/commit/7ce12875254a31d8acdb35ef5de7d36d215ccd92))
### Features
* **abacus-react:** add separate /static export path for React Server Components ([ed69f6b](https://github.com/antialias/soroban-abacus-flashcards/commit/ed69f6b917c543bbcaa4621a0e63745bee70f5bf))
* **web:** add test page for AbacusStatic RSC compatibility ([903dea2](https://github.com/antialias/soroban-abacus-flashcards/commit/903dea25844f1d2b3730fbcbd8478e7af1887663))
* **web:** improve calendar abacus preview styling ([8439727](https://github.com/antialias/soroban-abacus-flashcards/commit/8439727b152accf61f0c28158b92788510ca086e))
# [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)
### Bug Fixes
* adjust hero abacus position to avoid covering subtitle ([f03d341](https://github.com/antialias/soroban-abacus-flashcards/commit/f03d3413145cc7ddfba93728ecdec7eabea9ada6))
* configure favicon metadata and improve bead visibility ([e1369fa](https://github.com/antialias/soroban-abacus-flashcards/commit/e1369fa2754cd61745a2950e6cb767d6b08db38f))
* correct hero abacus scroll direction to flow with page content ([4232746](https://github.com/antialias/soroban-abacus-flashcards/commit/423274657c9698bba28f7246fbf48d8508d97ef9))
* 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))
* include column posts in favicon bounding box ([0b2f481](https://github.com/antialias/soroban-abacus-flashcards/commit/0b2f48106a939307b728c86fe2ea1be1e0247ea8))
* 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))
* reduce padding to minimize gap below last bead ([0e529be](https://github.com/antialias/soroban-abacus-flashcards/commit/0e529be789caf16e73f3e2ee77f52e243841aef4))
* resolve z-index layering and hero abacus visibility issues ([ed9a050](https://github.com/antialias/soroban-abacus-flashcards/commit/ed9a050d64db905e1328008f25dc0014e9a81999))
* 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 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))
### Features
* add 3D printing support for abacus models ([dafdfdd](https://github.com/antialias/soroban-abacus-flashcards/commit/dafdfdd233b53464b9825a8a9b5f2e6206fc54cb))
* 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 unified trophy abacus with hero mode integration ([6620418](https://github.com/antialias/soroban-abacus-flashcards/commit/6620418a704dcca810b511a5f394084521104e6b))
* 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))
* **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:** 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))
* 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)
# [2.2.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.1.0...abacus-react-v2.2.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

@@ -0,0 +1,286 @@
# Migration Guide: useAbacusState → useAbacusPlaceStates
## Overview
The `useAbacusState` hook has been **deprecated** in favor of the new `useAbacusPlaceStates` hook. This migration is part of a larger architectural improvement to eliminate array-based column indexing in favor of native place-value semantics.
## Why Migrate?
### Problems with `useAbacusState` (deprecated)
- ❌ Uses **array indices** for columns (0=leftmost, requires totalColumns)
- ❌ Requires threading `totalColumns` through component tree
- ❌ Index math creates confusion: `columnIndex = totalColumns - 1 - placeValue`
- ❌ Prone to off-by-one errors
- ❌ No support for BigInt (large numbers >15 digits)
### Benefits of `useAbacusPlaceStates` (new)
- ✅ Uses **place values** directly (0=ones, 1=tens, 2=hundreds)
- ✅ Native semantic meaning, no index conversion needed
- ✅ Cleaner architecture with `Map<PlaceValue, State>`
- ✅ Supports both `number` and `BigInt` for large values
- ✅ Type-safe with `ValidPlaceValues` (0-9)
- ✅ No totalColumns threading required
## Migration Steps
### 1. Update Hook Usage
**Before (deprecated):**
```tsx
import { useAbacusState } from '@soroban/abacus-react';
function MyComponent() {
const {
value,
setValue,
columnStates, // Array of column states
getColumnState,
setColumnState,
toggleBead
} = useAbacusState(123, 5); // totalColumns=5
// Need to calculate indices
const onesColumnIndex = 4; // rightmost
const tensColumnIndex = 3; // second from right
return <AbacusReact value={value} columns={5} />;
}
```
**After (new):**
```tsx
import { useAbacusPlaceStates } from '@soroban/abacus-react';
function MyComponent() {
const {
value,
setValue,
placeStates, // Map<PlaceValue, PlaceState>
getPlaceState,
setPlaceState,
toggleBeadAtPlace
} = useAbacusPlaceStates(123, 4); // maxPlaceValue=4 (0-4 = 5 columns)
// Direct place value access - no index math!
const onesState = getPlaceState(0);
const tensState = getPlaceState(1);
return <AbacusReact value={value} columns={5} />;
}
```
### 2. Update State Access Patterns
**Before (array indexing):**
```tsx
// Get state for tens column (need to know position in array)
const tensIndex = columnStates.length - 2; // second from right
const tensState = columnStates[tensIndex];
```
**After (place value):**
```tsx
// Get state for tens place - no calculation needed!
const tensState = getPlaceState(1); // 1 = tens place
```
### 3. Update State Manipulation
**Before:**
```tsx
// Toggle bead in ones column (need BeadConfig with column index)
toggleBead({
type: 'earth',
value: 1,
active: false,
position: 2,
placeValue: 0 // This was confusing - had place value BUT operated on column index
});
```
**After:**
```tsx
// Toggle bead at ones place - clean and semantic
toggleBeadAtPlace({
type: 'earth',
value: 1,
active: false,
position: 2,
placeValue: 0 // Now actually used as place value!
});
```
### 4. Update Iteration Logic
**Before (array iteration):**
```tsx
columnStates.forEach((state, columnIndex) => {
const placeValue = columnStates.length - 1 - columnIndex; // Manual conversion
console.log(`Column ${columnIndex} (place ${placeValue}):`, state);
});
```
**After (Map iteration):**
```tsx
placeStates.forEach((state, placeValue) => {
console.log(`Place ${placeValue}:`, state); // Direct access, no conversion!
});
```
## API Comparison
### useAbacusState (deprecated)
```typescript
function useAbacusState(
initialValue?: number,
targetColumns?: number
): {
value: number;
setValue: (newValue: number) => void;
columnStates: ColumnState[]; // Array
getColumnState: (columnIndex: number) => ColumnState;
setColumnState: (columnIndex: number, state: ColumnState) => void;
toggleBead: (bead: BeadConfig) => void;
}
```
### useAbacusPlaceStates (new)
```typescript
function useAbacusPlaceStates(
controlledValue?: number | bigint,
maxPlaceValue?: ValidPlaceValues
): {
value: number | bigint;
setValue: (newValue: number | bigint) => void;
placeStates: PlaceStatesMap; // Map
getPlaceState: (place: ValidPlaceValues) => PlaceState;
setPlaceState: (place: ValidPlaceValues, state: PlaceState) => void;
toggleBeadAtPlace: (bead: BeadConfig) => void;
}
```
## Complete Example
### Before: Array-based (deprecated)
```tsx
import { useState } from 'react';
import { useAbacusState, AbacusReact } from '@soroban/abacus-react';
function DeprecatedExample() {
const { value, setValue, columnStates } = useAbacusState(0, 3);
const handleAddTen = () => {
// Need to know array position of tens column
const totalColumns = columnStates.length;
const tensColumnIndex = totalColumns - 2; // Complex!
const current = columnStates[tensColumnIndex];
// Increment tens digit
const currentTensValue = (current.heavenActive ? 5 : 0) + current.earthActive;
const newTensValue = (currentTensValue + 1) % 10;
setValue(value + 10);
};
return (
<div>
<AbacusReact value={value} columns={3} interactive />
<button onClick={handleAddTen}>Add 10</button>
</div>
);
}
```
### After: Place-value based (new)
```tsx
import { useState } from 'react';
import { useAbacusPlaceStates, AbacusReact } from '@soroban/abacus-react';
function NewExample() {
const { value, setValue, getPlaceState } = useAbacusPlaceStates(0, 2);
const handleAddTen = () => {
// Direct access to tens place - simple!
const tensState = getPlaceState(1); // 1 = tens
// Increment tens digit
const currentTensValue = (tensState.heavenActive ? 5 : 0) + tensState.earthActive;
const newTensValue = (currentTensValue + 1) % 10;
if (typeof value === 'number') {
setValue(value + 10);
} else {
setValue(value + 10n);
}
};
return (
<div>
<AbacusReact value={value} columns={3} interactive />
<button onClick={handleAddTen}>Add 10</button>
</div>
);
}
```
## BigInt Support (New Feature)
The new hook supports BigInt for numbers exceeding JavaScript's safe integer limit:
```tsx
const { value, setValue } = useAbacusPlaceStates(
123456789012345678901234567890n, // BigInt!
29 // 30 digits (place values 0-29)
);
console.log(typeof value); // "bigint"
```
## Type Safety Improvements
The new hook uses branded types and strict typing:
```tsx
import type {
ValidPlaceValues, // 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
PlaceState,
PlaceStatesMap
} from '@soroban/abacus-react';
// Type-safe place value access
const onesState: PlaceState = getPlaceState(0);
const tensState: PlaceState = getPlaceState(1);
// Compile-time error for invalid place values
const invalidState = getPlaceState(15); // Error if maxPlaceValue < 15
```
## Timeline
- **Current**: Both hooks available, `useAbacusState` marked `@deprecated`
- **Next major version**: `useAbacusState` will be removed
- **Recommendation**: Migrate as soon as possible
## Getting Help
If you encounter issues during migration:
1. Check the [README.md](./README.md) for updated examples
2. Review [Storybook stories](./src) for usage patterns
3. Open an issue at https://github.com/anthropics/claude-code/issues
## Summary
| Feature | useAbacusState (old) | useAbacusPlaceStates (new) |
|---------|---------------------|---------------------------|
| Architecture | Array-based columns | Map-based place values |
| Index math | Required | Not needed |
| Semantic meaning | Indirect | Direct |
| BigInt support | ❌ No | ✅ Yes |
| Type safety | Basic | Enhanced |
| Column threading | Required | Not required |
| **Status** | ⚠️ Deprecated | ✅ Recommended |
**Bottom line:** The new hook eliminates complexity and makes your code more maintainable. Migration is straightforward - primarily renaming and removing index calculations.

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,289 @@ 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
// IMPORTANT: Use /static import path for RSC compatibility!
import { AbacusStatic } from '@soroban/abacus-react/static';
// ✅ 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
/>
```
**Import paths:**
- `@soroban/abacus-react` - Full package (client components with hooks/animations)
- `@soroban/abacus-react/static` - Server-compatible components only (no client code)
**Guaranteed Visual Consistency:**
Both `AbacusStatic` and `AbacusReact` share the same underlying layout engine. **Same props = same exact SVG output.** This ensures:
- Static previews match interactive versions pixel-perfect
- Server-rendered abaci look identical to client-rendered ones
- PDF generation produces accurate representations
- No visual discrepancies between environments
**Architecture: How We Guarantee Consistency**
The package uses a shared rendering architecture with dependency injection:
```
┌─────────────────────────────────────────────┐
│ Shared Utilities (AbacusUtils.ts) │
│ • calculateStandardDimensions() - Single │
│ source of truth for all layout dimensions│
│ • calculateBeadPosition() - Exact bead │
│ positioning using shared formulas │
└────────────┬────────────────────────────────┘
├──────────────────────────────────┐
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ AbacusStatic │ │ AbacusReact │
│ (Server/Static) │ │ (Interactive) │
└────────┬────────┘ └────────┬────────┘
│ │
└────────────┬───────────────────┘
┌────────────────────────┐
│ AbacusSVGRenderer │
│ • Pure SVG structure │
│ • Dependency injection │
│ • Bead component prop │
└────────────────────────┘
┌───────────────┴───────────────┐
↓ ↓
┌──────────────┐ ┌──────────────────┐
│ AbacusStatic │ │ AbacusAnimated │
│ Bead │ │ Bead │
│ (Simple SVG) │ │ (react-spring) │
└──────────────┘ └──────────────────┘
```
**Key Components:**
1. **`calculateStandardDimensions()`** - Returns complete layout dimensions (bar position, bead sizes, gaps, etc.)
2. **`calculateBeadPosition()`** - Calculates exact x,y coordinates for any bead
3. **`AbacusSVGRenderer`** - Shared SVG rendering component that accepts a bead component via dependency injection
4. **`AbacusStaticBead`** - Simple SVG shapes for static display (no hooks, RSC-compatible)
5. **`AbacusAnimatedBead`** - Client component with react-spring animations and gesture handling
This architecture eliminates code duplication (~560 lines removed in the refactor) while guaranteeing pixel-perfect consistency.
**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 |
| **Visual output** | **✅ Identical** | **✅ Identical** |
| Bundle size | 📦 Minimal | 📦 Full-featured |
| Use cases | Preview cards, thumbnails, static pages, PDFs | Interactive tutorials, games, tools |
```tsx
// Example: Server Component with static abacus cards
// app/flashcards/page.tsx
import { AbacusStatic } from '@soroban/abacus-react/static'
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 +389,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 +547,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 +619,149 @@ 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
```
### calculateStandardDimensions
**⚡ Core Architecture Function** - Calculate complete layout dimensions for consistent rendering.
This is the **single source of truth** for all layout dimensions, used internally by both `AbacusStatic` and `AbacusReact` to guarantee pixel-perfect consistency.
```tsx
import { calculateStandardDimensions } from '@soroban/abacus-react';
const dimensions = calculateStandardDimensions({
columns: 3,
scaleFactor: 1.5,
showNumbers: true,
columnLabels: ['ones', 'tens', 'hundreds']
});
// Returns complete layout info:
// {
// width, height, // SVG canvas size
// beadSize, // 12 * scaleFactor (standard bead size)
// rodSpacing, // 25 * scaleFactor (column spacing)
// rodWidth, // 3 * scaleFactor
// barThickness, // 2 * scaleFactor
// barY, // Reckoning bar Y position (30 * scaleFactor + labels)
// heavenY, earthY, // Inactive bead rest positions
// activeGap, // 1 * scaleFactor (gap to bar when active)
// inactiveGap, // 8 * scaleFactor (gap between active/inactive)
// adjacentSpacing, // 0.5 * scaleFactor (spacing between adjacent beads)
// padding, labelHeight, numbersHeight, totalColumns
// }
```
**Why this matters:** Same input parameters = same exact layout dimensions = pixel-perfect visual consistency across static and interactive displays.
### calculateBeadPosition
**⚡ Core Architecture Function** - Calculate exact x,y coordinates for any bead.
Used internally by `AbacusSVGRenderer` to position all beads consistently in both static and interactive modes.
```tsx
import { calculateBeadPosition, calculateStandardDimensions } from '@soroban/abacus-react';
const dimensions = calculateStandardDimensions({ columns: 3, scaleFactor: 1 });
const bead = {
type: 'heaven',
active: true,
position: 0,
placeValue: 1 // tens column
};
const position = calculateBeadPosition(bead, dimensions);
// Returns: { x: 25, y: 29 } // exact pixel coordinates
```
Useful for custom rendering or positioning tooltips/overlays relative to specific beads.
## Educational Use Cases
### Interactive Math Lessons
@@ -362,14 +824,41 @@ Full TypeScript definitions included:
```tsx
import {
// Components
AbacusReact,
// Hooks
useAbacusDiff,
useAbacusState,
useAbacusDimensions,
// Utility Functions
numberToAbacusState,
abacusStateToNumber,
calculateBeadDiff,
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual,
calculateStandardDimensions, // NEW: Shared layout calculator
calculateBeadPosition, // NEW: Bead position calculator
// Theme Presets
ABACUS_THEMES,
// Types
AbacusConfig,
BeadConfig,
BeadClickEvent,
AbacusCustomStyles,
AbacusOverlay,
AbacusCallbacks,
useAbacusDimensions
AbacusState,
BeadState,
BeadDiffResult,
BeadDiffOutput,
AbacusThemeName,
AbacusLayoutDimensions, // NEW: Complete layout dimensions type
BeadPositionConfig // NEW: Bead config for position calculation
} from '@soroban/abacus-react';
// All interfaces fully typed for excellent developer experience

View File

@@ -10,6 +10,11 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
},
"./static": {
"types": "./dist/static.d.ts",
"import": "./dist/static.es.js",
"require": "./dist/static.cjs.js"
}
},
"files": [

View File

@@ -0,0 +1,341 @@
/**
* Abacus 3D Enhancement Styles
* Three levels of progressive enhancement:
* - subtle: CSS perspective + shadows
* - realistic: Lighting + material design
* - delightful: Physics + micro-interactions
*/
/* ============================================
PROPOSAL 1: SUBTLE (CSS Perspective + Shadows)
============================================ */
.abacus-3d-container {
display: inline-block;
position: relative;
}
.abacus-3d-container.enhanced-subtle {
perspective: 1200px;
perspective-origin: 50% 50%;
}
.abacus-3d-container.enhanced-subtle .abacus-svg {
transform-style: preserve-3d;
transform: rotateX(2deg) rotateY(-1deg);
transition: transform 0.3s ease-out;
}
.abacus-3d-container.enhanced-subtle:hover .abacus-svg {
transform: rotateX(0deg) rotateY(0deg);
}
/* Bead depth shadows - subtle */
.abacus-3d-container.enhanced-subtle .abacus-bead.active {
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.25))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
}
.abacus-3d-container.enhanced-subtle .abacus-bead.inactive {
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
/* Frame depth */
.abacus-3d-container.enhanced-subtle rect[class*="column-post"],
.abacus-3d-container.enhanced-subtle rect[class*="reckoning-bar"] {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
/* ============================================
PROPOSAL 2: REALISTIC (Lighting + Materials)
============================================ */
.abacus-3d-container.enhanced-realistic {
perspective: 1200px;
perspective-origin: 50% 50%;
}
.abacus-3d-container.enhanced-realistic .abacus-svg {
transform-style: preserve-3d;
transform: rotateX(3deg) rotateY(-2deg);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.abacus-3d-container.enhanced-realistic:hover .abacus-svg {
transform: rotateX(0deg) rotateY(0deg);
}
/* Enhanced bead shadows with ambient occlusion */
.abacus-3d-container.enhanced-realistic .abacus-bead.active {
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.3))
drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2))
drop-shadow(0 1px 3px rgba(0, 0, 0, 0.15));
}
.abacus-3d-container.enhanced-realistic .abacus-bead.inactive {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15))
drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
/* Frame with depth and texture */
.abacus-3d-container.enhanced-realistic rect[class*="column-post"],
.abacus-3d-container.enhanced-realistic rect[class*="reckoning-bar"] {
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.25))
drop-shadow(0 1px 3px rgba(0, 0, 0, 0.15));
}
/* Material-specific enhancements */
.abacus-3d-container.enhanced-realistic .bead-material-glossy {
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.3))
drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2))
drop-shadow(0 0 4px rgba(255, 255, 255, 0.3));
}
.abacus-3d-container.enhanced-realistic .bead-material-satin {
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.25))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
}
.abacus-3d-container.enhanced-realistic .bead-material-matte {
filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2))
drop-shadow(0 1px 3px rgba(0, 0, 0, 0.1));
}
/* Wood grain texture overlay */
.abacus-3d-container.enhanced-realistic .frame-wood {
opacity: 0.4;
mix-blend-mode: multiply;
pointer-events: none;
}
/* Lighting effects - top-down */
.abacus-3d-container.enhanced-realistic.lighting-top-down::before {
content: '';
position: absolute;
top: -10%;
left: 50%;
width: 120%;
height: 30%;
transform: translateX(-50%);
background: radial-gradient(
ellipse at center,
rgba(255, 255, 255, 0.15) 0%,
transparent 70%
);
pointer-events: none;
z-index: 10;
}
/* Lighting effects - ambient */
.abacus-3d-container.enhanced-realistic.lighting-ambient::before {
content: '';
position: absolute;
inset: -10%;
background: radial-gradient(
circle at 50% 50%,
rgba(255, 255, 255, 0.08) 0%,
transparent 60%
);
pointer-events: none;
z-index: 10;
}
/* Lighting effects - dramatic */
.abacus-3d-container.enhanced-realistic.lighting-dramatic::before {
content: '';
position: absolute;
top: -20%;
left: -10%;
width: 60%;
height: 60%;
background: radial-gradient(
ellipse at center,
rgba(255, 255, 255, 0.25) 0%,
transparent 50%
);
pointer-events: none;
z-index: 10;
}
/* ============================================
PROPOSAL 3: DELIGHTFUL (Physics + Micro-interactions)
============================================ */
.abacus-3d-container.enhanced-delightful {
perspective: 1400px;
perspective-origin: 50% 50%;
}
.abacus-3d-container.enhanced-delightful .abacus-svg {
transform-style: preserve-3d;
transform: rotateX(4deg) rotateY(-3deg);
transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.abacus-3d-container.enhanced-delightful:hover .abacus-svg {
transform: rotateX(1deg) rotateY(-0.5deg);
}
/* Maximum depth shadows */
.abacus-3d-container.enhanced-delightful .abacus-bead.active {
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.35))
drop-shadow(0 4px 8px rgba(0, 0, 0, 0.25))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
}
.abacus-3d-container.enhanced-delightful .abacus-bead.inactive {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15))
drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
}
/* Hover parallax effect */
.abacus-3d-container.enhanced-delightful.parallax-enabled .abacus-bead {
transition: transform 0.15s ease-out, filter 0.15s ease-out;
}
.abacus-3d-container.enhanced-delightful.parallax-enabled .abacus-bead.parallax-lift {
transform: translateZ(4px) scale(1.02);
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.4))
drop-shadow(0 5px 10px rgba(0, 0, 0, 0.3));
}
/* Clack ripple effect */
@keyframes clack-ripple {
0% {
r: 0;
opacity: 0.6;
}
100% {
r: 25;
opacity: 0;
}
}
.clack-ripple {
fill: none;
stroke: currentColor;
stroke-width: 2;
opacity: 0;
}
.clack-ripple.animating {
animation: clack-ripple 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Particle effects */
@keyframes particle-rise {
0% {
transform: translateY(0) scale(1);
opacity: 0.8;
}
100% {
transform: translateY(-20px) scale(0);
opacity: 0;
}
}
@keyframes particle-sparkle {
0%, 100% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(180deg);
}
}
.particle {
pointer-events: none;
}
.particle.particle-subtle {
animation: particle-rise 0.5s ease-out forwards;
}
.particle.particle-sparkle {
animation: particle-sparkle 0.6s ease-in-out forwards;
}
/* Enhanced lighting with multiple sources */
.abacus-3d-container.enhanced-delightful::before {
content: '';
position: absolute;
top: -15%;
left: 50%;
width: 140%;
height: 40%;
transform: translateX(-50%);
background: radial-gradient(
ellipse at center,
rgba(255, 255, 255, 0.2) 0%,
rgba(255, 255, 255, 0.05) 50%,
transparent 80%
);
pointer-events: none;
z-index: 10;
}
.abacus-3d-container.enhanced-delightful::after {
content: '';
position: absolute;
bottom: -10%;
left: 50%;
width: 100%;
height: 20%;
transform: translateX(-50%);
background: radial-gradient(
ellipse at center,
rgba(0, 0, 0, 0.1) 0%,
transparent 60%
);
pointer-events: none;
z-index: -1;
}
/* Frame depth enhancement */
.abacus-3d-container.enhanced-delightful rect[class*="column-post"],
.abacus-3d-container.enhanced-delightful rect[class*="reckoning-bar"] {
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2))
drop-shadow(0 0 2px rgba(0, 0, 0, 0.1));
}
/* 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 */
@media (prefers-reduced-motion: reduce) {
.abacus-3d-container.enhanced-subtle .abacus-svg,
.abacus-3d-container.enhanced-realistic .abacus-svg,
.abacus-3d-container.enhanced-delightful .abacus-svg {
transition: none;
transform: none;
}
.abacus-3d-container.enhanced-delightful.parallax-enabled .abacus-bead {
transition: none;
}
.clack-ripple.animating {
animation: none;
}
.particle {
display: none;
}
}
/* Performance optimization - will-change hints */
.abacus-3d-container.enhanced-delightful .abacus-bead {
will-change: transform, filter;
}
.abacus-3d-container.enhanced-realistic .abacus-bead.active {
will-change: filter;
}

View File

@@ -0,0 +1,225 @@
/**
* Utility functions for 3D abacus effects
* Includes gradient generation, color manipulation, and material definitions
*/
import type { BeadMaterial, LightingStyle } from "./AbacusReact";
/**
* Darken a hex color by a given amount (0-1)
*/
export function darkenColor(hex: string, amount: number): string {
// Remove # if present
const color = hex.replace('#', '');
// Parse RGB
const r = parseInt(color.substring(0, 2), 16);
const g = parseInt(color.substring(2, 4), 16);
const b = parseInt(color.substring(4, 6), 16);
// Darken
const newR = Math.max(0, Math.floor(r * (1 - amount)));
const newG = Math.max(0, Math.floor(g * (1 - amount)));
const newB = Math.max(0, Math.floor(b * (1 - amount)));
// Convert back to hex
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}
/**
* Lighten a hex color by a given amount (0-1)
*/
export function lightenColor(hex: string, amount: number): string {
// Remove # if present
const color = hex.replace('#', '');
// Parse RGB
const r = parseInt(color.substring(0, 2), 16);
const g = parseInt(color.substring(2, 4), 16);
const b = parseInt(color.substring(4, 6), 16);
// Lighten
const newR = Math.min(255, Math.floor(r + (255 - r) * amount));
const newG = Math.min(255, Math.floor(g + (255 - g) * amount));
const newB = Math.min(255, Math.floor(b + (255 - b) * amount));
// Convert back to hex
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
}
/**
* Generate an SVG radial gradient for a bead based on material type
*/
export function getBeadGradient(
id: string,
color: string,
material: BeadMaterial = "satin",
active: boolean = true
): string {
const baseColor = active ? color : "rgb(211, 211, 211)";
switch (material) {
case "glossy":
// High shine with strong highlight
return `
<radialGradient id="${id}" cx="30%" cy="30%">
<stop offset="0%" stop-color="${lightenColor(baseColor, 0.6)}" stop-opacity="0.8" />
<stop offset="20%" stop-color="${lightenColor(baseColor, 0.3)}" />
<stop offset="50%" stop-color="${baseColor}" />
<stop offset="100%" stop-color="${darkenColor(baseColor, 0.4)}" />
</radialGradient>
`;
case "matte":
// Subtle, no shine
return `
<radialGradient id="${id}" cx="50%" cy="50%">
<stop offset="0%" stop-color="${lightenColor(baseColor, 0.1)}" />
<stop offset="80%" stop-color="${baseColor}" />
<stop offset="100%" stop-color="${darkenColor(baseColor, 0.15)}" />
</radialGradient>
`;
case "satin":
default:
// Medium shine, balanced
return `
<radialGradient id="${id}" cx="35%" cy="35%">
<stop offset="0%" stop-color="${lightenColor(baseColor, 0.4)}" stop-opacity="0.9" />
<stop offset="35%" stop-color="${lightenColor(baseColor, 0.15)}" />
<stop offset="70%" stop-color="${baseColor}" />
<stop offset="100%" stop-color="${darkenColor(baseColor, 0.25)}" />
</radialGradient>
`;
}
}
/**
* Generate shadow definition based on lighting style
*/
export function getLightingFilter(lighting: LightingStyle = "top-down"): string {
switch (lighting) {
case "dramatic":
return `
drop-shadow(0 8px 16px rgba(0, 0, 0, 0.4))
drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3))
`;
case "ambient":
return `
drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2))
drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15))
`;
case "top-down":
default:
return `
drop-shadow(0 6px 12px rgba(0, 0, 0, 0.3))
drop-shadow(0 3px 6px rgba(0, 0, 0, 0.2))
`;
}
}
/**
* Calculate Z-depth for a bead based on enhancement level and state
*/
export function getBeadZDepth(
enhanced3d: boolean | "subtle" | "realistic",
active: boolean
): number {
if (!enhanced3d || enhanced3d === true) return 0;
if (!active) return 0;
switch (enhanced3d) {
case "subtle":
return 6;
case "realistic":
return 10;
default:
return 0;
}
}
/**
* 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.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>
`;
}
/**
* Get container class names for 3D enhancement level
*/
export function get3DContainerClasses(
enhanced3d: boolean | "subtle" | "realistic" | undefined,
lighting?: LightingStyle
): string {
const classes: string[] = ["abacus-3d-container"];
if (!enhanced3d) return classes.join(" ");
// Add enhancement level
if (enhanced3d === true || enhanced3d === "subtle") {
classes.push("enhanced-subtle");
} else if (enhanced3d === "realistic") {
classes.push("enhanced-realistic");
}
// Add lighting class
if (lighting && enhanced3d !== "subtle") {
classes.push(`lighting-${lighting}`);
}
return classes.join(" ");
}
/**
* Generate unique gradient ID for a bead
*/
export function getBeadGradientId(
columnIndex: number,
beadType: "heaven" | "earth",
position: number,
material: BeadMaterial
): string {
return `bead-gradient-${columnIndex}-${beadType}-${position}-${material}`;
}
/**
* Physics config for different enhancement levels
*/
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic") {
const base = {
tension: 300,
friction: 22,
mass: 0.5,
clamp: false
};
if (!enhanced3d || enhanced3d === "subtle") {
return { ...base, clamp: true };
}
// realistic
return {
tension: 320,
friction: 24,
mass: 0.6,
clamp: false
};
}

View File

@@ -0,0 +1,352 @@
'use client'
/**
* AbacusAnimatedBead - Interactive bead component for AbacusReact (Core Architecture)
*
* This is the **client-side bead component** injected into AbacusSVGRenderer by AbacusReact.
* It provides animations and interactivity while the parent renderer handles positioning.
*
* ## Architecture Role:
* - Injected into `AbacusSVGRenderer` via dependency injection (BeadComponent prop)
* - Receives x,y position from `calculateBeadPosition()` (already calculated)
* - Adds animations and interactions on top of the shared layout
* - Used ONLY by AbacusReact (requires "use client")
*
* ## Features:
* - ✅ React Spring animations for smooth position changes
* - ✅ Drag gesture handling with @use-gesture/react
* - ✅ Direction indicators for tutorials (pulsing arrows)
* - ✅ 3D effects and gradients
* - ✅ Click and hover interactions
*
* ## Comparison:
* - `AbacusStaticBead` - Simple SVG shapes (no animations, RSC-compatible)
* - `AbacusAnimatedBead` - This component (animations, gestures, client-only)
*
* Both receive the same position from `calculateBeadPosition()`, ensuring visual consistency.
*/
import React, { useCallback, useRef } from 'react'
import { useSpring, animated, to } from '@react-spring/web'
import { useDrag } from '@use-gesture/react'
import type { BeadComponentProps } from './AbacusSVGRenderer'
import type { BeadConfig } from './AbacusReact'
interface AnimatedBeadProps extends BeadComponentProps {
// Animation controls
enableAnimation: boolean
physicsConfig: any
// Gesture handling
enableGestures: boolean
onGestureToggle?: (bead: BeadConfig, direction: 'activate' | 'deactivate') => void
// Direction indicators (for tutorials)
showDirectionIndicator?: boolean
direction?: 'activate' | 'deactivate'
isCurrentStep?: boolean
// 3D effects
enhanced3d?: 'none' | 'subtle' | 'realistic' | 'delightful'
columnIndex?: number
}
export function AbacusAnimatedBead({
bead,
x,
y,
size,
shape,
color,
hideInactiveBeads,
customStyle,
onClick,
onMouseEnter,
onMouseLeave,
onRef,
enableAnimation,
physicsConfig,
enableGestures,
onGestureToggle,
showDirectionIndicator,
direction,
isCurrentStep,
enhanced3d = 'none',
columnIndex,
}: AnimatedBeadProps) {
// x, y are already calculated by AbacusSVGRenderer
// Spring animation for position
const [{ springX, springY }, api] = useSpring(() => ({
springX: x,
springY: y,
config: physicsConfig,
}))
// Arrow pulse animation for direction indicators
const [{ arrowPulse }, arrowApi] = useSpring(() => ({
arrowPulse: 1,
config: enableAnimation ? { tension: 200, friction: 10 } : { duration: 0 },
}))
const gestureStateRef = useRef({
isDragging: false,
lastDirection: null as 'activate' | 'deactivate' | null,
startY: 0,
threshold: size * 0.3,
hasGestureTriggered: false,
})
// Calculate gesture direction based on bead type
const getGestureDirection = useCallback(
(deltaY: number) => {
const movement = Math.abs(deltaY)
if (movement < gestureStateRef.current.threshold) return null
if (bead.type === 'heaven') {
return deltaY > 0 ? 'activate' : 'deactivate'
} else {
return deltaY < 0 ? 'activate' : 'deactivate'
}
},
[bead.type, size]
)
// Gesture handler
const bind = enableGestures
? useDrag(
({ event, movement: [, deltaY], first, active }) => {
if (first) {
event?.preventDefault()
gestureStateRef.current.isDragging = true
gestureStateRef.current.lastDirection = null
gestureStateRef.current.hasGestureTriggered = false
return
}
if (!active || !gestureStateRef.current.isDragging) {
if (!active) {
gestureStateRef.current.isDragging = false
gestureStateRef.current.lastDirection = null
setTimeout(() => {
gestureStateRef.current.hasGestureTriggered = false
}, 100)
}
return
}
const currentDirection = getGestureDirection(deltaY)
if (
currentDirection &&
currentDirection !== gestureStateRef.current.lastDirection
) {
gestureStateRef.current.lastDirection = currentDirection
gestureStateRef.current.hasGestureTriggered = true
onGestureToggle?.(bead, currentDirection)
}
},
{
enabled: enableGestures,
preventDefault: true,
}
)
: () => ({})
// Update spring animation when position changes
React.useEffect(() => {
if (enableAnimation) {
api.start({ springX: x, springY: y, config: physicsConfig })
} else {
api.set({ springX: x, springY: y })
}
}, [x, y, enableAnimation, api, physicsConfig])
// Pulse animation for direction indicators
React.useEffect(() => {
if (showDirectionIndicator && direction && isCurrentStep) {
const startPulse = () => {
arrowApi.start({
from: { arrowPulse: 1 },
to: async (next) => {
await next({ arrowPulse: 1.3 })
await next({ arrowPulse: 1 })
},
loop: true,
})
}
const timeoutId = setTimeout(startPulse, 200)
return () => {
clearTimeout(timeoutId)
arrowApi.stop()
}
} else {
arrowApi.set({ arrowPulse: 1 })
}
}, [showDirectionIndicator, direction, isCurrentStep, arrowApi])
// Render bead shape
const renderShape = () => {
const halfSize = size / 2
// Determine fill - use gradient for realistic mode, otherwise use color
let fillValue = customStyle?.fill || 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})`
}
}
const opacity = bead.active ? (customStyle?.opacity ?? 1) : 0.3
const stroke = customStyle?.stroke || '#000'
const strokeWidth = customStyle?.strokeWidth || 0.5
switch (shape) {
case 'diamond':
return (
<polygon
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
fill={fillValue}
stroke={stroke}
strokeWidth={strokeWidth}
opacity={opacity}
/>
)
case 'square':
return (
<rect
width={size}
height={size}
fill={fillValue}
stroke={stroke}
strokeWidth={strokeWidth}
rx="1"
opacity={opacity}
/>
)
case 'circle':
default:
return (
<circle
cx={halfSize}
cy={halfSize}
r={halfSize}
fill={fillValue}
stroke={stroke}
strokeWidth={strokeWidth}
opacity={opacity}
/>
)
}
}
// Calculate offsets for shape positioning
const getXOffset = () => {
return shape === 'diamond' ? size * 0.7 : size / 2
}
const getYOffset = () => {
return size / 2
}
// Use animated.g if animations enabled, otherwise regular g
const GElement = enableAnimation ? animated.g : 'g'
const DirectionIndicatorG =
enableAnimation && showDirectionIndicator && direction ? animated.g : 'g'
// Build style object
const beadStyle: any = enableAnimation
? {
transform: to(
[springX, springY],
(sx, sy) => `translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`
),
cursor: enableGestures ? 'grab' : onClick ? 'pointer' : 'default',
touchAction: 'none' as const,
transition: 'opacity 0.2s ease-in-out',
}
: {
transform: `translate(${x - getXOffset()}px, ${y - getYOffset()}px)`,
cursor: enableGestures ? 'grab' : onClick ? 'pointer' : 'default',
touchAction: 'none' as const,
transition: 'opacity 0.2s ease-in-out',
}
// Don't render inactive beads if hideInactiveBeads is true
if (!bead.active && hideInactiveBeads) {
return null
}
const handleClick = (event: React.MouseEvent) => {
// Prevent click if gesture was triggered
if (gestureStateRef.current.hasGestureTriggered) {
event.preventDefault()
return
}
onClick?.(bead, event)
}
return (
<>
<GElement
className={`abacus-bead ${bead.active ? 'active' : 'inactive'} ${hideInactiveBeads && !bead.active ? 'hidden-inactive' : ''}`}
style={beadStyle}
{...bind()}
onClick={handleClick}
onMouseEnter={(e) => onMouseEnter?.(bead, e as any)}
onMouseLeave={(e) => onMouseLeave?.(bead, e as any)}
ref={(el) => onRef?.(bead, el as any)}
>
{renderShape()}
</GElement>
{/* Direction indicator for tutorials */}
{showDirectionIndicator && direction && (
<DirectionIndicatorG
className="direction-indicator"
style={
(enableAnimation
? {
transform: to(
[springX, springY, arrowPulse],
(sx, sy, pulse) => {
const centerX = shape === 'diamond' ? size * 0.7 : size / 2
const centerY = size / 2
return `translate(${sx - centerX}px, ${sy - centerY}px) scale(${pulse})`
}
),
pointerEvents: 'none' as const,
}
: {
transform: `translate(${x - (shape === 'diamond' ? size * 0.7 : size / 2)}px, ${y - size / 2}px) scale(1)`,
pointerEvents: 'none' as const,
}) as any
}
>
<circle
cx={shape === 'diamond' ? size * 0.7 : size / 2}
cy={size / 2}
r={size * 0.8}
fill="rgba(255, 165, 0, 0.3)"
stroke="orange"
strokeWidth="2"
/>
<text
x={shape === 'diamond' ? size * 0.7 : size / 2}
y={size / 2}
textAnchor="middle"
dy=".35em"
fontSize={size * 0.8}
fill="orange"
fontWeight="bold"
>
{direction === 'activate' ? '↓' : '↑'}
</text>
</DirectionIndicatorG>
)}
</>
)
}

View File

@@ -0,0 +1,468 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AbacusDisplayProvider, useAbacusDisplay, useAbacusConfig } from './AbacusContext';
import { AbacusReact } from './AbacusReact';
import { StandaloneBead } from './StandaloneBead';
import React from 'react';
const meta: Meta<typeof AbacusDisplayProvider> = {
title: 'Soroban/Components/AbacusDisplayProvider',
component: AbacusDisplayProvider,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Context provider for managing global abacus display configuration. Automatically persists settings to localStorage and provides SSR-safe hydration.'
}
}
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// Basic Provider Usage
export const BasicUsage: Story = {
name: 'Basic Provider Usage',
render: () => (
<AbacusDisplayProvider>
<div style={{ textAlign: 'center' }}>
<AbacusReact value={123} columns={3} showNumbers />
<p style={{ marginTop: '16px', fontSize: '14px', color: '#6b7280' }}>
This abacus inherits all settings from the provider
</p>
</div>
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Wrap your components with AbacusDisplayProvider to provide consistent configuration'
}
}
}
};
// With Initial Config
export const WithInitialConfig: Story = {
name: 'With Initial Config',
render: () => (
<AbacusDisplayProvider
initialConfig={{
beadShape: 'circle',
colorScheme: 'heaven-earth',
colorPalette: 'colorblind',
}}
>
<div style={{ textAlign: 'center' }}>
<AbacusReact value={456} columns={3} showNumbers />
<p style={{ marginTop: '16px', fontSize: '14px', color: '#6b7280' }}>
Circle beads with heaven-earth coloring (colorblind palette)
</p>
</div>
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Provide initial configuration to override defaults'
}
}
}
};
// Interactive Config Demo
const ConfigDemo: React.FC = () => {
const { config, updateConfig, resetToDefaults } = useAbacusDisplay();
return (
<div style={{ textAlign: 'center' }}>
<div style={{ marginBottom: '20px' }}>
<AbacusReact value={789} columns={3} showNumbers scaleFactor={1.2} />
</div>
<div style={{
display: 'inline-block',
textAlign: 'left',
padding: '20px',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: '#f9fafb'
}}>
<h3 style={{ marginTop: 0, fontSize: '16px' }}>Configuration Controls</h3>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '13px', marginBottom: '4px', fontWeight: '500' }}>
Bead Shape:
</label>
<select
value={config.beadShape}
onChange={(e) => updateConfig({ beadShape: e.target.value as any })}
style={{
width: '100%',
padding: '6px 8px',
borderRadius: '4px',
border: '1px solid #d1d5db'
}}
>
<option value="diamond">Diamond</option>
<option value="circle">Circle</option>
<option value="square">Square</option>
</select>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '13px', marginBottom: '4px', fontWeight: '500' }}>
Color Scheme:
</label>
<select
value={config.colorScheme}
onChange={(e) => updateConfig({ colorScheme: e.target.value as any })}
style={{
width: '100%',
padding: '6px 8px',
borderRadius: '4px',
border: '1px solid #d1d5db'
}}
>
<option value="monochrome">Monochrome</option>
<option value="place-value">Place Value</option>
<option value="heaven-earth">Heaven-Earth</option>
<option value="alternating">Alternating</option>
</select>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '13px', marginBottom: '4px', fontWeight: '500' }}>
Color Palette:
</label>
<select
value={config.colorPalette}
onChange={(e) => updateConfig({ colorPalette: e.target.value as any })}
style={{
width: '100%',
padding: '6px 8px',
borderRadius: '4px',
border: '1px solid #d1d5db'
}}
>
<option value="default">Default</option>
<option value="colorblind">Colorblind</option>
<option value="mnemonic">Mnemonic</option>
<option value="grayscale">Grayscale</option>
<option value="nature">Nature</option>
</select>
</div>
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'flex', alignItems: 'center', fontSize: '13px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={config.animated}
onChange={(e) => updateConfig({ animated: e.target.checked })}
style={{ marginRight: '6px' }}
/>
Enable Animations
</label>
</div>
<button
onClick={resetToDefaults}
style={{
width: '100%',
padding: '8px',
borderRadius: '4px',
border: '1px solid #d1d5db',
background: 'white',
cursor: 'pointer',
fontSize: '13px'
}}
>
Reset to Defaults
</button>
<div style={{
marginTop: '12px',
padding: '8px',
background: '#fef3c7',
borderRadius: '4px',
fontSize: '11px',
color: '#92400e'
}}>
💾 Changes are automatically saved to localStorage
</div>
</div>
</div>
);
};
export const InteractiveConfiguration: Story = {
name: 'Interactive Configuration',
render: () => (
<AbacusDisplayProvider>
<ConfigDemo />
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Use the useAbacusDisplay hook to access and modify configuration. Changes persist across sessions via localStorage.'
}
}
}
};
// Consistent Styling Across Components
export const ConsistentStyling: Story = {
name: 'Consistent Styling',
render: () => (
<AbacusDisplayProvider
initialConfig={{
beadShape: 'square',
colorScheme: 'place-value',
colorPalette: 'nature',
}}
>
<div>
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '12px' }}>Multiple Abacuses</h3>
<div style={{ display: 'flex', gap: '20px', justifyContent: 'center' }}>
<AbacusReact value={12} columns={2} showNumbers />
<AbacusReact value={345} columns={3} showNumbers />
<AbacusReact value={6789} columns={4} showNumbers />
</div>
</div>
<div style={{ textAlign: 'center' }}>
<h3 style={{ fontSize: '16px', marginBottom: '12px' }}>Standalone Beads</h3>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<StandaloneBead size={32} color="#ef4444" />
<StandaloneBead size={32} color="#f97316" />
<StandaloneBead size={32} color="#eab308" />
<StandaloneBead size={32} color="#22c55e" />
<StandaloneBead size={32} color="#3b82f6" />
</div>
</div>
<p style={{ marginTop: '16px', fontSize: '14px', color: '#6b7280', textAlign: 'center' }}>
All components share the same bead shape (square) from the provider
</p>
</div>
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Provider ensures consistent styling across all abacus components and standalone beads'
}
}
}
};
// Using the Config Hook
const ConfigDisplay: React.FC = () => {
const config = useAbacusConfig();
return (
<div style={{
padding: '20px',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: 'white',
fontFamily: 'monospace',
fontSize: '12px'
}}>
<h3 style={{ marginTop: 0, fontSize: '14px', fontFamily: 'sans-serif' }}>Current Configuration</h3>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{JSON.stringify(config, null, 2)}
</pre>
</div>
);
};
export const UsingConfigHook: Story = {
name: 'Using useAbacusConfig Hook',
render: () => (
<AbacusDisplayProvider
initialConfig={{
beadShape: 'diamond',
colorScheme: 'place-value',
animated: true,
}}
>
<div style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}>
<AbacusReact value={234} columns={3} showNumbers />
<ConfigDisplay />
</div>
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Use useAbacusConfig() hook to read configuration values in your components'
}
}
}
};
// localStorage Persistence Demo
const PersistenceDemo: React.FC = () => {
const { config, updateConfig } = useAbacusDisplay();
const [hasChanges, setHasChanges] = React.useState(false);
const handleChange = (updates: any) => {
updateConfig(updates);
setHasChanges(true);
setTimeout(() => setHasChanges(false), 2000);
};
return (
<div style={{ textAlign: 'center' }}>
<AbacusReact value={555} columns={3} showNumbers scaleFactor={1.2} />
<div style={{
marginTop: '20px',
padding: '16px',
border: '1px solid #e5e7eb',
borderRadius: '8px',
background: '#f9fafb',
maxWidth: '300px',
marginLeft: 'auto',
marginRight: 'auto'
}}>
<h4 style={{ marginTop: 0, fontSize: '14px' }}>Try changing settings:</h4>
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px', justifyContent: 'center' }}>
<button
onClick={() => handleChange({ beadShape: 'diamond' })}
style={{
padding: '6px 12px',
borderRadius: '4px',
border: config.beadShape === 'diamond' ? '2px solid #8b5cf6' : '1px solid #d1d5db',
background: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Diamond
</button>
<button
onClick={() => handleChange({ beadShape: 'circle' })}
style={{
padding: '6px 12px',
borderRadius: '4px',
border: config.beadShape === 'circle' ? '2px solid #8b5cf6' : '1px solid #d1d5db',
background: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Circle
</button>
<button
onClick={() => handleChange({ beadShape: 'square' })}
style={{
padding: '6px 12px',
borderRadius: '4px',
border: config.beadShape === 'square' ? '2px solid #8b5cf6' : '1px solid #d1d5db',
background: 'white',
cursor: 'pointer',
fontSize: '12px'
}}
>
Square
</button>
</div>
{hasChanges && (
<div style={{
padding: '8px',
background: '#dcfce7',
borderRadius: '4px',
fontSize: '12px',
color: '#166534',
marginBottom: '12px'
}}>
Saved to localStorage!
</div>
)}
<p style={{
margin: 0,
fontSize: '11px',
color: '#6b7280',
lineHeight: '1.5'
}}>
Reload this page and your settings will be preserved. Open DevTools Application Local Storage to see the saved data.
</p>
</div>
</div>
);
};
export const LocalStoragePersistence: Story = {
name: 'localStorage Persistence',
render: () => (
<AbacusDisplayProvider>
<PersistenceDemo />
</AbacusDisplayProvider>
),
parameters: {
docs: {
description: {
story: 'Configuration is automatically persisted to localStorage and restored on page reload. SSR-safe with proper hydration.'
}
}
}
};
// Multiple Providers (Not Recommended)
export const MultipleProviders: Story = {
name: 'Multiple Providers (Advanced)',
render: () => (
<div style={{ display: 'flex', gap: '40px' }}>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Provider A</h4>
<AbacusDisplayProvider initialConfig={{ beadShape: 'diamond', colorScheme: 'heaven-earth' }}>
<AbacusReact value={111} columns={3} showNumbers />
<p style={{ fontSize: '12px', marginTop: '8px', color: '#6b7280' }}>Diamond beads</p>
</AbacusDisplayProvider>
</div>
<div style={{ textAlign: 'center' }}>
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Provider B</h4>
<AbacusDisplayProvider initialConfig={{ beadShape: 'circle', colorScheme: 'place-value' }}>
<AbacusReact value={222} columns={3} showNumbers />
<p style={{ fontSize: '12px', marginTop: '8px', color: '#6b7280' }}>Circle beads</p>
</AbacusDisplayProvider>
</div>
</div>
),
parameters: {
docs: {
description: {
story: 'You can use multiple providers with different configs, but typically one provider at the app root is sufficient. Note: Each provider maintains its own localStorage key.'
}
}
}
};
// Without Provider (Fallback)
export const WithoutProvider: Story = {
name: 'Without Provider (Fallback)',
render: () => (
<div style={{ textAlign: 'center' }}>
<StandaloneBead size={40} shape="diamond" color="#8b5cf6" />
<p style={{ marginTop: '12px', fontSize: '14px', color: '#6b7280' }}>
Components work without a provider by using default configuration
</p>
</div>
),
parameters: {
docs: {
description: {
story: 'Components gracefully fall back to defaults when used outside a provider'
}
}
}
};

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

@@ -0,0 +1,423 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AbacusReact } from './AbacusReact';
import React from 'react';
const meta: Meta<typeof AbacusReact> = {
title: 'Soroban/3D Effects Showcase',
component: AbacusReact,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# 3D Enhancement Showcase
Two levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
## Subtle (CSS Perspective + Shadows)
- Light perspective tilt
- Depth shadows on active beads
- Smooth transitions
- **Zero performance cost**
## 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
`
}
}
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
// ============================================
// SIDE-BY-SIDE COMPARISON
// ============================================
export const CompareAllLevels: Story = {
name: '🎯 Compare All Levels',
render: () => (
<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>
<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>
<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: 'Side-by-side comparison of both enhancement levels. **Click beads** to see how they move!'
}
}
}
};
// ============================================
// PROPOSAL 1: SUBTLE
// ============================================
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: 'Subtle 3D with light perspective tilt and depth shadows. Click beads to interact!'
}
}
}
};
// ============================================
// PROPOSAL 2: REALISTIC (Materials)
// ============================================
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: '**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).'
}
}
}
};
// ============================================
// INTERACTIVE PLAYGROUND
// ============================================
export const Playground: Story = {
name: '🎮 Interactive Playground',
render: () => {
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', 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>
<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: '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>
);
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,383 @@
/**
* AbacusSVGRenderer - Shared SVG rendering component (Core Architecture)
*
* This is the **single SVG renderer** used by both AbacusStatic and AbacusReact to guarantee
* pixel-perfect visual consistency. It implements dependency injection to support different
* bead components while maintaining identical layout.
*
* ## Architecture Role:
* ```
* AbacusStatic + AbacusReact
* ↓
* calculateStandardDimensions() ← Single source for all layout dimensions
* ↓
* AbacusSVGRenderer ← This component (shared structure)
* ↓
* calculateBeadPosition() ← Exact positioning for every bead
* ↓
* BeadComponent (injected) ← AbacusStaticBead OR AbacusAnimatedBead
* ```
*
* ## Key Features:
* - ✅ No "use client" directive - works in React Server Components
* - ✅ No hooks or state - pure rendering from props
* - ✅ Dependency injection for bead components
* - ✅ Supports 3D gradients, background glows, overlays (via props)
* - ✅ Same props → same dimensions → same positions → same layout
*
* ## Why This Matters:
* Before this architecture, AbacusStatic and AbacusReact had ~700 lines of duplicate
* SVG rendering code with separate dimension calculations. This led to layout inconsistencies.
* Now they share this single renderer, eliminating duplication and guaranteeing consistency.
*/
import React from 'react'
import type { AbacusLayoutDimensions } from './AbacusUtils'
import type { BeadConfig, AbacusCustomStyles, ValidPlaceValues } from './AbacusReact'
import { numberToAbacusState, calculateBeadPosition, type AbacusState } from './AbacusUtils'
/**
* Props that bead components must accept
*/
export interface BeadComponentProps {
bead: BeadConfig
x: number
y: number
size: number
shape: 'circle' | 'diamond' | 'square'
color: string
hideInactiveBeads: boolean
customStyle?: {
fill?: string
stroke?: string
strokeWidth?: number
opacity?: number
}
onClick?: (bead: BeadConfig, event?: React.MouseEvent) => void
onMouseEnter?: (bead: BeadConfig, event?: React.MouseEvent) => void
onMouseLeave?: (bead: BeadConfig, event?: React.MouseEvent) => void
onRef?: (bead: BeadConfig, element: SVGElement | null) => void
}
/**
* Props for the SVG renderer
*/
export interface AbacusSVGRendererProps {
// Core data
value: number | bigint
columns: number
state: AbacusState
beadConfigs: BeadConfig[][] // Array of columns, each containing beads
// Layout
dimensions: AbacusLayoutDimensions
scaleFactor?: number
// Appearance
beadShape: 'circle' | 'diamond' | 'square'
colorScheme: string
colorPalette: string
hideInactiveBeads: boolean
frameVisible: boolean
showNumbers: boolean
customStyles?: AbacusCustomStyles
interactive?: boolean // Enable interactive CSS styles
// Tutorial features
highlightColumns?: number[]
columnLabels?: string[]
// 3D Enhancement (optional - only used by AbacusReact)
defsContent?: React.ReactNode // Custom defs content (gradients, patterns, etc.)
// Additional content (overlays, etc.)
children?: React.ReactNode // Rendered at the end of the SVG
// Dependency injection
BeadComponent: React.ComponentType<any> // Accept any bead component (base props + extra props)
getBeadColor: (bead: BeadConfig, totalColumns: number, colorScheme: string, colorPalette: string) => string
// Event handlers (optional, passed through to beads)
onBeadClick?: (bead: BeadConfig, event?: React.MouseEvent) => void
onBeadMouseEnter?: (bead: BeadConfig, event?: React.MouseEvent) => void
onBeadMouseLeave?: (bead: BeadConfig, event?: React.MouseEvent) => void
onBeadRef?: (bead: BeadConfig, element: SVGElement | null) => void
// Extra props calculator (for animations, gestures, etc.)
// This function is called for each bead to get extra props
calculateExtraBeadProps?: (bead: BeadConfig, baseProps: BeadComponentProps) => Record<string, any>
}
/**
* Pure SVG renderer for abacus
* Uses dependency injection to support both static and animated beads
*/
export function AbacusSVGRenderer({
value,
columns,
state,
beadConfigs,
dimensions,
scaleFactor = 1,
beadShape,
colorScheme,
colorPalette,
hideInactiveBeads,
frameVisible,
showNumbers,
customStyles,
interactive = false,
highlightColumns = [],
columnLabels = [],
defsContent,
children,
BeadComponent,
getBeadColor,
onBeadClick,
onBeadMouseEnter,
onBeadMouseLeave,
onBeadRef,
calculateExtraBeadProps,
}: AbacusSVGRendererProps) {
const { width, height, rodSpacing, barY, beadSize, barThickness, labelHeight, numbersHeight } = dimensions
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width * scaleFactor}
height={height * scaleFactor}
viewBox={`0 0 ${width} ${height}`}
className={`abacus-svg ${hideInactiveBeads ? 'hide-inactive-mode' : ''} ${interactive ? 'interactive' : ''}`}
style={{ overflow: 'visible', display: 'block' }}
>
<defs>
<style>{`
/* CSS-based opacity system for hidden inactive beads */
.abacus-bead {
transition: opacity 0.2s ease-in-out;
}
/* Hidden inactive beads are invisible by default */
.hide-inactive-mode .abacus-bead.hidden-inactive {
opacity: 0;
}
/* Interactive abacus: When hovering over the abacus, hidden inactive beads become semi-transparent */
.abacus-svg.hide-inactive-mode.interactive:hover .abacus-bead.hidden-inactive {
opacity: 0.5;
}
/* Interactive abacus: When hovering over a specific hidden inactive bead, it becomes fully visible */
.hide-inactive-mode.interactive .abacus-bead.hidden-inactive:hover {
opacity: 1 !important;
}
/* Non-interactive abacus: Hidden inactive beads always stay at opacity 0 */
.abacus-svg.hide-inactive-mode:not(.interactive) .abacus-bead.hidden-inactive {
opacity: 0 !important;
}
`}</style>
{/* Custom defs content (for 3D gradients, patterns, etc.) */}
{defsContent}
</defs>
{/* Background glow effects - rendered behind everything */}
{Array.from({ length: columns }, (_, colIndex) => {
const placeValue = columns - 1 - colIndex
const columnStyles = customStyles?.columns?.[colIndex]
const backgroundGlow = columnStyles?.backgroundGlow
if (!backgroundGlow) return null
const x = colIndex * rodSpacing + rodSpacing / 2
const glowWidth = rodSpacing + (backgroundGlow.spread || 0)
const glowHeight = height + (backgroundGlow.spread || 0)
return (
<rect
key={`background-glow-pv${placeValue}`}
x={x - glowWidth / 2}
y={-(backgroundGlow.spread || 0) / 2}
width={glowWidth}
height={glowHeight}
fill={backgroundGlow.fill || 'rgba(59, 130, 246, 0.2)'}
filter={backgroundGlow.blur ? `blur(${backgroundGlow.blur}px)` : 'none'}
opacity={backgroundGlow.opacity ?? 0.6}
rx={8}
style={{ pointerEvents: 'none' }}
/>
)
})}
{/* Column highlights */}
{highlightColumns.map((colIndex) => {
if (colIndex < 0 || colIndex >= columns) return null
const x = colIndex * rodSpacing + rodSpacing / 2
const highlightWidth = rodSpacing * 0.9
const highlightHeight = height - labelHeight - numbersHeight
return (
<rect
key={`column-highlight-${colIndex}`}
x={x - highlightWidth / 2}
y={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 >= columns) return null
const x = colIndex * rodSpacing + rodSpacing / 2
return (
<text
key={`column-label-${colIndex}`}
x={x}
y={labelHeight / 2 + 5}
textAnchor="middle"
fontSize="14"
fontWeight="600"
fill="rgba(0, 0, 0, 0.7)"
style={{ pointerEvents: 'none', userSelect: 'none' }}
>
{label}
</text>
)
})}
{/* Rods (column posts) */}
{frameVisible && beadConfigs.map((_, colIndex) => {
const placeValue = columns - 1 - colIndex
const x = colIndex * rodSpacing + rodSpacing / 2
// Apply custom column post styling (column-specific overrides global)
const columnStyles = customStyles?.columns?.[colIndex]
const globalColumnPosts = customStyles?.columnPosts
const rodStyle = {
fill: columnStyles?.columnPost?.fill || globalColumnPosts?.fill || 'rgb(0, 0, 0, 0.1)',
stroke: columnStyles?.columnPost?.stroke || globalColumnPosts?.stroke || 'none',
strokeWidth: columnStyles?.columnPost?.strokeWidth ?? globalColumnPosts?.strokeWidth ?? 0,
opacity: columnStyles?.columnPost?.opacity ?? globalColumnPosts?.opacity ?? 1,
}
return (
<rect
key={`rod-pv${placeValue}`}
x={x - dimensions.rodWidth / 2}
y={labelHeight}
width={dimensions.rodWidth}
height={height - labelHeight - numbersHeight}
fill={rodStyle.fill}
stroke={rodStyle.stroke}
strokeWidth={rodStyle.strokeWidth}
opacity={rodStyle.opacity}
className="column-post"
/>
)
})}
{/* Reckoning bar */}
{frameVisible && (
<rect
x={0}
y={barY}
width={columns * rodSpacing}
height={barThickness}
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 - delegated to injected component */}
{beadConfigs.map((columnBeads, colIndex) => {
const placeValue = columns - 1 - colIndex
// Get column state for inactive earth bead positioning
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
return (
<g key={`column-${colIndex}`}>
{columnBeads.map((bead, beadIndex) => {
// Calculate position using shared utility with column state for accurate positioning
const position = calculateBeadPosition(bead, dimensions, { earthActive: columnState.earthActive })
const color = getBeadColor(bead, columns, colorScheme, colorPalette)
// Get custom style for this specific bead
const customStyle =
bead.type === 'heaven'
? customStyles?.heavenBeads
: customStyles?.earthBeads
// Build base props
const baseProps: BeadComponentProps = {
bead,
x: position.x,
y: position.y,
size: beadSize,
shape: beadShape,
color,
hideInactiveBeads,
customStyle,
onClick: onBeadClick,
onMouseEnter: onBeadMouseEnter,
onMouseLeave: onBeadMouseLeave,
onRef: onBeadRef,
}
// Calculate extra props if provided (for animations, etc.)
const extraProps = calculateExtraBeadProps?.(bead, baseProps) || {}
return (
<BeadComponent
key={`bead-pv${bead.placeValue}-${bead.type}-${bead.position}`}
{...baseProps}
{...extraProps}
/>
)
})}
</g>
)
})}
{/* Column numbers */}
{showNumbers && beadConfigs.map((_, colIndex) => {
const placeValue = columns - 1 - colIndex
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
const digit = (columnState.heavenActive ? 5 : 0) + columnState.earthActive
const x = colIndex * rodSpacing + rodSpacing / 2
return (
<text
key={`number-${colIndex}`}
x={x}
y={height - numbersHeight / 2 + 5}
textAnchor="middle"
fontSize={customStyles?.numerals?.fontSize || '16px'}
fontWeight={customStyles?.numerals?.fontWeight || '600'}
fill={customStyles?.numerals?.color || 'rgba(0, 0, 0, 0.8)'}
style={{ pointerEvents: 'none', userSelect: 'none' }}
>
{digit}
</text>
)
})}
{/* Additional content (overlays, numbers, etc.) */}
{children}
</svg>
)
}
export default AbacusSVGRenderer

View File

@@ -0,0 +1,283 @@
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")
* - ✅ **Identical layout to AbacusReact** - same props = same exact SVG output
* - ✅ No animations, hooks, or client-side JavaScript
* - ✅ Lightweight rendering for static displays
*
* ## Shared Architecture (Zero Duplication!):
* Both AbacusStatic and AbacusReact use the **exact same rendering pipeline**:
*
* ```
* calculateStandardDimensions() → AbacusSVGRenderer → calculateBeadPosition()
* ↓
* ┌───────────────────┴───────────────────┐
* ↓ ↓
* AbacusStaticBead AbacusAnimatedBead
* (Simple SVG) (react-spring)
* ```
*
* - `calculateStandardDimensions()` - Single source of truth for layout (beadSize, gaps, bar position, etc.)
* - `AbacusSVGRenderer` - Shared SVG structure with dependency injection for bead components
* - `calculateBeadPosition()` - Exact positioning formulas used by both variants
* - `AbacusStaticBead` - RSC-compatible simple SVG shapes (this component)
* - `AbacusAnimatedBead` - Client component with animations (AbacusReact)
*
* ## Visual Consistency Guarantee:
* Both AbacusStatic and AbacusReact produce **pixel-perfect identical output** for the same props.
* This ensures previews match interactive versions, PDFs match web displays, etc.
*
* **Architecture benefit:** ~560 lines of duplicate code eliminated. Same props = same dimensions = same positions = same layout.
*
* ## When to Use:
* - React Server Components (Next.js App Router)
* - Static site generation
* - Non-interactive previews
* - PDF generation
* - 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,184 @@
/**
* AbacusStatic - Server Component compatible static abacus
*
* Shares layout and rendering with AbacusReact through dependency injection.
* Uses standard dimensions to ensure same props = same exact visual output.
* Reuses: AbacusSVGRenderer for structure, shared dimension/position calculators
* Different: No hooks, no animations, no interactions, simplified bead rendering
*/
import { numberToAbacusState, calculateStandardDimensions } from './AbacusUtils'
import { AbacusSVGRenderer } from './AbacusSVGRenderer'
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 (matches AbacusReact)
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'
}
/**
* 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 standard dimensions (same as AbacusReact!)
const dimensions = calculateStandardDimensions({
columns: effectiveColumns,
scaleFactor,
showNumbers: !!showNumbers,
columnLabels,
})
// Compact mode hides frame
const effectiveFrameVisible = compact ? false : frameVisible
// Use shared renderer with static bead component
return (
<AbacusSVGRenderer
value={value}
columns={effectiveColumns}
state={state}
beadConfigs={beadConfigs}
dimensions={dimensions}
scaleFactor={scaleFactor}
beadShape={beadShape}
colorScheme={colorScheme}
colorPalette={colorPalette}
hideInactiveBeads={hideInactiveBeads}
frameVisible={effectiveFrameVisible}
showNumbers={!!showNumbers}
customStyles={customStyles}
highlightColumns={highlightColumns}
columnLabels={columnLabels}
BeadComponent={AbacusStaticBead}
getBeadColor={getBeadColor}
/>
)
}
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>
)
}

Some files were not shown because too many files have changed in this diff Show More