Compare commits
29 Commits
abacus-rea
...
abacus-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d322301ef | ||
|
|
0641eb719e | ||
|
|
3588d5acde | ||
|
|
74f2d97434 | ||
|
|
4f9dc4666d | ||
|
|
3b8e864cfa | ||
|
|
7418adb959 | ||
|
|
7228bbc2eb | ||
|
|
ff1d60a233 | ||
|
|
9f7f001d74 | ||
|
|
35d8734a3a | ||
|
|
6a1cec06a7 | ||
|
|
ce4e44d630 | ||
|
|
35bbcecb9e | ||
|
|
cf1f950c7c | ||
|
|
de038d2afc | ||
|
|
e65541c100 | ||
|
|
f4ec0689ff | ||
|
|
af0552ccd9 | ||
|
|
90421cfc38 | ||
|
|
2b06aae394 | ||
|
|
ee53bb9a9d | ||
|
|
28a2d40996 | ||
|
|
37e330f26e | ||
|
|
cc96802df8 | ||
|
|
5d97673406 | ||
|
|
26bdb11237 | ||
|
|
5ac55cc149 | ||
|
|
096104b094 |
537
CHANGELOG.md
537
CHANGELOG.md
@@ -1,6 +1,543 @@
|
||||
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))
|
||||
* add 3D printing support for abacus models ([dafdfdd](https://github.com/antialias/soroban-abacus-flashcards/commit/dafdfdd233b53464b9825a8a9b5f2e6206fc54cb))
|
||||
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](https://github.com/antialias/soroban-abacus-flashcards/commit/0922ea10b77e7d16b8c414c596d23cb11e20c1cc))
|
||||
* add comprehensive Storybook coverage and migration guide ([7a4a37e](https://github.com/antialias/soroban-abacus-flashcards/commit/7a4a37ec6d0171782778e18122da782f069e0556))
|
||||
* add game preview system with mock arcade environment ([25880cc](https://github.com/antialias/soroban-abacus-flashcards/commit/25880cc7e463f98a5a23c812c1ffd43734d3fe1f))
|
||||
* add per-player stats tracking system ([613301c](https://github.com/antialias/soroban-abacus-flashcards/commit/613301cd137ad6f712571a0be45c708ce391fc8f))
|
||||
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](https://github.com/antialias/soroban-abacus-flashcards/commit/81ead65680892efa4d0ab07e7f0ef77eb1bc1405))
|
||||
* add unified trophy abacus with hero mode integration ([6620418](https://github.com/antialias/soroban-abacus-flashcards/commit/6620418a704dcca810b511a5f394084521104e6b))
|
||||
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](https://github.com/antialias/soroban-abacus-flashcards/commit/3628426a567d7e0273be75cce64632ae04b7d5eb))
|
||||
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](https://github.com/antialias/soroban-abacus-flashcards/commit/1d525c7b5320984a1582b8ab7eae57895c728428))
|
||||
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](https://github.com/antialias/soroban-abacus-flashcards/commit/2fc0a05f7f557cee55f7d31b585499dd04e68ff9))
|
||||
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](https://github.com/antialias/soroban-abacus-flashcards/commit/d568955d6abf389e6ab7c6979e33122a65917a46))
|
||||
* **arcade:** auto-create room when user has none ([ff88c3a](https://github.com/antialias/soroban-abacus-flashcards/commit/ff88c3a1b81703a87a1d57eeb5cc139da7d9df04))
|
||||
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](https://github.com/antialias/soroban-abacus-flashcards/commit/1461414ef4d0b213af241213447c91eed1abe5fb))
|
||||
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](https://github.com/antialias/soroban-abacus-flashcards/commit/780a7161bc05c2ca6597d7d8d89f01afd33d9f4d))
|
||||
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](https://github.com/antialias/soroban-abacus-flashcards/commit/4d8e873358271fe3fd50b228aea8277e20aa5966))
|
||||
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](https://github.com/antialias/soroban-abacus-flashcards/commit/656f5a7838ed6003c214ec484d4c37072270fa8d))
|
||||
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](https://github.com/antialias/soroban-abacus-flashcards/commit/6527c26a8166b23f074e85eb335a15800c1947a2))
|
||||
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](https://github.com/antialias/soroban-abacus-flashcards/commit/d25b888ffb3915d2d482442ab708ba3e159af512))
|
||||
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](https://github.com/antialias/soroban-abacus-flashcards/commit/fd765335efbc91366c596c7789b92882cd3379d9))
|
||||
* **card-sorting:** add green border to correctly positioned cards ([16fca86](https://github.com/antialias/soroban-abacus-flashcards/commit/16fca86b7687115f1cf565c533a512e92946e3a8)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
|
||||
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](https://github.com/antialias/soroban-abacus-flashcards/commit/3a8209975728cdcf914c43ba08339454a9e2457f))
|
||||
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](https://github.com/antialias/soroban-abacus-flashcards/commit/c367e0ceece41d8e7c2bc8aebe3239ff6053a115))
|
||||
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](https://github.com/antialias/soroban-abacus-flashcards/commit/b0b93d0175c8a1c8958d6ba346d969c234fdd6ff))
|
||||
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](https://github.com/antialias/soroban-abacus-flashcards/commit/ee7345d641e0ee72915afb9cdbd6d284b7e238bd)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
|
||||
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](https://github.com/antialias/soroban-abacus-flashcards/commit/ed6f1779141d0bc9dff2d532a3dfc638015936b5)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
|
||||
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](https://github.com/antialias/soroban-abacus-flashcards/commit/f6ed4a27a26d8bfa495ba5f580a446286b9674a0))
|
||||
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](https://github.com/antialias/soroban-abacus-flashcards/commit/4ba7f247175d93e4d339e2be7bbdb2e009992232))
|
||||
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](https://github.com/antialias/soroban-abacus-flashcards/commit/7028cfc51164e9219479e6040b03c29239aa7edb))
|
||||
* **card-sorting:** gentler spring animation for locked cards ([47189cb](https://github.com/antialias/soroban-abacus-flashcards/commit/47189cb6e79ed2915f5ddcc9cb3626540dfb07f3))
|
||||
* **card-sorting:** implement continuous bezier curve paths ([2d93024](https://github.com/antialias/soroban-abacus-flashcards/commit/2d9302410f5e98145a435b00df3ae5fcf3f4c0b5))
|
||||
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](https://github.com/antialias/soroban-abacus-flashcards/commit/0b0503f0354a4a82fe6b9bfe827729e8e5a9e329))
|
||||
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](https://github.com/antialias/soroban-abacus-flashcards/commit/2e7a02c9e4ab84e821d58661d6e7a326f7882afb))
|
||||
* **card-sorting:** optimize results screen for mobile ([d188789](https://github.com/antialias/soroban-abacus-flashcards/commit/d188789069b4c350ce3cc0d221bd4a43dab528e0))
|
||||
* **card-sorting:** redesign setup screen with modern UI ([73cf967](https://github.com/antialias/soroban-abacus-flashcards/commit/73cf96749234c480482f62392245b38c1fd5f0a0))
|
||||
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](https://github.com/antialias/soroban-abacus-flashcards/commit/222dc555fa5068e2594dcc074e33f70320f5742c))
|
||||
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](https://github.com/antialias/soroban-abacus-flashcards/commit/8f6feec4f21d0af0d1c98daf5017eddd91d3d578))
|
||||
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](https://github.com/antialias/soroban-abacus-flashcards/commit/c5f39d51eb45ec816f32151dc7f9d7c06360474b))
|
||||
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](https://github.com/antialias/soroban-abacus-flashcards/commit/e3184dd0d444e5dc204731f5b396d5c553cf7d11))
|
||||
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
|
||||
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](https://github.com/antialias/soroban-abacus-flashcards/commit/83d0ba26f5eeec3e189d279710d5bbcf13e82f29))
|
||||
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](https://github.com/antialias/soroban-abacus-flashcards/commit/4d0795a9df74fcb085af821eafb923bdcb5f0b0c))
|
||||
* dynamically crop favicon to active beads for maximum size ([5670322](https://github.com/antialias/soroban-abacus-flashcards/commit/567032296aecaad13408bdc17d108ec7c57fb4a8))
|
||||
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
|
||||
* **games:** add autoplay and improve carousel layout ([9f51edf](https://github.com/antialias/soroban-abacus-flashcards/commit/9f51edfaa95c14f55a30a6eceafb9099eeed437f))
|
||||
* **games:** add horizontal scroll support to carousels ([a224abb](https://github.com/antialias/soroban-abacus-flashcards/commit/a224abb6f660e1aa31ab04f5590b003fae072af9))
|
||||
* **games:** add rotating games hero carousel ([24231e6](https://github.com/antialias/soroban-abacus-flashcards/commit/24231e6b2ebbdcae066344df54e7e80e7d221128))
|
||||
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](https://github.com/antialias/soroban-abacus-flashcards/commit/fe9bfeabf9ee66923501b18e1b69f2d666d0817d))
|
||||
* **i18n:** add global language selector to navigation ([0506360](https://github.com/antialias/soroban-abacus-flashcards/commit/0506360117807665e8f5a6fcd8f1178339f6e65c))
|
||||
* **i18n:** add homepage translations for all supported languages ([8c9d35a](https://github.com/antialias/soroban-abacus-flashcards/commit/8c9d35a3b43dd29664f5afb1bd96c4e584d9ec75))
|
||||
* **i18n:** add Old High German (goh) language support ([b334a15](https://github.com/antialias/soroban-abacus-flashcards/commit/b334a15255ed9fa29beb43de66da0288691390c6))
|
||||
* **i18n:** complete Old High German translations for all locales ([0b06a1c](https://github.com/antialias/soroban-abacus-flashcards/commit/0b06a1ce005d92e7ae9c225aba40d240e965753d))
|
||||
* **i18n:** internationalize games page and tutorial content ([4253964](https://github.com/antialias/soroban-abacus-flashcards/commit/4253964af19f9aaa16f2394f41819223542fb519))
|
||||
* **i18n:** internationalize homepage with English translations ([40cff14](https://github.com/antialias/soroban-abacus-flashcards/commit/40cff143c72e9228d7cce607cab64c4a6d067017))
|
||||
* **i18n:** migrate from react-i18next to next-intl ([9016b76](https://github.com/antialias/soroban-abacus-flashcards/commit/9016b760247a20271255839e4dd7e5b9a8353b9f))
|
||||
* **i18n:** update games page hero section copy ([6333c60](https://github.com/antialias/soroban-abacus-flashcards/commit/6333c60352b920916afd81cc3b0229706a1519fa))
|
||||
* install embla-carousel-autoplay for games carousel ([946e5d1](https://github.com/antialias/soroban-abacus-flashcards/commit/946e5d19107020992be8945f8fe7c41e4bc2a0e2))
|
||||
* install embla-carousel-react for player profile carousel ([642ae95](https://github.com/antialias/soroban-abacus-flashcards/commit/642ae957383cfe1d6045f645bbe426fd80c56f35))
|
||||
* internationalize guide page with 6 languages ([e9c320b](https://github.com/antialias/soroban-abacus-flashcards/commit/e9c320bb1032e94c3852b9459236409da4669c09))
|
||||
* internationalize tutorial player ([26d41cf](https://github.com/antialias/soroban-abacus-flashcards/commit/26d41cfd058bfdf5b61ee6e20cfc61cbecb32f45))
|
||||
* optimize card sorting for mobile displays ([b443ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/b443ee9cdcd9fcb7674845d8c92f7c338ad98dea))
|
||||
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](https://github.com/antialias/soroban-abacus-flashcards/commit/6ae4d13dc784a87f85206c6ff6d005e5b23b678c))
|
||||
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](https://github.com/antialias/soroban-abacus-flashcards/commit/4a78485d2e20f2cbf36cc898a1beafa8eb48bfbf))
|
||||
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](https://github.com/antialias/soroban-abacus-flashcards/commit/d7eb957a8dabbcac35e166a83dd679a628e19baa))
|
||||
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](https://github.com/antialias/soroban-abacus-flashcards/commit/c0d6526d30aca8deaeda2b7c2e27eb37af8b577c))
|
||||
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](https://github.com/antialias/soroban-abacus-flashcards/commit/f457f1a1c22b6cb7fff23a7701474322cf423dd9))
|
||||
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](https://github.com/antialias/soroban-abacus-flashcards/commit/cae335958751c27684bfb10c8e2e526b460954ed))
|
||||
* **rithmomachia:** add helpful error messages for failed captures ([b172440](https://github.com/antialias/soroban-abacus-flashcards/commit/b172440a41e958ced98903bb8f4c2e4b423e1356))
|
||||
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](https://github.com/antialias/soroban-abacus-flashcards/commit/d42bcff0d922895549c1c12f8e02a3ae6d53425a))
|
||||
* **rithmomachia:** Add interactive playing guide modal ([3121d82](https://github.com/antialias/soroban-abacus-flashcards/commit/3121d8240a567817f5f205a4ef4a788fcf451f71))
|
||||
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](https://github.com/antialias/soroban-abacus-flashcards/commit/82d89131f00517f162ec496397cb390f9ecfc52e))
|
||||
* **rithmomachia:** add ratio capture example to guide ([9150b0c](https://github.com/antialias/soroban-abacus-flashcards/commit/9150b0c678ce7104fe984ee0fc93748b43a245f4))
|
||||
* **rithmomachia:** add standalone guide page route ([3fcc79f](https://github.com/antialias/soroban-abacus-flashcards/commit/3fcc79fe9eae11d4bd3a724c1b1f7d086e7cae81))
|
||||
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](https://github.com/antialias/soroban-abacus-flashcards/commit/27f1c989d59a19844b90a5148ae27fb97161da2d))
|
||||
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](https://github.com/antialias/soroban-abacus-flashcards/commit/275f401e3c25b75fec4700a8c2d4be6e33f0afe9))
|
||||
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](https://github.com/antialias/soroban-abacus-flashcards/commit/74bc3c0dcf8d1ee7084e88a04861a85f9b623809))
|
||||
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](https://github.com/antialias/soroban-abacus-flashcards/commit/1d5f01c966cf1eec9a9c19ee37f1cad93c89df40))
|
||||
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](https://github.com/antialias/soroban-abacus-flashcards/commit/b7fac788292e00c6060a47fdbcca89a7e7fee35c))
|
||||
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](https://github.com/antialias/soroban-abacus-flashcards/commit/9fd54067ce257e028b02f4784568ff3f2bbb32ca))
|
||||
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](https://github.com/antialias/soroban-abacus-flashcards/commit/4829e41ea13fae2edec10837e65e505929445782))
|
||||
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](https://github.com/antialias/soroban-abacus-flashcards/commit/0a308016e9d6a926c52dbfc5623b60b169d16d03))
|
||||
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](https://github.com/antialias/soroban-abacus-flashcards/commit/f5558563ea93ef7428aa220c2e15e3f02711420f))
|
||||
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](https://github.com/antialias/soroban-abacus-flashcards/commit/55aff829f4c284e8cfe6d471c0821575928b93bc))
|
||||
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](https://github.com/antialias/soroban-abacus-flashcards/commit/9fde1ef9e703e26b2450128155b53fdf2d2e1fe5))
|
||||
* **rithmomachia:** guide defaults to docked right on open ([11f674d](https://github.com/antialias/soroban-abacus-flashcards/commit/11f674d542ea5e4e88bd60ff1068451805d9766e))
|
||||
* **rithmomachia:** improve guide pieces section layout ([a270bfc](https://github.com/antialias/soroban-abacus-flashcards/commit/a270bfc0cc4a3b6b54ba43a5af14a227cc7d29f9))
|
||||
* **rithmomachia:** improve guide UX and add persistence ([b314740](https://github.com/antialias/soroban-abacus-flashcards/commit/b31474069734350a7059cd7c73255a7e11b78eb9))
|
||||
* **rithmomachia:** improve roster status notice UX ([e27df45](https://github.com/antialias/soroban-abacus-flashcards/commit/e27df45256147f958ca215f9dd1f4e133e8cf06c))
|
||||
* **rithmomachia:** integrate roster warning into game nav ([8a11594](https://github.com/antialias/soroban-abacus-flashcards/commit/8a11594203fb91faee6cbc4cb74367164ecd6d85))
|
||||
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](https://github.com/antialias/soroban-abacus-flashcards/commit/04741971b296976f4476ecd949e84066fc549010))
|
||||
* **rithmomachia:** recreate original guide modal header layout ([2489695](https://github.com/antialias/soroban-abacus-flashcards/commit/24896957d0817758c5f64c0e3473e6a0a343af67))
|
||||
* **rithmomachia:** show capture error on hover instead of click ([339b678](https://github.com/antialias/soroban-abacus-flashcards/commit/339b6780f657ace5bfe1611c4ef64bb0c2c31587))
|
||||
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](https://github.com/antialias/soroban-abacus-flashcards/commit/b0c4523c0b4669c96a50b2812ba6cb2faa3f9a22))
|
||||
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](https://github.com/antialias/soroban-abacus-flashcards/commit/5c186f3947cc38f1f5db5de3e68e590b90c2d092))
|
||||
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](https://github.com/antialias/soroban-abacus-flashcards/commit/5c2ddbef05d7f4195d21b084cb1c0c4193ee3c9c))
|
||||
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](https://github.com/antialias/soroban-abacus-flashcards/commit/17d2460a8769a21d33fabc5f909cf5b939712d36))
|
||||
* **rithmomachia:** simplify guide language for clarity ([85cb630](https://github.com/antialias/soroban-abacus-flashcards/commit/85cb630add395a6693ecbbe9c8fc6aaf8c47be29))
|
||||
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](https://github.com/antialias/soroban-abacus-flashcards/commit/be2a00e8b366b5606525309b4c7813f5c35c7f7c))
|
||||
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](https://github.com/antialias/soroban-abacus-flashcards/commit/08c97620f5e694b8526c448c44d265e6dd1fe1eb))
|
||||
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](https://github.com/antialias/soroban-abacus-flashcards/commit/0769eaaa1dc238b901e3a7cfe0486e6122d5eda9))
|
||||
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](https://github.com/antialias/soroban-abacus-flashcards/commit/976a7de949c22842f4b6da3ced990f502a1c2733))
|
||||
* **room-share:** add QR code button for easy mobile joining ([349290a](https://github.com/antialias/soroban-abacus-flashcards/commit/349290ac6a411651686b64d2e6b540083d2df1d9))
|
||||
* show rithmomachia turn in nav ([7c89bfe](https://github.com/antialias/soroban-abacus-flashcards/commit/7c89bfef9c60db0e2c46e920500dcc1fbe90d3df))
|
||||
* switch to royal color theme with transparent background ([944ad65](https://github.com/antialias/soroban-abacus-flashcards/commit/944ad6574e01a67ce1fdbb1f2452fe632c78ce43)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adjust hero abacus position to avoid covering subtitle ([f03d341](https://github.com/antialias/soroban-abacus-flashcards/commit/f03d3413145cc7ddfba93728ecdec7eabea9ada6))
|
||||
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](https://github.com/antialias/soroban-abacus-flashcards/commit/fbcde2505f7ff2bf3426f3458e480c4548314ba4))
|
||||
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](https://github.com/antialias/soroban-abacus-flashcards/commit/7c1c2d7bebbb9a1acb274d17dd43b6ee5d196f44))
|
||||
* **arcade:** implement optimistic locking in session manager ([71fd66d](https://github.com/antialias/soroban-abacus-flashcards/commit/71fd66d96a3b03650c90f59f6e516aae7dddc345))
|
||||
* board rotation now properly fills height in portrait mode ([b5a96ea](https://github.com/antialias/soroban-abacus-flashcards/commit/b5a96eaeb1e29c20304142a7a0adf62f1cef570f))
|
||||
* **card-sorting:** add border radius to outer card container ([a922eba](https://github.com/antialias/soroban-abacus-flashcards/commit/a922eba73c4656ee941ce4dfb1dc57a62f076570))
|
||||
* **card-sorting:** add debug logging for spring animations ([d42947e](https://github.com/antialias/soroban-abacus-flashcards/commit/d42947eb8d5d3d8298f5d3b3d1644891c268dbb6))
|
||||
* **card-sorting:** add missing gameMode support after hard reset ([a832325](https://github.com/antialias/soroban-abacus-flashcards/commit/a832325debde289d6928c5e6f9c24311c5e079ad))
|
||||
* **card-sorting:** add missing useMemo import ([949d76d](https://github.com/antialias/soroban-abacus-flashcards/commit/949d76d844c786ada8a6373e4abb7f498f6befb9))
|
||||
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](https://github.com/antialias/soroban-abacus-flashcards/commit/84c66feec6b4112b015e1afd95bf33b24b5f6a4f))
|
||||
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](https://github.com/antialias/soroban-abacus-flashcards/commit/829c741e554d1490dd7a5bbc17f2a32f7195dc07))
|
||||
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](https://github.com/antialias/soroban-abacus-flashcards/commit/fc5cf1216fe03edfb7e44afda01192f4b97b4f4e))
|
||||
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](https://github.com/antialias/soroban-abacus-flashcards/commit/4dce16cca46c965199b7e09f8b34bfa221efac33))
|
||||
* **card-sorting:** animate cards from game board to results grid ([17d45fe](https://github.com/antialias/soroban-abacus-flashcards/commit/17d45fe88cd9773f5e550f6ee5a7f0c82cca2023))
|
||||
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](https://github.com/antialias/soroban-abacus-flashcards/commit/d02ab5922c416042d525f54097a6975ae1541586))
|
||||
* **card-sorting:** enable card scaling for spectators ([6b095c3](https://github.com/antialias/soroban-abacus-flashcards/commit/6b095c33830341c46139bc847ddaab3db632265e))
|
||||
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](https://github.com/antialias/soroban-abacus-flashcards/commit/f3f6eca1db30df9e1e34cc4e77a069a6a3954f3d))
|
||||
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](https://github.com/antialias/soroban-abacus-flashcards/commit/ae45298ec48efb29587c0a1c1a7986a72821f3ef))
|
||||
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d94aaa58531ed4f9047e2ca92724d9264643d))
|
||||
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](https://github.com/antialias/soroban-abacus-flashcards/commit/4b4fbfef322ecda06020ad52d4b1788267112460))
|
||||
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](https://github.com/antialias/soroban-abacus-flashcards/commit/5cca279687d8973d25bd9a411a55b632d1c82f63))
|
||||
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](https://github.com/antialias/soroban-abacus-flashcards/commit/79c94699fa1cc2a2886e3ab1addc5fcd975602f5))
|
||||
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](https://github.com/antialias/soroban-abacus-flashcards/commit/170abed2318432f309de40692f6092bb4c4a1a45))
|
||||
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](https://github.com/antialias/soroban-abacus-flashcards/commit/275cc62a523d9e849f2162001141b6d75ae0925e))
|
||||
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](https://github.com/antialias/soroban-abacus-flashcards/commit/f3687ed236eff4ebe61699ec02909024c7086fb5))
|
||||
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](https://github.com/antialias/soroban-abacus-flashcards/commit/51368c6ec59d5447ce2875c5e1181dec97fd509d))
|
||||
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](https://github.com/antialias/soroban-abacus-flashcards/commit/0d8af09517534f1e1cf1f57160391d465a279d76))
|
||||
* **card-sorting:** preserve rotation when starting drag ([3364144](https://github.com/antialias/soroban-abacus-flashcards/commit/3364144fb6212934b6ad6d63ac6e7b78b436b258))
|
||||
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](https://github.com/antialias/soroban-abacus-flashcards/commit/a0b14f87e9c5b32fcbb685da4e70c563f70ed91a))
|
||||
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](https://github.com/antialias/soroban-abacus-flashcards/commit/bd014bec4ffa12bcd8f4a4e84ff51203c90c1f1d))
|
||||
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](https://github.com/antialias/soroban-abacus-flashcards/commit/34785f466faaa6b9f2958df786af88561fa80b06))
|
||||
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](https://github.com/antialias/soroban-abacus-flashcards/commit/627b873382eaa76ad16477280d10451cf2951e1a))
|
||||
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](https://github.com/antialias/soroban-abacus-flashcards/commit/564a00f82b6ca6aa8a2c0586ca49fc42d44991a8))
|
||||
* **card-sorting:** prevent replaying own movements from server ([308168a](https://github.com/antialias/soroban-abacus-flashcards/commit/308168a7fb51013b0851e98b161ba1a1a3e39fbb))
|
||||
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](https://github.com/antialias/soroban-abacus-flashcards/commit/30953b8c4a3cf147f980455818f9ce8eea07837c))
|
||||
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](https://github.com/antialias/soroban-abacus-flashcards/commit/8aff60ce3f8d302ce5c1bde7cb773e63064c36b7))
|
||||
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](https://github.com/antialias/soroban-abacus-flashcards/commit/a44aa5a4c2d84cab7cf0bbf87485bb61548fdeb2))
|
||||
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](https://github.com/antialias/soroban-abacus-flashcards/commit/15c53ea4eb4abb824eb0360fb645b1f3e455578e))
|
||||
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](https://github.com/antialias/soroban-abacus-flashcards/commit/f5fb4d7b76e25286bcdecd017894ff2d78b31963))
|
||||
* **card-sorting:** show only active players in team members section ([fa9f1a5](https://github.com/antialias/soroban-abacus-flashcards/commit/fa9f1a568f3dff2f4e5e7d3e8841b951ef1b7d04))
|
||||
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](https://github.com/antialias/soroban-abacus-flashcards/commit/0eefc332ac2724c54b477301a269915e895db94f))
|
||||
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](https://github.com/antialias/soroban-abacus-flashcards/commit/b0cd194838705bb7bbf21ac9e318eaba491097b2))
|
||||
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](https://github.com/antialias/soroban-abacus-flashcards/commit/cee399ed1513d32d0fff51a6f63898aa861605e1))
|
||||
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](https://github.com/antialias/soroban-abacus-flashcards/commit/f389afa831935e896a626f526cfee378e340a64b))
|
||||
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](https://github.com/antialias/soroban-abacus-flashcards/commit/6972fdf1105b6e854494efe1c4c587e6b6ff32a9))
|
||||
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](https://github.com/antialias/soroban-abacus-flashcards/commit/e1258ee0416010909774694c0b25306b6f30329c))
|
||||
* configure favicon metadata and improve bead visibility ([e1369fa](https://github.com/antialias/soroban-abacus-flashcards/commit/e1369fa2754cd61745a2950e6cb767d6b08db38f))
|
||||
* copy entire packages/core and packages/templates ([0ccada0](https://github.com/antialias/soroban-abacus-flashcards/commit/0ccada0ca783e635f9ae08f33a69c392018ee342))
|
||||
* correct hero abacus scroll direction to flow with page content ([4232746](https://github.com/antialias/soroban-abacus-flashcards/commit/423274657c9698bba28f7246fbf48d8508d97ef9))
|
||||
* correct Typst template path in Dockerfile ([4c518de](https://github.com/antialias/soroban-abacus-flashcards/commit/4c518decb7fcc0b519d07680cbfd01c94c23dd41))
|
||||
* delete existing user sessions before creating new ones ([0cced47](https://github.com/antialias/soroban-abacus-flashcards/commit/0cced47a0f414a04371bdb253fc5a43e4d9557be))
|
||||
* extract pure SVG content from AbacusReact renders ([b07f1c4](https://github.com/antialias/soroban-abacus-flashcards/commit/b07f1c421616bcfd1f949f9a42ce1b03df418945))
|
||||
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](https://github.com/antialias/soroban-abacus-flashcards/commit/5a8c98fc10704e459690308a84dc7ee2bfa0ef6c))
|
||||
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](https://github.com/antialias/soroban-abacus-flashcards/commit/f80a73b35c324959bfd7141ebf086cb47d3c0ebc))
|
||||
* **games:** use specific transition properties for smooth carousel loop ([187271e](https://github.com/antialias/soroban-abacus-flashcards/commit/187271e51527ee0129f71d77be1bd24072b963c4))
|
||||
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](https://github.com/antialias/soroban-abacus-flashcards/commit/4d4d930bd307ce5a405fc5751af6682a9f221f1f))
|
||||
* **i18n:** use useMessages() for tutorial translations ([95b0105](https://github.com/antialias/soroban-abacus-flashcards/commit/95b0105ca3b28c5adfa843e8d77a8b27d9e7ade4))
|
||||
* include column posts in favicon bounding box ([0b2f481](https://github.com/antialias/soroban-abacus-flashcards/commit/0b2f48106a939307b728c86fe2ea1be1e0247ea8))
|
||||
* increase server update debounce to 2000ms for low bandwidth ([633ff12](https://github.com/antialias/soroban-abacus-flashcards/commit/633ff127500c893a215491afa0e6ff814ad553bf))
|
||||
* Integrate threshold input into Point Victory card ([b29bbee](https://github.com/antialias/soroban-abacus-flashcards/commit/b29bbeefcad92be42f7a3ca27ac126db4232ab26))
|
||||
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](https://github.com/antialias/soroban-abacus-flashcards/commit/d7b35d954421fd7577cd2c26247666e5953b647d))
|
||||
* **nav:** show full navigation on /games page ([d3fe6ac](https://github.com/antialias/soroban-abacus-flashcards/commit/d3fe6acbb0390e1df71869a4095e5ee6021e06b1))
|
||||
* **qr-button:** improve layout and z-index ([646a422](https://github.com/antialias/soroban-abacus-flashcards/commit/646a4228d0573796b1a429e31bc037411024c0ff))
|
||||
* **qr-button:** increase mini QR code size to 80px ([61ac737](https://github.com/antialias/soroban-abacus-flashcards/commit/61ac7378bdb01132b26bfc265a057c095ea41606))
|
||||
* **qr-button:** increase mini QR code to 84px ([3fae5ea](https://github.com/antialias/soroban-abacus-flashcards/commit/3fae5ea6fa9ebd0f8fe8c9140a027be7f6a041aa))
|
||||
* **qr-button:** make button square and increase QR size ([dc2d466](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d46663b8e0ec94a1508a57c4f8c2d8ba03506))
|
||||
* **qr-button:** match height of stacked buttons ([81f202d](https://github.com/antialias/soroban-abacus-flashcards/commit/81f202d21556aa430402fda814519adbc8883831))
|
||||
* reduce padding to minimize gap below last bead ([0e529be](https://github.com/antialias/soroban-abacus-flashcards/commit/0e529be789caf16e73f3e2ee77f52e243841aef4))
|
||||
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
|
||||
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
|
||||
* resolve z-index layering and hero abacus visibility issues ([ed9a050](https://github.com/antialias/soroban-abacus-flashcards/commit/ed9a050d64db905e1328008f25dc0014e9a81999))
|
||||
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
|
||||
* **rithmomachia:** add missing i18next dependencies ([91154d9](https://github.com/antialias/soroban-abacus-flashcards/commit/91154d93647e59f7e5f96d1db5624a7ec9b1b9ff))
|
||||
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](https://github.com/antialias/soroban-abacus-flashcards/commit/dae615ee72a7ec7d0b235a22c61ebc4af0d8eadb))
|
||||
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](https://github.com/antialias/soroban-abacus-flashcards/commit/cda1126cb0eab6840df89f3a8778d72410298093))
|
||||
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](https://github.com/antialias/soroban-abacus-flashcards/commit/709322373a91c8174d21052d184fa84dd8bda326))
|
||||
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](https://github.com/antialias/soroban-abacus-flashcards/commit/2a917484938bc269cf16acb501d4d26584405e0f))
|
||||
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](https://github.com/antialias/soroban-abacus-flashcards/commit/cfac27750526fb1f6a7e4314a96aab3b92e08e44))
|
||||
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](https://github.com/antialias/soroban-abacus-flashcards/commit/618e56358deb66cba968472f39b8d4e28b4dd211))
|
||||
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](https://github.com/antialias/soroban-abacus-flashcards/commit/aafb64f3e337c6cf925766fe179b91f66c4a040b))
|
||||
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](https://github.com/antialias/soroban-abacus-flashcards/commit/1bcd99c949e4d2b4fb1c0813debd50176fa58cb9))
|
||||
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](https://github.com/antialias/soroban-abacus-flashcards/commit/14259a19a9817d0947467faa004d5f43118f8d8d))
|
||||
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](https://github.com/antialias/soroban-abacus-flashcards/commit/4fa20f44cb9758f29d1f1512232be0fdc0b53b3d))
|
||||
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](https://github.com/antialias/soroban-abacus-flashcards/commit/4834ece98e86f2fb00511bb876a5c32c289df0e0))
|
||||
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](https://github.com/antialias/soroban-abacus-flashcards/commit/56f3164155beb94ceec2838bed9fc74fd75524db))
|
||||
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](https://github.com/antialias/soroban-abacus-flashcards/commit/d0a8fcdea6aa4fdacfee33e183c92923634ee2b7))
|
||||
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](https://github.com/antialias/soroban-abacus-flashcards/commit/a673177bec1c709463ce0f266848f473a79f4ef0))
|
||||
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a0374fac5dce676df5890663b75531589ed93a))
|
||||
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](https://github.com/antialias/soroban-abacus-flashcards/commit/190f8cf302aa966f029d05931811e217c67bfe39))
|
||||
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](https://github.com/antialias/soroban-abacus-flashcards/commit/774c6b0ce712b1a77bb684457da9831e6ec91138))
|
||||
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](https://github.com/antialias/soroban-abacus-flashcards/commit/54bfd2fac86be3597d40c67a1235e4c4ed8e2709))
|
||||
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](https://github.com/antialias/soroban-abacus-flashcards/commit/8f4a79c9b0cad55336584fdc8e67409015d3a8ae))
|
||||
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](https://github.com/antialias/soroban-abacus-flashcards/commit/bd49964186a0daa1639ae849b128a76081643daf))
|
||||
* **room-info:** hide Leave Room button when user is alone ([5927f61](https://github.com/antialias/soroban-abacus-flashcards/commit/5927f61c3c34ba583ee45c8cee48a116c1c03071))
|
||||
* separate horizontal and vertical bounding box logic ([83090df](https://github.com/antialias/soroban-abacus-flashcards/commit/83090df4dfad1d1d5cfa6c278c241526cacc7972))
|
||||
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](https://github.com/antialias/soroban-abacus-flashcards/commit/88993f36629206a7bdcf9aa9d5641f1580b64de5))
|
||||
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
|
||||
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](https://github.com/antialias/soroban-abacus-flashcards/commit/f8fe6e4a415f8655626af567129d0cda61b82e15))
|
||||
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](https://github.com/antialias/soroban-abacus-flashcards/commit/f4ffc5b0277535358bea7588309a1a4afd1983a1))
|
||||
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](https://github.com/antialias/soroban-abacus-flashcards/commit/440b492e85beff1612697346b6c5cfc8461e83da))
|
||||
* various game improvements and UI enhancements ([b67cf61](https://github.com/antialias/soroban-abacus-flashcards/commit/b67cf610c570d54744553cd8f6694243fa50bee1))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* optimize Docker image size to reduce build failures ([9ca3106](https://github.com/antialias/soroban-abacus-flashcards/commit/9ca310636183f4970db925ce8fa368e23645eb02))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **card-sorting:** remove reveal numbers feature ([ea5e3e8](https://github.com/antialias/soroban-abacus-flashcards/commit/ea5e3e838bd6a5b8b38469a70aa92a0e9baba769))
|
||||
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](https://github.com/antialias/soroban-abacus-flashcards/commit/e4df8432b9c4a2055d47833d56b6e9fcf325ca94))
|
||||
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](https://github.com/antialias/soroban-abacus-flashcards/commit/82c133f742f3f5c40b723c18d1997b518f25b320))
|
||||
* **games:** move page title to nav bar ([712ee58](https://github.com/antialias/soroban-abacus-flashcards/commit/712ee58e5956e5bbdb13d5a5fb367020c87c8c9a))
|
||||
* **games:** remove redundant subtitle below nav ([ad5bb87](https://github.com/antialias/soroban-abacus-flashcards/commit/ad5bb87325a44825f0cd85b38eb0e5f0eea7a695))
|
||||
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](https://github.com/antialias/soroban-abacus-flashcards/commit/876513c9cc6323c20845ae8f1a3a5478d449f9e4))
|
||||
* reorganize Harmony and Victory guide sections ([fb629c4](https://github.com/antialias/soroban-abacus-flashcards/commit/fb629c44ea37a7b296561919a4980c10d14efed8))
|
||||
* restructure /create page into hub with sub-pages ([b91b23d](https://github.com/antialias/soroban-abacus-flashcards/commit/b91b23d95ffaeeaa30dbc8579f4c30bae8829ee7))
|
||||
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](https://github.com/antialias/soroban-abacus-flashcards/commit/a0a867b27166a838ca7e0dcbd8f89fe1be812a80))
|
||||
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](https://github.com/antialias/soroban-abacus-flashcards/commit/f0a066d8f0a51d35e18f87a8436c0d05153c03b5))
|
||||
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](https://github.com/antialias/soroban-abacus-flashcards/commit/eace0ed52979b71870f77ee68f8568558f2aaecb))
|
||||
* **rithmomachia:** extract guide sections into separate files ([765525d](https://github.com/antialias/soroban-abacus-flashcards/commit/765525dc451897f561f017e444aae892dc27177f))
|
||||
* **rithmomachia:** extract hooks (phase 5) ([324a659](https://github.com/antialias/soroban-abacus-flashcards/commit/324a65992f97c295ea3968aaf54d266373f4c035))
|
||||
* **rithmomachia:** extract phase components (phase 4) ([11364f6](https://github.com/antialias/soroban-abacus-flashcards/commit/11364f6394c15e49850e5cad2cbd32e1ea08a178))
|
||||
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](https://github.com/antialias/soroban-abacus-flashcards/commit/3abc325ea27feee5c4cc59f02296ff218f342a81))
|
||||
* **rithmomachia:** make setup phase UI more compact ([e55f848](https://github.com/antialias/soroban-abacus-flashcards/commit/e55f848a26092a2b4a5b09c3c255544ea9666f1b))
|
||||
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](https://github.com/antialias/soroban-abacus-flashcards/commit/dfeeb0e0db8b2c4a38198cf71cd918439d6c211b)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
|
||||
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](https://github.com/antialias/soroban-abacus-flashcards/commit/82a5eb2e4bf74f42a183a15f1129e5ec84cc5231))
|
||||
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](https://github.com/antialias/soroban-abacus-flashcards/commit/0471da598d8d591b3f9d63f467cb35f999924c13))
|
||||
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/2ab6ab57995a6d7d9c66b9fba8de945507209661))
|
||||
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab7a1df327d7258228af9851762555583a20d61))
|
||||
* use AbacusReact for dynamic Open Graph image ([9c20f12](https://github.com/antialias/soroban-abacus-flashcards/commit/9c20f12bacff4fe7f8bd7a87032afbed9711e94b))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add 3D enhancement documentation to README ([cc96802](https://github.com/antialias/soroban-abacus-flashcards/commit/cc96802df87c805c946ee59af509663ba570e75b))
|
||||
* add database migration guide and playing guide modal spec ([5a29af7](https://github.com/antialias/soroban-abacus-flashcards/commit/5a29af78e27e897ab35273611b79c4b669304f71))
|
||||
* add deployment verification guidelines to prevent false positives ([3d8da23](https://github.com/antialias/soroban-abacus-flashcards/commit/3d8da2348b4e8a227e963791d15dc6718eac5af1))
|
||||
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](https://github.com/antialias/soroban-abacus-flashcards/commit/008ccead0f9c634fe52fd156e6f9a04d6cdd7744))
|
||||
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](https://github.com/antialias/soroban-abacus-flashcards/commit/e3c1f10233cc0924ff96a643c7c4c1f1278de3e3))
|
||||
* update workflow to require manual testing before commits ([0991796](https://github.com/antialias/soroban-abacus-flashcards/commit/0991796f1eccef345f10205e675e4c33d1a62b17))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](https://github.com/antialias/soroban-abacus-flashcards/commit/88ca35e0440157ff9349e8d3d2d3cc844f18ffea)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
|
||||
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](https://github.com/antialias/soroban-abacus-flashcards/commit/94e5e6a268b387380b88b192737bd55578b98bc7)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
|
||||
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](https://github.com/antialias/soroban-abacus-flashcards/commit/7bf2d730d370e562486b229f4d209099ff8c4463))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* trigger compose-updater deployment test ([2b06aae](https://github.com/antialias/soroban-abacus-flashcards/commit/2b06aae39474cc80d501c47c9685fa99e7120c48))
|
||||
* verify compose-updater automatic deployment cycle ([af0552c](https://github.com/antialias/soroban-abacus-flashcards/commit/af0552ccd98f7b5a62d6e4074b7d87b3716af698))
|
||||
|
||||
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))
|
||||
* add 3D printing support for abacus models ([dafdfdd](https://github.com/antialias/soroban-abacus-flashcards/commit/dafdfdd233b53464b9825a8a9b5f2e6206fc54cb))
|
||||
* add comprehensive metadata, SEO, and make AbacusReact SSR-compatible ([0922ea1](https://github.com/antialias/soroban-abacus-flashcards/commit/0922ea10b77e7d16b8c414c596d23cb11e20c1cc))
|
||||
* add comprehensive Storybook coverage and migration guide ([7a4a37e](https://github.com/antialias/soroban-abacus-flashcards/commit/7a4a37ec6d0171782778e18122da782f069e0556))
|
||||
* add game preview system with mock arcade environment ([25880cc](https://github.com/antialias/soroban-abacus-flashcards/commit/25880cc7e463f98a5a23c812c1ffd43734d3fe1f))
|
||||
* add per-player stats tracking system ([613301c](https://github.com/antialias/soroban-abacus-flashcards/commit/613301cd137ad6f712571a0be45c708ce391fc8f))
|
||||
* add Strategy & Tactics section to Rithmomachia guide ([81ead65](https://github.com/antialias/soroban-abacus-flashcards/commit/81ead65680892efa4d0ab07e7f0ef77eb1bc1405))
|
||||
* add unified trophy abacus with hero mode integration ([6620418](https://github.com/antialias/soroban-abacus-flashcards/commit/6620418a704dcca810b511a5f394084521104e6b))
|
||||
* **arcade:** add ability to deactivate remote players without kicking user ([3628426](https://github.com/antialias/soroban-abacus-flashcards/commit/3628426a567d7e0273be75cce64632ae04b7d5eb))
|
||||
* **arcade:** add native abacus numbers support to pressure gauge ([1d525c7](https://github.com/antialias/soroban-abacus-flashcards/commit/1d525c7b5320984a1582b8ab7eae57895c728428))
|
||||
* **arcade:** add Rithmomachia (Battle of Numbers) game ([2fc0a05](https://github.com/antialias/soroban-abacus-flashcards/commit/2fc0a05f7f557cee55f7d31b585499dd04e68ff9))
|
||||
* **arcade:** add yjs-demo collaborative game and Yjs persistence layer ([d568955](https://github.com/antialias/soroban-abacus-flashcards/commit/d568955d6abf389e6ab7c6979e33122a65917a46))
|
||||
* **arcade:** auto-create room when user has none ([ff88c3a](https://github.com/antialias/soroban-abacus-flashcards/commit/ff88c3a1b81703a87a1d57eeb5cc139da7d9df04))
|
||||
* **card-sorting:** add activity feed notifications for collaborative mode ([1461414](https://github.com/antialias/soroban-abacus-flashcards/commit/1461414ef4d0b213af241213447c91eed1abe5fb))
|
||||
* **card-sorting:** add auto-submit countdown for perfect sequences ([780a716](https://github.com/antialias/soroban-abacus-flashcards/commit/780a7161bc05c2ca6597d7d8d89f01afd33d9f4d))
|
||||
* **card-sorting:** add bezier curves to connecting arrows ([4d8e873](https://github.com/antialias/soroban-abacus-flashcards/commit/4d8e873358271fe3fd50b228aea8277e20aa5966))
|
||||
* **card-sorting:** add CardPosition type and position syncing ([656f5a7](https://github.com/antialias/soroban-abacus-flashcards/commit/656f5a7838ed6003c214ec484d4c37072270fa8d))
|
||||
* **card-sorting:** add collapsible stats sidebar for spectators ([6527c26](https://github.com/antialias/soroban-abacus-flashcards/commit/6527c26a8166b23f074e85eb335a15800c1947a2))
|
||||
* **card-sorting:** add game mode selector UI to setup phase ([d25b888](https://github.com/antialias/soroban-abacus-flashcards/commit/d25b888ffb3915d2d482442ab708ba3e159af512))
|
||||
* **card-sorting:** add GameMode type system for multiplayer support ([fd76533](https://github.com/antialias/soroban-abacus-flashcards/commit/fd765335efbc91366c596c7789b92882cd3379d9))
|
||||
* **card-sorting:** add green border to correctly positioned cards ([16fca86](https://github.com/antialias/soroban-abacus-flashcards/commit/16fca86b7687115f1cf565c533a512e92946e3a8)), closes [#22c55](https://github.com/antialias/soroban-abacus-flashcards/issues/22c55)
|
||||
* **card-sorting:** add player emoji indicators on moving cards ([3a82099](https://github.com/antialias/soroban-abacus-flashcards/commit/3a8209975728cdcf914c43ba08339454a9e2457f))
|
||||
* **card-sorting:** add react-spring animations for real-time sync ([c367e0c](https://github.com/antialias/soroban-abacus-flashcards/commit/c367e0ceece41d8e7c2bc8aebe3239ff6053a115))
|
||||
* **card-sorting:** add smooth transition to drop shadow ([b0b93d0](https://github.com/antialias/soroban-abacus-flashcards/commit/b0b93d0175c8a1c8958d6ba346d969c234fdd6ff))
|
||||
* **card-sorting:** add spectator mode UI enhancements ([ee7345d](https://github.com/antialias/soroban-abacus-flashcards/commit/ee7345d641e0ee72915afb9cdbd6d284b7e238bd)), closes [#6366f1](https://github.com/antialias/soroban-abacus-flashcards/issues/6366f1) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
|
||||
* **card-sorting:** add team scoring UI for collaborative mode ([ed6f177](https://github.com/antialias/soroban-abacus-flashcards/commit/ed6f1779141d0bc9dff2d532a3dfc638015936b5)), closes [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#8b5cf6](https://github.com/antialias/soroban-abacus-flashcards/issues/8b5cf6)
|
||||
* **card-sorting:** add updateCardPositions action to Provider ([f6ed4a2](https://github.com/antialias/soroban-abacus-flashcards/commit/f6ed4a27a26d8bfa495ba5f580a446286b9674a0))
|
||||
* **card-sorting:** auto-arrange prefix/suffix cards in corners ([4ba7f24](https://github.com/antialias/soroban-abacus-flashcards/commit/4ba7f247175d93e4d339e2be7bbdb2e009992232))
|
||||
* **card-sorting:** fade correctly positioned cards to 50% opacity ([7028cfc](https://github.com/antialias/soroban-abacus-flashcards/commit/7028cfc51164e9219479e6040b03c29239aa7edb))
|
||||
* **card-sorting:** gentler spring animation for locked cards ([47189cb](https://github.com/antialias/soroban-abacus-flashcards/commit/47189cb6e79ed2915f5ddcc9cb3626540dfb07f3))
|
||||
* **card-sorting:** implement continuous bezier curve paths ([2d93024](https://github.com/antialias/soroban-abacus-flashcards/commit/2d9302410f5e98145a435b00df3ae5fcf3f4c0b5))
|
||||
* **card-sorting:** improve card distribution for natural scattered look ([0b0503f](https://github.com/antialias/soroban-abacus-flashcards/commit/0b0503f0354a4a82fe6b9bfe827729e8e5a9e329))
|
||||
* **card-sorting:** make player emoji fill entire card background ([2e7a02c](https://github.com/antialias/soroban-abacus-flashcards/commit/2e7a02c9e4ab84e821d58661d6e7a326f7882afb))
|
||||
* **card-sorting:** optimize results screen for mobile ([d188789](https://github.com/antialias/soroban-abacus-flashcards/commit/d188789069b4c350ce3cc0d221bd4a43dab528e0))
|
||||
* **card-sorting:** redesign setup screen with modern UI ([73cf967](https://github.com/antialias/soroban-abacus-flashcards/commit/73cf96749234c480482f62392245b38c1fd5f0a0))
|
||||
* **card-sorting:** scale correctly positioned cards to 50% ([222dc55](https://github.com/antialias/soroban-abacus-flashcards/commit/222dc555fa5068e2594dcc074e33f70320f5742c))
|
||||
* **card-sorting:** shrink/fade cards in correct suffix as well ([8f6feec](https://github.com/antialias/soroban-abacus-flashcards/commit/8f6feec4f21d0af0d1c98daf5017eddd91d3d578))
|
||||
* **card-sorting:** smooth spring transition from game table to results grid ([c5f39d5](https://github.com/antialias/soroban-abacus-flashcards/commit/c5f39d51eb45ec816f32151dc7f9d7c06360474b))
|
||||
* **card-sorting:** wrap prefix/suffix cards to multiple rows ([e3184dd](https://github.com/antialias/soroban-abacus-flashcards/commit/e3184dd0d444e5dc204731f5b396d5c553cf7d11))
|
||||
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
|
||||
* **create-room:** replace hardcoded game grid with dynamic Radix Select dropdown ([83d0ba2](https://github.com/antialias/soroban-abacus-flashcards/commit/83d0ba26f5eeec3e189d279710d5bbcf13e82f29))
|
||||
* dynamic day-of-month favicon using subprocess pattern ([4d0795a](https://github.com/antialias/soroban-abacus-flashcards/commit/4d0795a9df74fcb085af821eafb923bdcb5f0b0c))
|
||||
* dynamically crop favicon to active beads for maximum size ([5670322](https://github.com/antialias/soroban-abacus-flashcards/commit/567032296aecaad13408bdc17d108ec7c57fb4a8))
|
||||
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
|
||||
* **games:** add autoplay and improve carousel layout ([9f51edf](https://github.com/antialias/soroban-abacus-flashcards/commit/9f51edfaa95c14f55a30a6eceafb9099eeed437f))
|
||||
* **games:** add horizontal scroll support to carousels ([a224abb](https://github.com/antialias/soroban-abacus-flashcards/commit/a224abb6f660e1aa31ab04f5590b003fae072af9))
|
||||
* **games:** add rotating games hero carousel ([24231e6](https://github.com/antialias/soroban-abacus-flashcards/commit/24231e6b2ebbdcae066344df54e7e80e7d221128))
|
||||
* **i18n:** add dynamic locale switching without page reload ([fe9bfea](https://github.com/antialias/soroban-abacus-flashcards/commit/fe9bfeabf9ee66923501b18e1b69f2d666d0817d))
|
||||
* **i18n:** add global language selector to navigation ([0506360](https://github.com/antialias/soroban-abacus-flashcards/commit/0506360117807665e8f5a6fcd8f1178339f6e65c))
|
||||
* **i18n:** add homepage translations for all supported languages ([8c9d35a](https://github.com/antialias/soroban-abacus-flashcards/commit/8c9d35a3b43dd29664f5afb1bd96c4e584d9ec75))
|
||||
* **i18n:** add Old High German (goh) language support ([b334a15](https://github.com/antialias/soroban-abacus-flashcards/commit/b334a15255ed9fa29beb43de66da0288691390c6))
|
||||
* **i18n:** complete Old High German translations for all locales ([0b06a1c](https://github.com/antialias/soroban-abacus-flashcards/commit/0b06a1ce005d92e7ae9c225aba40d240e965753d))
|
||||
* **i18n:** internationalize games page and tutorial content ([4253964](https://github.com/antialias/soroban-abacus-flashcards/commit/4253964af19f9aaa16f2394f41819223542fb519))
|
||||
* **i18n:** internationalize homepage with English translations ([40cff14](https://github.com/antialias/soroban-abacus-flashcards/commit/40cff143c72e9228d7cce607cab64c4a6d067017))
|
||||
* **i18n:** migrate from react-i18next to next-intl ([9016b76](https://github.com/antialias/soroban-abacus-flashcards/commit/9016b760247a20271255839e4dd7e5b9a8353b9f))
|
||||
* **i18n:** update games page hero section copy ([6333c60](https://github.com/antialias/soroban-abacus-flashcards/commit/6333c60352b920916afd81cc3b0229706a1519fa))
|
||||
* install embla-carousel-autoplay for games carousel ([946e5d1](https://github.com/antialias/soroban-abacus-flashcards/commit/946e5d19107020992be8945f8fe7c41e4bc2a0e2))
|
||||
* install embla-carousel-react for player profile carousel ([642ae95](https://github.com/antialias/soroban-abacus-flashcards/commit/642ae957383cfe1d6045f645bbe426fd80c56f35))
|
||||
* internationalize guide page with 6 languages ([e9c320b](https://github.com/antialias/soroban-abacus-flashcards/commit/e9c320bb1032e94c3852b9459236409da4669c09))
|
||||
* internationalize tutorial player ([26d41cf](https://github.com/antialias/soroban-abacus-flashcards/commit/26d41cfd058bfdf5b61ee6e20cfc61cbecb32f45))
|
||||
* optimize card sorting for mobile displays ([b443ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/b443ee9cdcd9fcb7674845d8c92f7c338ad98dea))
|
||||
* Redesign Rithmomachia setup page with dramatic medieval theme ([6ae4d13](https://github.com/antialias/soroban-abacus-flashcards/commit/6ae4d13dc784a87f85206c6ff6d005e5b23b678c))
|
||||
* **rithmomachia:** add 80% opacity to guide modal when not hovered ([4a78485](https://github.com/antialias/soroban-abacus-flashcards/commit/4a78485d2e20f2cbf36cc898a1beafa8eb48bfbf))
|
||||
* **rithmomachia:** add CaptureContext for capture dialog state management ([d7eb957](https://github.com/antialias/soroban-abacus-flashcards/commit/d7eb957a8dabbcac35e166a83dd679a628e19baa))
|
||||
* **rithmomachia:** add ghost panel preview for guide docking ([c0d6526](https://github.com/antialias/soroban-abacus-flashcards/commit/c0d6526d30aca8deaeda2b7c2e27eb37af8b577c))
|
||||
* **rithmomachia:** add guide docking with resizable panels ([f457f1a](https://github.com/antialias/soroban-abacus-flashcards/commit/f457f1a1c22b6cb7fff23a7701474322cf423dd9))
|
||||
* **rithmomachia:** add helper piece selection for mathematical captures ([cae3359](https://github.com/antialias/soroban-abacus-flashcards/commit/cae335958751c27684bfb10c8e2e526b460954ed))
|
||||
* **rithmomachia:** add helpful error messages for failed captures ([b172440](https://github.com/antialias/soroban-abacus-flashcards/commit/b172440a41e958ced98903bb8f4c2e4b423e1356))
|
||||
* **rithmomachia:** add initial board visual to guide Overview section ([d42bcff](https://github.com/antialias/soroban-abacus-flashcards/commit/d42bcff0d922895549c1c12f8e02a3ae6d53425a))
|
||||
* **rithmomachia:** Add interactive playing guide modal ([3121d82](https://github.com/antialias/soroban-abacus-flashcards/commit/3121d8240a567817f5f205a4ef4a788fcf451f71))
|
||||
* **rithmomachia:** add number bond visualization and helper placeholders ([82d8913](https://github.com/antialias/soroban-abacus-flashcards/commit/82d89131f00517f162ec496397cb390f9ecfc52e))
|
||||
* **rithmomachia:** add ratio capture example to guide ([9150b0c](https://github.com/antialias/soroban-abacus-flashcards/commit/9150b0c678ce7104fe984ee0fc93748b43a245f4))
|
||||
* **rithmomachia:** add standalone guide page route ([3fcc79f](https://github.com/antialias/soroban-abacus-flashcards/commit/3fcc79fe9eae11d4bd3a724c1b1f7d086e7cae81))
|
||||
* **rithmomachia:** add useBoardLayout hook for centralized layout calculations ([27f1c98](https://github.com/antialias/soroban-abacus-flashcards/commit/27f1c989d59a19844b90a5148ae27fb97161da2d))
|
||||
* **rithmomachia:** add usePieceSelection hook for selection state management ([275f401](https://github.com/antialias/soroban-abacus-flashcards/commit/275f401e3c25b75fec4700a8c2d4be6e33f0afe9))
|
||||
* **rithmomachia:** add visual board examples to Capture section ([74bc3c0](https://github.com/antialias/soroban-abacus-flashcards/commit/74bc3c0dcf8d1ee7084e88a04861a85f9b623809))
|
||||
* **rithmomachia:** add visual board examples to Harmony section ([1d5f01c](https://github.com/antialias/soroban-abacus-flashcards/commit/1d5f01c966cf1eec9a9c19ee37f1cad93c89df40))
|
||||
* **rithmomachia:** add visual winning example to Victory section ([b7fac78](https://github.com/antialias/soroban-abacus-flashcards/commit/b7fac788292e00c6060a47fdbcca89a7e7fee35c))
|
||||
* **rithmomachia:** auto-size tab labels with react-textfit ([9fd5406](https://github.com/antialias/soroban-abacus-flashcards/commit/9fd54067ce257e028b02f4784568ff3f2bbb32ca))
|
||||
* **rithmomachia:** cycle through valid helpers with dynamic number tooltips ([4829e41](https://github.com/antialias/soroban-abacus-flashcards/commit/4829e41ea13fae2edec10837e65e505929445782))
|
||||
* **rithmomachia:** enhance capture relation UI with smooth animations ([0a30801](https://github.com/antialias/soroban-abacus-flashcards/commit/0a308016e9d6a926c52dbfc5623b60b169d16d03))
|
||||
* **rithmomachia:** enhance Harmony section with comprehensive content ([f555856](https://github.com/antialias/soroban-abacus-flashcards/commit/f5558563ea93ef7428aa220c2e15e3f02711420f))
|
||||
* **rithmomachia:** enhance Pieces section with visual examples and pyramid details ([55aff82](https://github.com/antialias/soroban-abacus-flashcards/commit/55aff829f4c284e8cfe6d471c0821575928b93bc))
|
||||
* **rithmomachia:** enhance Pyramid section with comprehensive details ([9fde1ef](https://github.com/antialias/soroban-abacus-flashcards/commit/9fde1ef9e703e26b2450128155b53fdf2d2e1fe5))
|
||||
* **rithmomachia:** guide defaults to docked right on open ([11f674d](https://github.com/antialias/soroban-abacus-flashcards/commit/11f674d542ea5e4e88bd60ff1068451805d9766e))
|
||||
* **rithmomachia:** improve guide pieces section layout ([a270bfc](https://github.com/antialias/soroban-abacus-flashcards/commit/a270bfc0cc4a3b6b54ba43a5af14a227cc7d29f9))
|
||||
* **rithmomachia:** improve guide UX and add persistence ([b314740](https://github.com/antialias/soroban-abacus-flashcards/commit/b31474069734350a7059cd7c73255a7e11b78eb9))
|
||||
* **rithmomachia:** improve roster status notice UX ([e27df45](https://github.com/antialias/soroban-abacus-flashcards/commit/e27df45256147f958ca215f9dd1f4e133e8cf06c))
|
||||
* **rithmomachia:** integrate roster warning into game nav ([8a11594](https://github.com/antialias/soroban-abacus-flashcards/commit/8a11594203fb91faee6cbc4cb74367164ecd6d85))
|
||||
* **rithmomachia:** make guide modal ultra-responsive down to 150px width ([0474197](https://github.com/antialias/soroban-abacus-flashcards/commit/04741971b296976f4476ecd949e84066fc549010))
|
||||
* **rithmomachia:** recreate original guide modal header layout ([2489695](https://github.com/antialias/soroban-abacus-flashcards/commit/24896957d0817758c5f64c0e3473e6a0a343af67))
|
||||
* **rithmomachia:** show capture error on hover instead of click ([339b678](https://github.com/antialias/soroban-abacus-flashcards/commit/339b6780f657ace5bfe1611c4ef64bb0c2c31587))
|
||||
* **rithmomachia:** show pyramid face numbers on hover instead of selection ([b0c4523](https://github.com/antialias/soroban-abacus-flashcards/commit/b0c4523c0b4669c96a50b2812ba6cb2faa3f9a22))
|
||||
* **rithmomachia:** show pyramid face numbers when selected ([5c186f3](https://github.com/antialias/soroban-abacus-flashcards/commit/5c186f3947cc38f1f5db5de3e68e590b90c2d092))
|
||||
* **rithmomachia:** show pyramid face numbers when selected with subtle animation ([5c2ddbe](https://github.com/antialias/soroban-abacus-flashcards/commit/5c2ddbef05d7f4195d21b084cb1c0c4193ee3c9c))
|
||||
* **rithmomachia:** show real preview layout when dragging guide to dock ([17d2460](https://github.com/antialias/soroban-abacus-flashcards/commit/17d2460a8769a21d33fabc5f909cf5b939712d36))
|
||||
* **rithmomachia:** simplify guide language for clarity ([85cb630](https://github.com/antialias/soroban-abacus-flashcards/commit/85cb630add395a6693ecbbe9c8fc6aaf8c47be29))
|
||||
* **rithmomachia:** skip helper selection UI and auto-select first valid helper ([be2a00e](https://github.com/antialias/soroban-abacus-flashcards/commit/be2a00e8b366b5606525309b4c7813f5c35c7f7c))
|
||||
* **rithmomachia:** Update harmony system to classical three-piece proportions ([08c9762](https://github.com/antialias/soroban-abacus-flashcards/commit/08c97620f5e694b8526c448c44d265e6dd1fe1eb))
|
||||
* **rithmomachia:** Update to traditional board setup with 25 pieces per side ([0769eaa](https://github.com/antialias/soroban-abacus-flashcards/commit/0769eaaa1dc238b901e3a7cfe0486e6122d5eda9))
|
||||
* **rithmomachia:** use actual piece SVGs in number bond with 2.5s rotation animation ([976a7de](https://github.com/antialias/soroban-abacus-flashcards/commit/976a7de949c22842f4b6da3ced990f502a1c2733))
|
||||
* **room-share:** add QR code button for easy mobile joining ([349290a](https://github.com/antialias/soroban-abacus-flashcards/commit/349290ac6a411651686b64d2e6b540083d2df1d9))
|
||||
* show rithmomachia turn in nav ([7c89bfe](https://github.com/antialias/soroban-abacus-flashcards/commit/7c89bfef9c60db0e2c46e920500dcc1fbe90d3df))
|
||||
* switch to royal color theme with transparent background ([944ad65](https://github.com/antialias/soroban-abacus-flashcards/commit/944ad6574e01a67ce1fdbb1f2452fe632c78ce43)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#f59e0](https://github.com/antialias/soroban-abacus-flashcards/issues/f59e0) [#a855f7](https://github.com/antialias/soroban-abacus-flashcards/issues/a855f7) [#7e22](https://github.com/antialias/soroban-abacus-flashcards/issues/7e22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* adjust hero abacus position to avoid covering subtitle ([f03d341](https://github.com/antialias/soroban-abacus-flashcards/commit/f03d3413145cc7ddfba93728ecdec7eabea9ada6))
|
||||
* **arcade:** add automatic retry for version conflict rejections ([fbcde25](https://github.com/antialias/soroban-abacus-flashcards/commit/fbcde2505f7ff2bf3426f3458e480c4548314ba4))
|
||||
* **arcade:** allow deactivating players from users who left the room ([7c1c2d7](https://github.com/antialias/soroban-abacus-flashcards/commit/7c1c2d7bebbb9a1acb274d17dd43b6ee5d196f44))
|
||||
* **arcade:** implement optimistic locking in session manager ([71fd66d](https://github.com/antialias/soroban-abacus-flashcards/commit/71fd66d96a3b03650c90f59f6e516aae7dddc345))
|
||||
* board rotation now properly fills height in portrait mode ([b5a96ea](https://github.com/antialias/soroban-abacus-flashcards/commit/b5a96eaeb1e29c20304142a7a0adf62f1cef570f))
|
||||
* **card-sorting:** add border radius to outer card container ([a922eba](https://github.com/antialias/soroban-abacus-flashcards/commit/a922eba73c4656ee941ce4dfb1dc57a62f076570))
|
||||
* **card-sorting:** add debug logging for spring animations ([d42947e](https://github.com/antialias/soroban-abacus-flashcards/commit/d42947eb8d5d3d8298f5d3b3d1644891c268dbb6))
|
||||
* **card-sorting:** add missing gameMode support after hard reset ([a832325](https://github.com/antialias/soroban-abacus-flashcards/commit/a832325debde289d6928c5e6f9c24311c5e079ad))
|
||||
* **card-sorting:** add missing useMemo import ([949d76d](https://github.com/antialias/soroban-abacus-flashcards/commit/949d76d844c786ada8a6373e4abb7f498f6befb9))
|
||||
* **card-sorting:** add overflow hidden to clip rounded corners ([84c66fe](https://github.com/antialias/soroban-abacus-flashcards/commit/84c66feec6b4112b015e1afd95bf33b24b5f6a4f))
|
||||
* **card-sorting:** adjust connecting paths for scaled cards ([829c741](https://github.com/antialias/soroban-abacus-flashcards/commit/829c741e554d1490dd7a5bbc17f2a32f7195dc07))
|
||||
* **card-sorting:** adjust game board for spectator panels ([fc5cf12](https://github.com/antialias/soroban-abacus-flashcards/commit/fc5cf1216fe03edfb7e44afda01192f4b97b4f4e))
|
||||
* **card-sorting:** adjust viewport dimensions for spectator panels ([4dce16c](https://github.com/antialias/soroban-abacus-flashcards/commit/4dce16cca46c965199b7e09f8b34bfa221efac33))
|
||||
* **card-sorting:** animate cards from game board to results grid ([17d45fe](https://github.com/antialias/soroban-abacus-flashcards/commit/17d45fe88cd9773f5e550f6ee5a7f0c82cca2023))
|
||||
* **card-sorting:** correct suffix card detection in auto-arrange ([d02ab59](https://github.com/antialias/soroban-abacus-flashcards/commit/d02ab5922c416042d525f54097a6975ae1541586))
|
||||
* **card-sorting:** enable card scaling for spectators ([6b095c3](https://github.com/antialias/soroban-abacus-flashcards/commit/6b095c33830341c46139bc847ddaab3db632265e))
|
||||
* **card-sorting:** enable New Game button during active gameplay ([f3f6eca](https://github.com/antialias/soroban-abacus-flashcards/commit/f3f6eca1db30df9e1e34cc4e77a069a6a3954f3d))
|
||||
* **card-sorting:** end drag immediately when card becomes locked ([ae45298](https://github.com/antialias/soroban-abacus-flashcards/commit/ae45298ec48efb29587c0a1c1a7986a72821f3ef))
|
||||
* **card-sorting:** filter local player from emoji overlays on dragged cards ([dc2d94a](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d94aaa58531ed4f9047e2ca92724d9264643d))
|
||||
* **card-sorting:** fix results panel layout to not cover cards ([4b4fbfe](https://github.com/antialias/soroban-abacus-flashcards/commit/4b4fbfef322ecda06020ad52d4b1788267112460))
|
||||
* **card-sorting:** hide activity notifications in spectator mode ([5cca279](https://github.com/antialias/soroban-abacus-flashcards/commit/5cca279687d8973d25bd9a411a55b632d1c82f63))
|
||||
* **card-sorting:** keep arrow sequence numbers upright ([79c9469](https://github.com/antialias/soroban-abacus-flashcards/commit/79c94699fa1cc2a2886e3ab1addc5fcd975602f5))
|
||||
* **card-sorting:** lock correctly positioned prefix/suffix cards ([170abed](https://github.com/antialias/soroban-abacus-flashcards/commit/170abed2318432f309de40692f6092bb4c4a1a45))
|
||||
* **card-sorting:** lock spring positions after initial animation completes ([275cc62](https://github.com/antialias/soroban-abacus-flashcards/commit/275cc62a523d9e849f2162001141b6d75ae0925e))
|
||||
* **card-sorting:** New Game now restarts with same settings instantly ([f3687ed](https://github.com/antialias/soroban-abacus-flashcards/commit/f3687ed236eff4ebe61699ec02909024c7086fb5))
|
||||
* **card-sorting:** only shrink/fade cards in correct prefix ([51368c6](https://github.com/antialias/soroban-abacus-flashcards/commit/51368c6ec59d5447ce2875c5e1181dec97fd509d))
|
||||
* **card-sorting:** preserve card positions on pause/resume ([0d8af09](https://github.com/antialias/soroban-abacus-flashcards/commit/0d8af09517534f1e1cf1f57160391d465a279d76))
|
||||
* **card-sorting:** preserve rotation when starting drag ([3364144](https://github.com/antialias/soroban-abacus-flashcards/commit/3364144fb6212934b6ad6d63ac6e7b78b436b258))
|
||||
* **card-sorting:** prevent duplicate START_GAME moves on Play Again ([a0b14f8](https://github.com/antialias/soroban-abacus-flashcards/commit/a0b14f87e9c5b32fcbb685da4e70c563f70ed91a))
|
||||
* **card-sorting:** prevent ghost movements with proper optimistic updates ([bd014be](https://github.com/antialias/soroban-abacus-flashcards/commit/bd014bec4ffa12bcd8f4a4e84ff51203c90c1f1d))
|
||||
* **card-sorting:** prevent infinite loop when all cards are correct ([34785f4](https://github.com/antialias/soroban-abacus-flashcards/commit/34785f466faaa6b9f2958df786af88561fa80b06))
|
||||
* **card-sorting:** prevent infinite loop with tolerance-based position comparison ([627b873](https://github.com/antialias/soroban-abacus-flashcards/commit/627b873382eaa76ad16477280d10451cf2951e1a))
|
||||
* **card-sorting:** prevent position jump when clicking rotated cards ([564a00f](https://github.com/antialias/soroban-abacus-flashcards/commit/564a00f82b6ca6aa8a2c0586ca49fc42d44991a8))
|
||||
* **card-sorting:** prevent replaying own movements from server ([308168a](https://github.com/antialias/soroban-abacus-flashcards/commit/308168a7fb51013b0851e98b161ba1a1a3e39fbb))
|
||||
* **card-sorting:** prevent springs from reinitializing on window resize ([30953b8](https://github.com/antialias/soroban-abacus-flashcards/commit/30953b8c4a3cf147f980455818f9ce8eea07837c))
|
||||
* **card-sorting:** prevent springs from resetting after animation ([8aff60c](https://github.com/antialias/soroban-abacus-flashcards/commit/8aff60ce3f8d302ce5c1bde7cb773e63064c36b7))
|
||||
* **card-sorting:** remove hasAnimatedRef logic causing backwards animation ([a44aa5a](https://github.com/antialias/soroban-abacus-flashcards/commit/a44aa5a4c2d84cab7cf0bbf87485bb61548fdeb2))
|
||||
* **card-sorting:** remove remaining reveal numbers references ([15c53ea](https://github.com/antialias/soroban-abacus-flashcards/commit/15c53ea4eb4abb824eb0360fb645b1f3e455578e))
|
||||
* **card-sorting:** restore prefix/suffix card shrinking visual feedback ([f5fb4d7](https://github.com/antialias/soroban-abacus-flashcards/commit/f5fb4d7b76e25286bcdecd017894ff2d78b31963))
|
||||
* **card-sorting:** show only active players in team members section ([fa9f1a5](https://github.com/antialias/soroban-abacus-flashcards/commit/fa9f1a568f3dff2f4e5e7d3e8841b951ef1b7d04))
|
||||
* **card-sorting:** smooth scale animation while dragging cards ([0eefc33](https://github.com/antialias/soroban-abacus-flashcards/commit/0eefc332ac2724c54b477301a269915e895db94f))
|
||||
* **card-sorting:** stabilize inferred sequence for locked cards during drag ([b0cd194](https://github.com/antialias/soroban-abacus-flashcards/commit/b0cd194838705bb7bbf21ac9e318eaba491097b2))
|
||||
* **card-sorting:** use empty deps array for useSprings to prevent recreation ([cee399e](https://github.com/antialias/soroban-abacus-flashcards/commit/cee399ed1513d32d0fff51a6f63898aa861605e1))
|
||||
* **card-sorting:** use ref to track initialized state and prevent re-animation ([f389afa](https://github.com/antialias/soroban-abacus-flashcards/commit/f389afa831935e896a626f526cfee378e340a64b))
|
||||
* **card-sorting:** use same coordinate system for game board and results ([6972fdf](https://github.com/antialias/soroban-abacus-flashcards/commit/6972fdf1105b6e854494efe1c4c587e6b6ff32a9))
|
||||
* **complement-race:** prevent delivery move thrashing in steam sprint mode ([e1258ee](https://github.com/antialias/soroban-abacus-flashcards/commit/e1258ee0416010909774694c0b25306b6f30329c))
|
||||
* configure favicon metadata and improve bead visibility ([e1369fa](https://github.com/antialias/soroban-abacus-flashcards/commit/e1369fa2754cd61745a2950e6cb767d6b08db38f))
|
||||
* copy entire packages/core and packages/templates ([0ccada0](https://github.com/antialias/soroban-abacus-flashcards/commit/0ccada0ca783e635f9ae08f33a69c392018ee342))
|
||||
* correct hero abacus scroll direction to flow with page content ([4232746](https://github.com/antialias/soroban-abacus-flashcards/commit/423274657c9698bba28f7246fbf48d8508d97ef9))
|
||||
* correct Typst template path in Dockerfile ([4c518de](https://github.com/antialias/soroban-abacus-flashcards/commit/4c518decb7fcc0b519d07680cbfd01c94c23dd41))
|
||||
* delete existing user sessions before creating new ones ([0cced47](https://github.com/antialias/soroban-abacus-flashcards/commit/0cced47a0f414a04371bdb253fc5a43e4d9557be))
|
||||
* extract pure SVG content from AbacusReact renders ([b07f1c4](https://github.com/antialias/soroban-abacus-flashcards/commit/b07f1c421616bcfd1f949f9a42ce1b03df418945))
|
||||
* **games:** prevent horizontal page scroll from carousel overflow ([5a8c98f](https://github.com/antialias/soroban-abacus-flashcards/commit/5a8c98fc10704e459690308a84dc7ee2bfa0ef6c))
|
||||
* **games:** smooth scroll feel for carousel wheel navigation ([f80a73b](https://github.com/antialias/soroban-abacus-flashcards/commit/f80a73b35c324959bfd7141ebf086cb47d3c0ebc))
|
||||
* **games:** use specific transition properties for smooth carousel loop ([187271e](https://github.com/antialias/soroban-abacus-flashcards/commit/187271e51527ee0129f71d77be1bd24072b963c4))
|
||||
* **i18n:** eliminate FOUC by loading messages server-side ([4d4d930](https://github.com/antialias/soroban-abacus-flashcards/commit/4d4d930bd307ce5a405fc5751af6682a9f221f1f))
|
||||
* **i18n:** use useMessages() for tutorial translations ([95b0105](https://github.com/antialias/soroban-abacus-flashcards/commit/95b0105ca3b28c5adfa843e8d77a8b27d9e7ade4))
|
||||
* include column posts in favicon bounding box ([0b2f481](https://github.com/antialias/soroban-abacus-flashcards/commit/0b2f48106a939307b728c86fe2ea1be1e0247ea8))
|
||||
* increase server update debounce to 2000ms for low bandwidth ([633ff12](https://github.com/antialias/soroban-abacus-flashcards/commit/633ff127500c893a215491afa0e6ff814ad553bf))
|
||||
* Integrate threshold input into Point Victory card ([b29bbee](https://github.com/antialias/soroban-abacus-flashcards/commit/b29bbeefcad92be42f7a3ca27ac126db4232ab26))
|
||||
* mark dynamic routes as force-dynamic to prevent static generation errors ([d7b35d9](https://github.com/antialias/soroban-abacus-flashcards/commit/d7b35d954421fd7577cd2c26247666e5953b647d))
|
||||
* **nav:** show full navigation on /games page ([d3fe6ac](https://github.com/antialias/soroban-abacus-flashcards/commit/d3fe6acbb0390e1df71869a4095e5ee6021e06b1))
|
||||
* **qr-button:** improve layout and z-index ([646a422](https://github.com/antialias/soroban-abacus-flashcards/commit/646a4228d0573796b1a429e31bc037411024c0ff))
|
||||
* **qr-button:** increase mini QR code size to 80px ([61ac737](https://github.com/antialias/soroban-abacus-flashcards/commit/61ac7378bdb01132b26bfc265a057c095ea41606))
|
||||
* **qr-button:** increase mini QR code to 84px ([3fae5ea](https://github.com/antialias/soroban-abacus-flashcards/commit/3fae5ea6fa9ebd0f8fe8c9140a027be7f6a041aa))
|
||||
* **qr-button:** make button square and increase QR size ([dc2d466](https://github.com/antialias/soroban-abacus-flashcards/commit/dc2d46663b8e0ec94a1508a57c4f8c2d8ba03506))
|
||||
* **qr-button:** match height of stacked buttons ([81f202d](https://github.com/antialias/soroban-abacus-flashcards/commit/81f202d21556aa430402fda814519adbc8883831))
|
||||
* reduce padding to minimize gap below last bead ([0e529be](https://github.com/antialias/soroban-abacus-flashcards/commit/0e529be789caf16e73f3e2ee77f52e243841aef4))
|
||||
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
|
||||
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
|
||||
* resolve z-index layering and hero abacus visibility issues ([ed9a050](https://github.com/antialias/soroban-abacus-flashcards/commit/ed9a050d64db905e1328008f25dc0014e9a81999))
|
||||
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
|
||||
* **rithmomachia:** add missing i18next dependencies ([91154d9](https://github.com/antialias/soroban-abacus-flashcards/commit/91154d93647e59f7e5f96d1db5624a7ec9b1b9ff))
|
||||
* **rithmomachia:** add missing pyramid section keys to Japanese (ja.json) ([dae615e](https://github.com/antialias/soroban-abacus-flashcards/commit/dae615ee72a7ec7d0b235a22c61ebc4af0d8eadb))
|
||||
* **rithmomachia:** adjust error dialog sizing to prevent text clipping ([cda1126](https://github.com/antialias/soroban-abacus-flashcards/commit/cda1126cb0eab6840df89f3a8778d72410298093))
|
||||
* **rithmomachia:** adjust roster notice position to not overlap nav ([7093223](https://github.com/antialias/soroban-abacus-flashcards/commit/709322373a91c8174d21052d184fa84dd8bda326))
|
||||
* **rithmomachia:** change undock icon to pop-out arrow ([2a91748](https://github.com/antialias/soroban-abacus-flashcards/commit/2a917484938bc269cf16acb501d4d26584405e0f))
|
||||
* **rithmomachia:** correct board dimensions to 16x8 and restore original layout values ([cfac277](https://github.com/antialias/soroban-abacus-flashcards/commit/cfac27750526fb1f6a7e4314a96aab3b92e08e44))
|
||||
* **rithmomachia:** Correct board setup to match reference image exactly ([618e563](https://github.com/antialias/soroban-abacus-flashcards/commit/618e56358deb66cba968472f39b8d4e28b4dd211))
|
||||
* **rithmomachia:** correct makeMove parameter types for capture handling ([aafb64f](https://github.com/antialias/soroban-abacus-flashcards/commit/aafb64f3e337c6cf925766fe179b91f66c4a040b))
|
||||
* **rithmomachia:** fix guide modal resize drift by calculating from initial state ([1bcd99c](https://github.com/antialias/soroban-abacus-flashcards/commit/1bcd99c949e4d2b4fb1c0813debd50176fa58cb9))
|
||||
* **rithmomachia:** fix harmony section translation structure for hi/ja/es ([14259a1](https://github.com/antialias/soroban-abacus-flashcards/commit/14259a19a9817d0947467faa004d5f43118f8d8d))
|
||||
* **rithmomachia:** fix modal resizing zoom issue ([4fa20f4](https://github.com/antialias/soroban-abacus-flashcards/commit/4fa20f44cb9758f29d1f1512232be0fdc0b53b3d))
|
||||
* **rithmomachia:** Fix TypeScript errors in playing guide modal ([4834ece](https://github.com/antialias/soroban-abacus-flashcards/commit/4834ece98e86f2fb00511bb876a5c32c289df0e0))
|
||||
* **rithmomachia:** handle pyramid pieces in hover error tooltip ([56f3164](https://github.com/antialias/soroban-abacus-flashcards/commit/56f3164155beb94ceec2838bed9fc74fd75524db))
|
||||
* **rithmomachia:** implement proper board cropping and highlighting in guide ([d0a8fcd](https://github.com/antialias/soroban-abacus-flashcards/commit/d0a8fcdea6aa4fdacfee33e183c92923634ee2b7))
|
||||
* **rithmomachia:** improve guide modal tab navigation at narrow widths ([a673177](https://github.com/antialias/soroban-abacus-flashcards/commit/a673177bec1c709463ce0f266848f473a79f4ef0))
|
||||
* **rithmomachia:** reconnect player assignment UI and fix setup layout ([a1a0374](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a0374fac5dce676df5890663b75531589ed93a))
|
||||
* **rithmomachia:** render guide as docked in preview panel ([190f8cf](https://github.com/antialias/soroban-abacus-flashcards/commit/190f8cf302aa966f029d05931811e217c67bfe39))
|
||||
* **rithmomachia:** show actual values in tooltips for non-helper relations ([774c6b0](https://github.com/antialias/soroban-abacus-flashcards/commit/774c6b0ce712b1a77bb684457da9831e6ec91138))
|
||||
* **rithmomachia:** show guest-friendly message when they can't fix too many players ([54bfd2f](https://github.com/antialias/soroban-abacus-flashcards/commit/54bfd2fac86be3597d40c67a1235e4c4ed8e2709))
|
||||
* **rithmomachia:** smooth guide dragging from docked state without jump ([8f4a79c](https://github.com/antialias/soroban-abacus-flashcards/commit/8f4a79c9b0cad55336584fdc8e67409015d3a8ae))
|
||||
* **rithmomachia:** validate move path before showing capture error on hover ([bd49964](https://github.com/antialias/soroban-abacus-flashcards/commit/bd49964186a0daa1639ae849b128a76081643daf))
|
||||
* **room-info:** hide Leave Room button when user is alone ([5927f61](https://github.com/antialias/soroban-abacus-flashcards/commit/5927f61c3c34ba583ee45c8cee48a116c1c03071))
|
||||
* separate horizontal and vertical bounding box logic ([83090df](https://github.com/antialias/soroban-abacus-flashcards/commit/83090df4dfad1d1d5cfa6c278c241526cacc7972))
|
||||
* tolerate OpenSCAD CGAL warnings if output file is created ([88993f3](https://github.com/antialias/soroban-abacus-flashcards/commit/88993f36629206a7bdcf9aa9d5641f1580b64de5))
|
||||
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
|
||||
* use Debian base for deps stage to match runner for binary compatibility ([f8fe6e4](https://github.com/antialias/soroban-abacus-flashcards/commit/f8fe6e4a415f8655626af567129d0cda61b82e15))
|
||||
* use default BOSL2 branch instead of non-existent v2.0.0 tag ([f4ffc5b](https://github.com/antialias/soroban-abacus-flashcards/commit/f4ffc5b0277535358bea7588309a1a4afd1983a1))
|
||||
* use nested SVG viewBox for actual cropping, not just scaling ([440b492](https://github.com/antialias/soroban-abacus-flashcards/commit/440b492e85beff1612697346b6c5cfc8461e83da))
|
||||
* various game improvements and UI enhancements ([b67cf61](https://github.com/antialias/soroban-abacus-flashcards/commit/b67cf610c570d54744553cd8f6694243fa50bee1))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* optimize Docker image size to reduce build failures ([9ca3106](https://github.com/antialias/soroban-abacus-flashcards/commit/9ca310636183f4970db925ce8fa368e23645eb02))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **card-sorting:** remove reveal numbers feature ([ea5e3e8](https://github.com/antialias/soroban-abacus-flashcards/commit/ea5e3e838bd6a5b8b38469a70aa92a0e9baba769))
|
||||
* **card-sorting:** send complete card sequence instead of individual moves ([e4df843](https://github.com/antialias/soroban-abacus-flashcards/commit/e4df8432b9c4a2055d47833d56b6e9fcf325ca94))
|
||||
* **games:** implement carousel, fix victories bug, add conditional stats ([82c133f](https://github.com/antialias/soroban-abacus-flashcards/commit/82c133f742f3f5c40b723c18d1997b518f25b320))
|
||||
* **games:** move page title to nav bar ([712ee58](https://github.com/antialias/soroban-abacus-flashcards/commit/712ee58e5956e5bbdb13d5a5fb367020c87c8c9a))
|
||||
* **games:** remove redundant subtitle below nav ([ad5bb87](https://github.com/antialias/soroban-abacus-flashcards/commit/ad5bb87325a44825f0cd85b38eb0e5f0eea7a695))
|
||||
* **games:** remove wheel scrolling, enable overflow visible carousel ([876513c](https://github.com/antialias/soroban-abacus-flashcards/commit/876513c9cc6323c20845ae8f1a3a5478d449f9e4))
|
||||
* reorganize Harmony and Victory guide sections ([fb629c4](https://github.com/antialias/soroban-abacus-flashcards/commit/fb629c44ea37a7b296561919a4980c10d14efed8))
|
||||
* restructure /create page into hub with sub-pages ([b91b23d](https://github.com/antialias/soroban-abacus-flashcards/commit/b91b23d95ffaeeaa30dbc8579f4c30bae8829ee7))
|
||||
* **rithmomachia:** extract board and capture components (phase 2+3) ([a0a867b](https://github.com/antialias/soroban-abacus-flashcards/commit/a0a867b27166a838ca7e0dcbd8f89fe1be812a80))
|
||||
* **rithmomachia:** extract CaptureErrorDialog component (Phase 2 partial) ([f0a066d](https://github.com/antialias/soroban-abacus-flashcards/commit/f0a066d8f0a51d35e18f87a8436c0d05153c03b5))
|
||||
* **rithmomachia:** extract constants and coordinate utilities (Phase 1) ([eace0ed](https://github.com/antialias/soroban-abacus-flashcards/commit/eace0ed52979b71870f77ee68f8568558f2aaecb))
|
||||
* **rithmomachia:** extract guide sections into separate files ([765525d](https://github.com/antialias/soroban-abacus-flashcards/commit/765525dc451897f561f017e444aae892dc27177f))
|
||||
* **rithmomachia:** extract hooks (phase 5) ([324a659](https://github.com/antialias/soroban-abacus-flashcards/commit/324a65992f97c295ea3968aaf54d266373f4c035))
|
||||
* **rithmomachia:** extract phase components (phase 4) ([11364f6](https://github.com/antialias/soroban-abacus-flashcards/commit/11364f6394c15e49850e5cad2cbd32e1ea08a178))
|
||||
* **rithmomachia:** extract reusable components from SetupPhase ([3abc325](https://github.com/antialias/soroban-abacus-flashcards/commit/3abc325ea27feee5c4cc59f02296ff218f342a81))
|
||||
* **rithmomachia:** make setup phase UI more compact ([e55f848](https://github.com/antialias/soroban-abacus-flashcards/commit/e55f848a26092a2b4a5b09c3c255544ea9666f1b))
|
||||
* **rithmomachia:** redesign error notification with modern UI ([dfeeb0e](https://github.com/antialias/soroban-abacus-flashcards/commit/dfeeb0e0db8b2c4a38198cf71cd918439d6c211b)), closes [#1e293](https://github.com/antialias/soroban-abacus-flashcards/issues/1e293) [#0f172](https://github.com/antialias/soroban-abacus-flashcards/issues/0f172) [#f1f5f9](https://github.com/antialias/soroban-abacus-flashcards/issues/f1f5f9)
|
||||
* **rithmomachia:** simplify capture error dialog to one-liner ([82a5eb2](https://github.com/antialias/soroban-abacus-flashcards/commit/82a5eb2e4bf74f42a183a15f1129e5ec84cc5231))
|
||||
* **rithmomachia:** Update board setup to authoritative CSV layout ([0471da5](https://github.com/antialias/soroban-abacus-flashcards/commit/0471da598d8d591b3f9d63f467cb35f999924c13))
|
||||
* **rithmomachia:** update capture components to use CaptureContext ([2ab6ab5](https://github.com/antialias/soroban-abacus-flashcards/commit/2ab6ab57995a6d7d9c66b9fba8de945507209661))
|
||||
* **rithmomachia:** use useBoardLayout and usePieceSelection in BoardDisplay ([0ab7a1d](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab7a1df327d7258228af9851762555583a20d61))
|
||||
* use AbacusReact for dynamic Open Graph image ([9c20f12](https://github.com/antialias/soroban-abacus-flashcards/commit/9c20f12bacff4fe7f8bd7a87032afbed9711e94b))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add 3D enhancement documentation to README ([cc96802](https://github.com/antialias/soroban-abacus-flashcards/commit/cc96802df87c805c946ee59af509663ba570e75b))
|
||||
* add database migration guide and playing guide modal spec ([5a29af7](https://github.com/antialias/soroban-abacus-flashcards/commit/5a29af78e27e897ab35273611b79c4b669304f71))
|
||||
* add deployment verification guidelines to prevent false positives ([3d8da23](https://github.com/antialias/soroban-abacus-flashcards/commit/3d8da2348b4e8a227e963791d15dc6718eac5af1))
|
||||
* **card-sorting:** add comprehensive multiplayer plan ([008ccea](https://github.com/antialias/soroban-abacus-flashcards/commit/008ccead0f9c634fe52fd156e6f9a04d6cdd7744))
|
||||
* **rithmomachia:** Add concise one-page playing guide ([e3c1f10](https://github.com/antialias/soroban-abacus-flashcards/commit/e3c1f10233cc0924ff96a643c7c4c1f1278de3e3))
|
||||
* update workflow to require manual testing before commits ([0991796](https://github.com/antialias/soroban-abacus-flashcards/commit/0991796f1eccef345f10205e675e4c33d1a62b17))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **rithmomachia:** improve divider styling and make tabs responsive ([88ca35e](https://github.com/antialias/soroban-abacus-flashcards/commit/88ca35e0440157ff9349e8d3d2d3cc844f18ffea)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3)
|
||||
* **rithmomachia:** improve pyramid face numbers visibility and contrast ([94e5e6a](https://github.com/antialias/soroban-abacus-flashcards/commit/94e5e6a268b387380b88b192737bd55578b98bc7)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#b45309](https://github.com/antialias/soroban-abacus-flashcards/issues/b45309)
|
||||
* **rithmomachia:** increase pyramid face numbers size and boldness ([7bf2d73](https://github.com/antialias/soroban-abacus-flashcards/commit/7bf2d730d370e562486b229f4d209099ff8c4463))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* trigger compose-updater deployment test ([2b06aae](https://github.com/antialias/soroban-abacus-flashcards/commit/2b06aae39474cc80d501c47c9685fa99e7120c48))
|
||||
|
||||
## [4.68.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.67.1...v4.68.0) (2025-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus:** add nativeAbacusNumbers setting to schema and UI ([79f7347](https://github.com/antialias/soroban-abacus-flashcards/commit/79f7347d4800646378470a7f9aca8e7f2fd5573c))
|
||||
|
||||
@@ -161,11 +161,14 @@
|
||||
"Bash(printenv:*)",
|
||||
"Bash(typst:*)",
|
||||
"Bash(npx tsx:*)",
|
||||
"Bash(sort:*)"
|
||||
"Bash(sort:*)",
|
||||
"Bash(scp:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["sqlite"]
|
||||
"enabledMcpjsonServers": [
|
||||
"sqlite"
|
||||
]
|
||||
}
|
||||
|
||||
1
apps/web/README.md
Normal file
1
apps/web/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Test deployment - Mon Nov 3 16:31:57 CST 2025
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { writeFileSync, readFileSync, mkdirSync, rmSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
64
apps/web/src/app/test-static-abacus/page.tsx
Normal file
64
apps/web/src/app/test-static-abacus/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Test page for AbacusStatic - Server Component
|
||||
* This demonstrates that AbacusStatic works without "use client"
|
||||
*/
|
||||
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
export default function TestStaticAbacusPage() {
|
||||
const numbers = [1, 2, 3, 4, 5, 10, 25, 50, 100, 123, 456, 789]
|
||||
|
||||
return (
|
||||
<div style={{ padding: '40px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1 style={{ marginBottom: '10px' }}>AbacusStatic Test (Server Component)</h1>
|
||||
<p style={{ color: '#64748b', marginBottom: '30px' }}>
|
||||
This page is a React Server Component - no "use client" directive!
|
||||
All abacus displays below are rendered on the server with zero client-side JavaScript.
|
||||
</p>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))',
|
||||
gap: '20px',
|
||||
}}
|
||||
>
|
||||
{numbers.map((num) => (
|
||||
<div
|
||||
key={num}
|
||||
style={{
|
||||
padding: '20px',
|
||||
background: 'white',
|
||||
border: '2px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<AbacusStatic
|
||||
value={num}
|
||||
columns="auto"
|
||||
hideInactiveBeads
|
||||
compact
|
||||
scaleFactor={0.9}
|
||||
/>
|
||||
<span style={{ fontSize: '20px', fontWeight: 'bold', color: '#475569' }}>
|
||||
{num}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '40px', padding: '20px', background: '#f0fdf4', borderRadius: '8px' }}>
|
||||
<h2 style={{ marginTop: 0, color: '#166534' }}>✅ Success!</h2>
|
||||
<p style={{ color: '#15803d' }}>
|
||||
If you can see the abacus displays above, then AbacusStatic is working correctly
|
||||
in React Server Components. Check the page source - you'll see pure HTML/SVG with
|
||||
no client-side hydration markers!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { AbacusReact, useAbacusConfig, ABACUS_THEMES } from '@soroban/abacus-react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useMyAbacus } from '@/contexts/MyAbacusContext'
|
||||
import { HomeHeroContext } from '@/contexts/HomeHeroContext'
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
|
||||
export function MyAbacus() {
|
||||
const { isOpen, close, toggle } = useMyAbacus()
|
||||
@@ -31,21 +30,6 @@ export function MyAbacus() {
|
||||
const isHeroVisible = homeHeroContext?.isHeroVisible ?? false
|
||||
const isHeroMode = isOnHomePage && isHeroVisible && !isOpen
|
||||
|
||||
// Track scroll position for hero mode
|
||||
const [scrollY, setScrollY] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isHeroMode) return
|
||||
|
||||
const handleScroll = () => {
|
||||
setScrollY(window.scrollY)
|
||||
}
|
||||
|
||||
handleScroll() // Initial position
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [isHeroMode])
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@@ -72,33 +56,9 @@ export function MyAbacus() {
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Hero mode styles - white structural (from original HeroAbacus)
|
||||
const structuralStyles = {
|
||||
columnPosts: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// Trophy abacus styles - golden/premium look
|
||||
const trophyStyles = {
|
||||
columnPosts: {
|
||||
fill: '#fbbf24',
|
||||
stroke: '#f59e0b',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: '#fbbf24',
|
||||
stroke: '#f59e0b',
|
||||
strokeWidth: 4,
|
||||
},
|
||||
}
|
||||
// Use theme presets from abacus-react instead of manual definitions
|
||||
const structuralStyles = ABACUS_THEMES.light
|
||||
const trophyStyles = ABACUS_THEMES.trophy
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -114,7 +74,7 @@ export function MyAbacus() {
|
||||
inset: 0,
|
||||
bg: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
zIndex: Z_INDEX.MY_ABACUS_BACKDROP,
|
||||
zIndex: 101,
|
||||
animation: 'backdropFadeIn 0.4s ease-out',
|
||||
})}
|
||||
onClick={close}
|
||||
@@ -144,7 +104,7 @@ export function MyAbacus() {
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
zIndex: Z_INDEX.MY_ABACUS + 1,
|
||||
zIndex: 103,
|
||||
animation: 'fadeIn 0.3s ease-out 0.2s both',
|
||||
_hover: {
|
||||
bg: 'rgba(255, 255, 255, 0.2)',
|
||||
@@ -162,36 +122,28 @@ export function MyAbacus() {
|
||||
data-component="my-abacus"
|
||||
data-mode={isOpen ? 'open' : isHeroMode ? 'hero' : 'button'}
|
||||
onClick={isOpen || isHeroMode ? undefined : toggle}
|
||||
style={
|
||||
isHeroMode
|
||||
? {
|
||||
// Hero mode: position accounts for scroll to flow with page (subtract scroll to move up with content)
|
||||
// Positioned lower (60vh instead of 50vh) to avoid covering subtitle
|
||||
top: `calc(60vh - ${scrollY}px)`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={css({
|
||||
position: 'fixed',
|
||||
zIndex: Z_INDEX.MY_ABACUS,
|
||||
position: isHeroMode ? 'absolute' : 'fixed',
|
||||
zIndex: 102,
|
||||
cursor: isOpen || isHeroMode ? 'default' : 'pointer',
|
||||
transition: isHeroMode ? 'none' : 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
// Three modes: hero (inline with content), button (bottom-right), open (center)
|
||||
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
// Three modes: hero (absolute - scrolls with document), button (fixed), open (fixed)
|
||||
...(isOpen
|
||||
? {
|
||||
// Open mode: center of screen
|
||||
// Open mode: fixed to center of viewport
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}
|
||||
: isHeroMode
|
||||
? {
|
||||
// Hero mode: centered horizontally, top handled by inline style
|
||||
// Hero mode: absolute positioning - scrolls naturally with document
|
||||
top: '60vh',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}
|
||||
: {
|
||||
// Button mode: bottom-right corner
|
||||
// Button mode: fixed to bottom-right corner
|
||||
bottom: { base: '4', md: '6' },
|
||||
right: { base: '4', md: '6' },
|
||||
transform: 'translate(0, 0)',
|
||||
@@ -259,58 +211,22 @@ export function MyAbacus() {
|
||||
animated={isOpen || isHeroMode}
|
||||
customStyles={isHeroMode ? structuralStyles : trophyStyles}
|
||||
onValueChange={setAbacusValue}
|
||||
// 3D Enhancement - realistic mode for hero and open states
|
||||
enhanced3d={isOpen || isHeroMode ? 'realistic' : undefined}
|
||||
material3d={
|
||||
isOpen || isHeroMode
|
||||
? {
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'satin',
|
||||
lighting: 'dramatic',
|
||||
woodGrain: true,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and achievement info - only visible when open */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
mt: { base: '16', md: '20', lg: '24' },
|
||||
textAlign: 'center',
|
||||
animation: 'fadeIn 0.5s ease-out 0.3s both',
|
||||
maxW: '600px',
|
||||
px: '8',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl', lg: '4xl' },
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
My Abacus
|
||||
</h2>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'md', md: 'lg' },
|
||||
color: 'gray.300',
|
||||
mb: '4',
|
||||
fontWeight: 'medium',
|
||||
})}
|
||||
>
|
||||
Your personal abacus grows with you
|
||||
</p>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'sm', md: 'md' },
|
||||
color: 'gray.400',
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
Complete tutorials, play games, and earn achievements to unlock higher place values
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Keyframes for animations */}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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[] }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"Bash(git add:*)",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(npm run build:*)"
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(git reset:*)",
|
||||
"Bash(cat:*)"
|
||||
]
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
|
||||
@@ -1,3 +1,43 @@
|
||||
# [2.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.5.0...abacus-react-v2.6.0) (2025-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **abacus-react:** correct column highlighting offset in AbacusStatic ([0641eb7](https://github.com/antialias/soroban-abacus-flashcards/commit/0641eb719ef56c67de965296006df666f83e5b08))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** add AbacusStatic for React Server Components ([3b8e864](https://github.com/antialias/soroban-abacus-flashcards/commit/3b8e864cfa3af50b1912ce7ff55003d7f6b9c229))
|
||||
* **web:** add test page for AbacusStatic Server Component ([3588d5a](https://github.com/antialias/soroban-abacus-flashcards/commit/3588d5acde25588ce4db3ee32adb04ace0e394d4))
|
||||
|
||||
# [2.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.4.0...abacus-react-v2.5.0) (2025-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** add core utility functions for state management ([e65541c](https://github.com/antialias/soroban-abacus-flashcards/commit/e65541c100e590a51448750c6d5178ed4f3e8eeb))
|
||||
* **abacus-react:** add layout and educational props ([35bbcec](https://github.com/antialias/soroban-abacus-flashcards/commit/35bbcecb9e36f1ef5917a5a629f5e78f1f490e9c))
|
||||
* **abacus-react:** add pre-defined theme presets ([cf1f950](https://github.com/antialias/soroban-abacus-flashcards/commit/cf1f950c7c5fb9ee1f0de673235d6f037be3b9d6))
|
||||
* **abacus-react:** add React hooks for abacus calculations ([de038d2](https://github.com/antialias/soroban-abacus-flashcards/commit/de038d2afc26c36c1490d5ea45dace0ab812c5cc))
|
||||
* **abacus-react:** export new utilities, hooks, and themes ([ce4e44d](https://github.com/antialias/soroban-abacus-flashcards/commit/ce4e44d6302746053ad40dc61bab57ef3a0a9f31))
|
||||
|
||||
# [2.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.3.0...abacus-react-v2.4.0) (2025-11-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove distracting parallax and wobble 3D effects ([28a2d40](https://github.com/antialias/soroban-abacus-flashcards/commit/28a2d40996256700bf19cd80130b26e24441949f))
|
||||
* remove wobble physics and enhance wood grain visibility ([5d97673](https://github.com/antialias/soroban-abacus-flashcards/commit/5d976734062eb3d943bfdfdd125473c56b533759))
|
||||
* rewrite 3D stories to use props instead of CSS wrappers ([26bdb11](https://github.com/antialias/soroban-abacus-flashcards/commit/26bdb112370cece08634e3d693d15336111fc70f))
|
||||
* use absolute positioning for hero abacus to eliminate scroll lag ([096104b](https://github.com/antialias/soroban-abacus-flashcards/commit/096104b094b45aa584f2b9d47a440a8c14d82fc0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* complete 3D enhancement integration for all three proposals ([5ac55cc](https://github.com/antialias/soroban-abacus-flashcards/commit/5ac55cc14980b778f9be32f0833f8760aa16b631))
|
||||
* enable 3D enhancement on hero/open MyAbacus modes ([37e330f](https://github.com/antialias/soroban-abacus-flashcards/commit/37e330f26e5398c2358599361cd417b4aeefac7d))
|
||||
|
||||
# [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.2.0...abacus-react-v2.3.0) (2025-11-03)
|
||||
|
||||
|
||||
|
||||
402
packages/abacus-react/ENHANCEMENT_PLAN.md
Normal file
402
packages/abacus-react/ENHANCEMENT_PLAN.md
Normal 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?
|
||||
162
packages/abacus-react/INTEGRATION_SUMMARY.md
Normal file
162
packages/abacus-react/INTEGRATION_SUMMARY.md
Normal 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
|
||||
@@ -13,6 +13,8 @@ A comprehensive React component for rendering interactive Soroban (Japanese abac
|
||||
- 🔧 **Developer-friendly** - Comprehensive hooks and callback system
|
||||
- 🎓 **Tutorial system** - Built-in overlay and guidance capabilities
|
||||
- 🧩 **Framework-free SVG** - Complete control over rendering
|
||||
- ✨ **3D Enhancement** - Three levels of progressive 3D effects for immersive visuals
|
||||
- 🚀 **Server Component support** - AbacusStatic works in React Server Components (Next.js App Router)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -85,34 +87,227 @@ Personalized colors and highlights
|
||||
/>
|
||||
```
|
||||
|
||||
### Theme Presets
|
||||
|
||||
Use pre-defined themes for quick styling:
|
||||
|
||||
```tsx
|
||||
import { AbacusReact, ABACUS_THEMES } from '@soroban/abacus-react';
|
||||
|
||||
// Available themes: 'light', 'dark', 'trophy', 'translucent', 'solid', 'traditional'
|
||||
<AbacusReact
|
||||
value={123}
|
||||
columns={3}
|
||||
customStyles={ABACUS_THEMES.dark}
|
||||
/>
|
||||
|
||||
<AbacusReact
|
||||
value={456}
|
||||
columns={3}
|
||||
customStyles={ABACUS_THEMES.trophy} // Golden frame for achievements
|
||||
/>
|
||||
|
||||
<AbacusReact
|
||||
value={789}
|
||||
columns={3}
|
||||
customStyles={ABACUS_THEMES.traditional} // Brown wooden appearance
|
||||
/>
|
||||
```
|
||||
|
||||
**Available Themes:**
|
||||
- `light` - Solid white frame with subtle gray accents (best for light backgrounds)
|
||||
- `dark` - Translucent white with subtle glow (best for dark backgrounds)
|
||||
- `trophy` - Golden frame with warm tones (best for achievements/rewards)
|
||||
- `translucent` - Nearly invisible frame (best for inline/minimal UI)
|
||||
- `solid` - Black frame (best for high contrast/educational contexts)
|
||||
- `traditional` - Brown wooden appearance (best for traditional soroban aesthetic)
|
||||
|
||||
### Static Display (Server Components)
|
||||
|
||||
For static, non-interactive displays that work in React Server Components:
|
||||
|
||||
```tsx
|
||||
import { AbacusStatic } from '@soroban/abacus-react';
|
||||
|
||||
// ✅ Works in React Server Components - no "use client" needed!
|
||||
// ✅ No JavaScript sent to client
|
||||
// ✅ Perfect for SSG, SSR, and static previews
|
||||
|
||||
<AbacusStatic
|
||||
value={123}
|
||||
columns="auto"
|
||||
hideInactiveBeads
|
||||
compact
|
||||
/>
|
||||
```
|
||||
|
||||
**When to use `AbacusStatic` vs `AbacusReact`:**
|
||||
|
||||
| Feature | AbacusStatic | AbacusReact |
|
||||
|---------|--------------|-------------|
|
||||
| React Server Components | ✅ Yes | ❌ No (requires "use client") |
|
||||
| Client-side JavaScript | ❌ None | ✅ Yes |
|
||||
| User interaction | ❌ No | ✅ Click/drag beads |
|
||||
| Animations | ❌ No | ✅ Smooth transitions |
|
||||
| Sound effects | ❌ No | ✅ Optional sounds |
|
||||
| 3D effects | ❌ No | ✅ Yes |
|
||||
| Bundle size | 📦 Minimal | 📦 Full-featured |
|
||||
| Use cases | Preview cards, thumbnails, static pages | Interactive tutorials, games, tools |
|
||||
|
||||
```tsx
|
||||
// Example: Server Component with static abacus cards
|
||||
// app/flashcards/page.tsx
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
export default function FlashcardsPage() {
|
||||
const numbers = [1, 5, 10, 25, 50, 100]
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{numbers.map(num => (
|
||||
<div key={num} className="card">
|
||||
<AbacusStatic value={num} columns="auto" compact />
|
||||
<p>{num}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Compact/Inline Display
|
||||
|
||||
Create mini abacus displays for inline use:
|
||||
|
||||
```tsx
|
||||
// Compact mode - automatically hides frame and optimizes spacing
|
||||
<AbacusReact
|
||||
value={7}
|
||||
columns={1}
|
||||
compact={true}
|
||||
hideInactiveBeads={true}
|
||||
scaleFactor={0.7}
|
||||
/>
|
||||
|
||||
// Or manually control frame visibility
|
||||
<AbacusReact
|
||||
value={42}
|
||||
columns={2}
|
||||
frameVisible={false} // Hide column posts and reckoning bar
|
||||
/>
|
||||
```
|
||||
|
||||
### Tutorial System
|
||||
|
||||
Educational guidance with tooltips
|
||||
Educational guidance with tooltips and column highlighting
|
||||
|
||||
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/tutorial-mode.svg" alt="Tutorial System">
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
value={42}
|
||||
columns={2}
|
||||
columns={3}
|
||||
interactive={true}
|
||||
// Highlight the tens column with a label
|
||||
highlightColumns={[1]} // Highlight column index 1 (tens)
|
||||
columnLabels={['ones', 'tens', 'hundreds']} // Add labels to columns
|
||||
overlays={[{
|
||||
id: 'tip',
|
||||
type: 'tooltip',
|
||||
target: { type: 'bead', columnIndex: 0, beadType: 'earth', beadPosition: 1 },
|
||||
content: <div>Click this bead!</div>,
|
||||
target: { type: 'bead', columnIndex: 1, beadType: 'earth', beadPosition: 1 },
|
||||
content: <div>Click this bead in the tens column!</div>,
|
||||
offset: { x: 0, y: -30 }
|
||||
}]}
|
||||
callbacks={{
|
||||
onBeadClick: (event) => {
|
||||
if (event.columnIndex === 0 && event.beadType === 'earth' && event.position === 1) {
|
||||
console.log('Correct!');
|
||||
if (event.columnIndex === 1 && event.beadType === 'earth' && event.position === 1) {
|
||||
console.log('Correct! You clicked the tens column.');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Column Highlighting:**
|
||||
- `highlightColumns` - Array of column indices to highlight (e.g., `[0, 2]` highlights first and third columns)
|
||||
- `columnLabels` - Optional labels displayed above each column (indexed left to right)
|
||||
|
||||
## 3D Enhancement
|
||||
|
||||
Make the abacus feel tangible and satisfying with three progressive levels of 3D effects.
|
||||
|
||||
### Subtle Mode
|
||||
|
||||
Light depth shadows and perspective for subtle dimensionality.
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
value={12345}
|
||||
columns={5}
|
||||
enhanced3d="subtle"
|
||||
interactive
|
||||
animated
|
||||
/>
|
||||
```
|
||||
|
||||
### Realistic Mode
|
||||
|
||||
Material-based rendering with lighting effects and textures.
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
value={7890}
|
||||
columns={4}
|
||||
enhanced3d="realistic"
|
||||
material3d={{
|
||||
heavenBeads: 'glossy', // 'glossy' | 'satin' | 'matte'
|
||||
earthBeads: 'satin',
|
||||
lighting: 'top-down', // 'top-down' | 'ambient' | 'dramatic'
|
||||
woodGrain: true // Add wood texture to frame
|
||||
}}
|
||||
interactive
|
||||
animated
|
||||
/>
|
||||
```
|
||||
|
||||
**Materials:**
|
||||
- `glossy` - High shine with strong highlights
|
||||
- `satin` - Balanced shine (default)
|
||||
- `matte` - Subtle shading, no shine
|
||||
|
||||
**Lighting:**
|
||||
- `top-down` - Balanced directional light from above
|
||||
- `ambient` - Soft light from all directions
|
||||
- `dramatic` - Strong directional light for high contrast
|
||||
|
||||
### Delightful Mode
|
||||
|
||||
Maximum satisfaction with enhanced physics and interactive effects.
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
value={8642}
|
||||
columns={4}
|
||||
enhanced3d="delightful"
|
||||
material3d={{
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'satin',
|
||||
lighting: 'dramatic',
|
||||
woodGrain: true
|
||||
}}
|
||||
physics3d={{
|
||||
hoverParallax: true // Beads lift on hover with Z-depth
|
||||
}}
|
||||
interactive
|
||||
animated
|
||||
soundEnabled
|
||||
/>
|
||||
```
|
||||
|
||||
**Physics Options:**
|
||||
- `hoverParallax` - Beads near mouse cursor lift up with depth perception
|
||||
|
||||
All 3D modes work with existing configurations and preserve exact geometry.
|
||||
|
||||
## Core API
|
||||
|
||||
@@ -132,10 +327,18 @@ interface AbacusConfig {
|
||||
colorPalette?: 'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature';
|
||||
hideInactiveBeads?: boolean; // Hide/show inactive beads
|
||||
|
||||
// Layout & Frame
|
||||
frameVisible?: boolean; // Show/hide column posts and reckoning bar
|
||||
compact?: boolean; // Compact layout (implies frameVisible=false)
|
||||
|
||||
// Interaction
|
||||
interactive?: boolean; // Enable user interactions
|
||||
animated?: boolean; // Enable animations
|
||||
gestures?: boolean; // Enable drag gestures
|
||||
|
||||
// Tutorial Features
|
||||
highlightColumns?: number[]; // Highlight specific columns by index
|
||||
columnLabels?: string[]; // Optional labels for columns
|
||||
}
|
||||
```
|
||||
|
||||
@@ -282,6 +485,60 @@ function AdvancedExample() {
|
||||
|
||||
## Hooks
|
||||
|
||||
### useAbacusDiff
|
||||
|
||||
Calculate bead differences between values for tutorials and animations:
|
||||
|
||||
```tsx
|
||||
import { useAbacusDiff } from '@soroban/abacus-react';
|
||||
|
||||
function Tutorial() {
|
||||
const [currentValue, setCurrentValue] = useState(5);
|
||||
const targetValue = 15;
|
||||
|
||||
// Get diff information: which beads need to move
|
||||
const diff = useAbacusDiff(currentValue, targetValue);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{diff.summary}</p> {/* "add heaven bead in tens column, then..." */}
|
||||
<AbacusReact
|
||||
value={currentValue}
|
||||
stepBeadHighlights={diff.highlights} // Highlight beads that need to change
|
||||
interactive
|
||||
onValueChange={setCurrentValue}
|
||||
/>
|
||||
<p>Changes needed: {diff.changes.length}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `changes` - Array of bead movements with direction and order
|
||||
- `highlights` - Bead highlight data for stepBeadHighlights prop
|
||||
- `hasChanges` - Boolean indicating if any changes needed
|
||||
- `summary` - Human-readable description of changes (e.g., "add heaven bead in ones column")
|
||||
|
||||
### useAbacusState
|
||||
|
||||
Convert numbers to abacus bead states:
|
||||
|
||||
```tsx
|
||||
import { useAbacusState } from '@soroban/abacus-react';
|
||||
|
||||
function BeadAnalyzer() {
|
||||
const value = 123;
|
||||
const state = useAbacusState(value);
|
||||
|
||||
// Check bead positions
|
||||
const onesHasHeaven = state[0].heavenActive; // false (3 < 5)
|
||||
const tensEarthCount = state[1].earthActive; // 2 (20 = 2 tens)
|
||||
|
||||
return <div>Ones column heaven bead: {onesHasHeaven ? 'active' : 'inactive'}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### useAbacusDimensions
|
||||
|
||||
Get exact sizing information for layout planning:
|
||||
@@ -300,6 +557,92 @@ function MyComponent() {
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Functions
|
||||
|
||||
Low-level functions for working with abacus states and calculations:
|
||||
|
||||
### numberToAbacusState
|
||||
|
||||
Convert a number to bead positions:
|
||||
|
||||
```tsx
|
||||
import { numberToAbacusState } from '@soroban/abacus-react';
|
||||
|
||||
const state = numberToAbacusState(123, 5); // 5 columns
|
||||
// Returns: {
|
||||
// 0: { heavenActive: false, earthActive: 3 }, // ones = 3
|
||||
// 1: { heavenActive: false, earthActive: 2 }, // tens = 2
|
||||
// 2: { heavenActive: true, earthActive: 0 }, // hundreds = 1
|
||||
// ...
|
||||
// }
|
||||
```
|
||||
|
||||
### abacusStateToNumber
|
||||
|
||||
Convert bead positions back to a number:
|
||||
|
||||
```tsx
|
||||
import { abacusStateToNumber } from '@soroban/abacus-react';
|
||||
|
||||
const state = {
|
||||
0: { heavenActive: false, earthActive: 3 },
|
||||
1: { heavenActive: false, earthActive: 2 },
|
||||
2: { heavenActive: true, earthActive: 0 }
|
||||
};
|
||||
|
||||
const value = abacusStateToNumber(state); // 123
|
||||
```
|
||||
|
||||
### calculateBeadDiff
|
||||
|
||||
Calculate the exact bead movements needed between two states:
|
||||
|
||||
```tsx
|
||||
import { calculateBeadDiff, numberToAbacusState } from '@soroban/abacus-react';
|
||||
|
||||
const fromState = numberToAbacusState(5);
|
||||
const toState = numberToAbacusState(15);
|
||||
const diff = calculateBeadDiff(fromState, toState);
|
||||
|
||||
console.log(diff.summary); // "add heaven bead in tens column"
|
||||
console.log(diff.changes); // Detailed array of movements with order
|
||||
```
|
||||
|
||||
### calculateBeadDiffFromValues
|
||||
|
||||
Convenience wrapper for calculating diff from numbers:
|
||||
|
||||
```tsx
|
||||
import { calculateBeadDiffFromValues } from '@soroban/abacus-react';
|
||||
|
||||
const diff = calculateBeadDiffFromValues(42, 57);
|
||||
// Equivalent to: calculateBeadDiff(numberToAbacusState(42), numberToAbacusState(57))
|
||||
```
|
||||
|
||||
### validateAbacusValue
|
||||
|
||||
Check if a value is within the supported range:
|
||||
|
||||
```tsx
|
||||
import { validateAbacusValue } from '@soroban/abacus-react';
|
||||
|
||||
const result = validateAbacusValue(123456, 5); // 5 columns max
|
||||
console.log(result.isValid); // false
|
||||
console.log(result.error); // "Value exceeds maximum for 5 columns (max: 99999)"
|
||||
```
|
||||
|
||||
### areStatesEqual
|
||||
|
||||
Compare two abacus states:
|
||||
|
||||
```tsx
|
||||
import { areStatesEqual, numberToAbacusState } from '@soroban/abacus-react';
|
||||
|
||||
const state1 = numberToAbacusState(123);
|
||||
const state2 = numberToAbacusState(123);
|
||||
const isEqual = areStatesEqual(state1, state2); // true
|
||||
```
|
||||
|
||||
## Educational Use Cases
|
||||
|
||||
### Interactive Math Lessons
|
||||
@@ -362,14 +705,37 @@ Full TypeScript definitions included:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
// Components
|
||||
AbacusReact,
|
||||
|
||||
// Hooks
|
||||
useAbacusDiff,
|
||||
useAbacusState,
|
||||
useAbacusDimensions,
|
||||
|
||||
// Utility Functions
|
||||
numberToAbacusState,
|
||||
abacusStateToNumber,
|
||||
calculateBeadDiff,
|
||||
calculateBeadDiffFromValues,
|
||||
validateAbacusValue,
|
||||
areStatesEqual,
|
||||
|
||||
// Theme Presets
|
||||
ABACUS_THEMES,
|
||||
|
||||
// Types
|
||||
AbacusConfig,
|
||||
BeadConfig,
|
||||
BeadClickEvent,
|
||||
AbacusCustomStyles,
|
||||
AbacusOverlay,
|
||||
AbacusCallbacks,
|
||||
useAbacusDimensions
|
||||
AbacusState,
|
||||
BeadState,
|
||||
BeadDiffResult,
|
||||
BeadDiffOutput,
|
||||
AbacusThemeName
|
||||
} from '@soroban/abacus-react';
|
||||
|
||||
// All interfaces fully typed for excellent developer experience
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
/* Wood grain texture overlay */
|
||||
.abacus-3d-container.enhanced-realistic .frame-wood {
|
||||
opacity: 0.15;
|
||||
opacity: 0.4;
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -293,11 +293,6 @@
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Wobble physics - applied via inline styles from React Spring */
|
||||
.bead-wobble {
|
||||
/* transform-origin set dynamically */
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
/* Frame depth enhancement */
|
||||
.abacus-3d-container.enhanced-delightful rect[class*="column-post"],
|
||||
@@ -307,25 +302,11 @@
|
||||
drop-shadow(0 0 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
/* Wood grain texture - enhanced */
|
||||
.frame-wood-enhanced {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(139, 90, 43, 0.03) 2px,
|
||||
rgba(139, 90, 43, 0.03) 4px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
rgba(101, 67, 33, 0.02) 1px,
|
||||
rgba(101, 67, 33, 0.02) 2px
|
||||
);
|
||||
opacity: 0.2;
|
||||
/* Wood grain texture - enhanced for delightful mode */
|
||||
.abacus-3d-container.enhanced-delightful .frame-wood {
|
||||
opacity: 0.45;
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Accessibility - Reduced motion */
|
||||
|
||||
@@ -124,7 +124,7 @@ export function getLightingFilter(lighting: LightingStyle = "top-down"): string
|
||||
* Calculate Z-depth for a bead based on enhancement level and state
|
||||
*/
|
||||
export function getBeadZDepth(
|
||||
enhanced3d: boolean | "subtle" | "realistic" | "delightful",
|
||||
enhanced3d: boolean | "subtle" | "realistic",
|
||||
active: boolean
|
||||
): number {
|
||||
if (!enhanced3d || enhanced3d === true) return 0;
|
||||
@@ -136,77 +136,28 @@ export function getBeadZDepth(
|
||||
return 6;
|
||||
case "realistic":
|
||||
return 10;
|
||||
case "delightful":
|
||||
return 12;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate wobble rotation based on velocity (for delightful mode)
|
||||
*/
|
||||
export function getWobbleRotation(velocity: number, axis: "x" | "y" = "x"): string {
|
||||
const maxRotation = 3; // degrees
|
||||
const rotation = Math.max(-maxRotation, Math.min(maxRotation, velocity * -2));
|
||||
|
||||
if (axis === "x") {
|
||||
return `rotateX(${rotation}deg)`;
|
||||
}
|
||||
return `rotateY(${rotation}deg)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate parallax offset based on mouse position
|
||||
*/
|
||||
export function calculateParallaxOffset(
|
||||
beadX: number,
|
||||
beadY: number,
|
||||
mouseX: number,
|
||||
mouseY: number,
|
||||
containerX: number,
|
||||
containerY: number,
|
||||
intensity: number = 0.5
|
||||
): { x: number; y: number; z: number } {
|
||||
// Calculate distance from bead center to mouse
|
||||
const dx = (mouseX - containerX) - beadX;
|
||||
const dy = (mouseY - containerY) - beadY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Max influence radius (pixels)
|
||||
const maxRadius = 150;
|
||||
|
||||
if (distance > maxRadius) {
|
||||
return { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
|
||||
// Calculate lift amount (inverse square falloff)
|
||||
const influence = Math.max(0, 1 - (distance / maxRadius));
|
||||
const lift = influence * influence * intensity;
|
||||
|
||||
return {
|
||||
x: dx * lift * 0.1,
|
||||
y: dy * lift * 0.1,
|
||||
z: lift * 8
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate wood grain texture SVG pattern
|
||||
*/
|
||||
export function getWoodGrainPattern(id: string): string {
|
||||
return `
|
||||
<pattern id="${id}" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse">
|
||||
<rect width="100" height="100" fill="#8B5A2B" opacity="0.3"/>
|
||||
<!-- Grain lines -->
|
||||
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
|
||||
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
|
||||
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
|
||||
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.3"/>
|
||||
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="0.5" fill="none" opacity="0.4"/>
|
||||
<!-- Knots -->
|
||||
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.2"/>
|
||||
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.2"/>
|
||||
<rect width="100" height="100" fill="#8B5A2B" opacity="0.5"/>
|
||||
<!-- Grain lines - more visible -->
|
||||
<path d="M 0 10 Q 25 8 50 10 T 100 10" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
|
||||
<path d="M 0 30 Q 25 28 50 30 T 100 30" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
|
||||
<path d="M 0 50 Q 25 48 50 50 T 100 50" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
|
||||
<path d="M 0 70 Q 25 68 50 70 T 100 70" stroke="#654321" stroke-width="1" fill="none" opacity="0.5"/>
|
||||
<path d="M 0 90 Q 25 88 50 90 T 100 90" stroke="#654321" stroke-width="1" fill="none" opacity="0.6"/>
|
||||
<!-- Knots - more prominent -->
|
||||
<ellipse cx="20" cy="25" rx="8" ry="6" fill="#654321" opacity="0.35"/>
|
||||
<ellipse cx="75" cy="65" rx="6" ry="8" fill="#654321" opacity="0.35"/>
|
||||
<ellipse cx="45" cy="82" rx="5" ry="7" fill="#654321" opacity="0.3"/>
|
||||
</pattern>
|
||||
`;
|
||||
}
|
||||
@@ -215,9 +166,8 @@ export function getWoodGrainPattern(id: string): string {
|
||||
* Get container class names for 3D enhancement level
|
||||
*/
|
||||
export function get3DContainerClasses(
|
||||
enhanced3d: boolean | "subtle" | "realistic" | "delightful" | undefined,
|
||||
lighting?: LightingStyle,
|
||||
parallaxEnabled?: boolean
|
||||
enhanced3d: boolean | "subtle" | "realistic" | undefined,
|
||||
lighting?: LightingStyle
|
||||
): string {
|
||||
const classes: string[] = ["abacus-3d-container"];
|
||||
|
||||
@@ -228,8 +178,6 @@ export function get3DContainerClasses(
|
||||
classes.push("enhanced-subtle");
|
||||
} else if (enhanced3d === "realistic") {
|
||||
classes.push("enhanced-realistic");
|
||||
} else if (enhanced3d === "delightful") {
|
||||
classes.push("enhanced-delightful");
|
||||
}
|
||||
|
||||
// Add lighting class
|
||||
@@ -237,11 +185,6 @@ export function get3DContainerClasses(
|
||||
classes.push(`lighting-${lighting}`);
|
||||
}
|
||||
|
||||
// Add parallax class
|
||||
if (parallaxEnabled && enhanced3d === "delightful") {
|
||||
classes.push("parallax-enabled");
|
||||
}
|
||||
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
@@ -260,7 +203,7 @@ export function getBeadGradientId(
|
||||
/**
|
||||
* Physics config for different enhancement levels
|
||||
*/
|
||||
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" | "delightful") {
|
||||
export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic") {
|
||||
const base = {
|
||||
tension: 300,
|
||||
friction: 22,
|
||||
@@ -272,20 +215,11 @@ export function getPhysicsConfig(enhanced3d: boolean | "subtle" | "realistic" |
|
||||
return { ...base, clamp: true };
|
||||
}
|
||||
|
||||
if (enhanced3d === "realistic") {
|
||||
return {
|
||||
tension: 320,
|
||||
friction: 24,
|
||||
mass: 0.6,
|
||||
clamp: false
|
||||
};
|
||||
}
|
||||
|
||||
// delightful
|
||||
// realistic
|
||||
return {
|
||||
tension: 280,
|
||||
friction: 20,
|
||||
mass: 0.7,
|
||||
clamp: false, // Allow overshoot for satisfying settle
|
||||
tension: 320,
|
||||
friction: 24,
|
||||
mass: 0.6,
|
||||
clamp: false
|
||||
};
|
||||
}
|
||||
|
||||
71
packages/abacus-react/src/AbacusHooks.ts
Normal file
71
packages/abacus-react/src/AbacusHooks.ts
Normal 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])
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AbacusReact } from './AbacusReact';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import './Abacus3D.css';
|
||||
import React from 'react';
|
||||
|
||||
const meta: Meta<typeof AbacusReact> = {
|
||||
title: 'Soroban/3D Effects Showcase',
|
||||
@@ -13,29 +12,20 @@ const meta: Meta<typeof AbacusReact> = {
|
||||
component: `
|
||||
# 3D Enhancement Showcase
|
||||
|
||||
Three levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
|
||||
Two levels of progressive 3D enhancement for the abacus to make interactions feel satisfying and real.
|
||||
|
||||
## Proposal 1: Subtle (CSS Perspective + Shadows)
|
||||
## Subtle (CSS Perspective + Shadows)
|
||||
- Light perspective tilt
|
||||
- Depth shadows on active beads
|
||||
- Smooth transitions
|
||||
- **Zero performance cost**
|
||||
|
||||
## Proposal 2: Realistic (Lighting + Materials)
|
||||
- Everything from Proposal 1 +
|
||||
- Realistic lighting effects
|
||||
- Material-based bead rendering (glossy/satin/matte)
|
||||
- Ambient occlusion
|
||||
- Frame depth
|
||||
|
||||
## Proposal 3: Delightful (Physics + Micro-interactions)
|
||||
- Everything from Proposal 2 +
|
||||
- Enhanced physics with satisfying bounce
|
||||
- Clack ripple effects when beads snap
|
||||
- Hover parallax
|
||||
- Maximum satisfaction
|
||||
|
||||
**Note:** Currently these are CSS-only demos. Full integration with React Spring physics coming next!
|
||||
## Realistic (Lighting + Materials)
|
||||
- Everything from Subtle +
|
||||
- Realistic lighting effects with material gradients
|
||||
- Glossy/Satin/Matte bead materials
|
||||
- Wood grain textures on frame
|
||||
- Enhanced physics for realistic motion
|
||||
`
|
||||
}
|
||||
}
|
||||
@@ -46,596 +36,387 @@ Three levels of progressive 3D enhancement for the abacus to make interactions f
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Wrapper component to apply 3D CSS classes
|
||||
const Wrapper3D: React.FC<{
|
||||
children: React.ReactNode;
|
||||
level: 'subtle' | 'realistic' | 'delightful';
|
||||
lighting?: 'top-down' | 'ambient' | 'dramatic';
|
||||
}> = ({ children, level, lighting }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const svg = containerRef.current.querySelector('.abacus-svg');
|
||||
const beads = containerRef.current.querySelectorAll('.abacus-bead');
|
||||
|
||||
// Add classes to container
|
||||
containerRef.current.classList.add('abacus-3d-container');
|
||||
containerRef.current.classList.add(`enhanced-${level}`);
|
||||
if (lighting) {
|
||||
containerRef.current.classList.add(`lighting-${lighting}`);
|
||||
}
|
||||
|
||||
// Apply will-change for performance
|
||||
if (level === 'delightful') {
|
||||
beads.forEach(bead => {
|
||||
(bead as HTMLElement).style.willChange = 'transform, filter';
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [level, lighting]);
|
||||
|
||||
return <div ref={containerRef}>{children}</div>;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PROPOSAL 1: SUBTLE
|
||||
// SIDE-BY-SIDE COMPARISON
|
||||
// ============================================
|
||||
|
||||
export const Subtle_Static: Story = {
|
||||
name: '1. Subtle - Static Display',
|
||||
export const CompareAllLevels: Story = {
|
||||
name: '🎯 Compare All Levels',
|
||||
render: () => (
|
||||
<Wrapper3D level="subtle">
|
||||
<AbacusReact
|
||||
value={12345}
|
||||
columns={5}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Subtle 3D with light perspective tilt and depth shadows. Notice the slight elevation of active beads.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Subtle_Interactive: Story = {
|
||||
name: '1. Subtle - Interactive',
|
||||
render: () => (
|
||||
<Wrapper3D level="subtle">
|
||||
<AbacusReact
|
||||
value={678}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
soundEnabled
|
||||
colorScheme="heaven-earth"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Subtle 3D + interaction. Click beads to see depth shadows change. Notice how the perspective gives a sense of physicality.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Subtle_Tutorial: Story = {
|
||||
name: '1. Subtle - Tutorial Mode',
|
||||
render: () => {
|
||||
const [step, setStep] = React.useState(0);
|
||||
const highlights = [
|
||||
{ placeValue: 0, beadType: 'earth' as const, position: 2 },
|
||||
{ placeValue: 1, beadType: 'heaven' as const },
|
||||
{ placeValue: 2, beadType: 'earth' as const, position: 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="subtle">
|
||||
<AbacusReact
|
||||
value={123}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
highlightBeads={[highlights[step]]}
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<div style={{ marginTop: '20px' }}>
|
||||
<button onClick={() => setStep((step + 1) % 3)} style={{ padding: '8px 16px' }}>
|
||||
Next Step ({step + 1}/3)
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>No Enhancement</h3>
|
||||
<AbacusReact
|
||||
value={4242}
|
||||
columns={4}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Tutorial mode with subtle 3D effects. The depth helps highlight which bead to focus on.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PROPOSAL 2: REALISTIC
|
||||
// ============================================
|
||||
|
||||
export const Realistic_TopDown: Story = {
|
||||
name: '2. Realistic - Top-Down Lighting',
|
||||
render: () => (
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={24680}
|
||||
columns={5}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
beadShape="circle"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Realistic 3D with top-down lighting. Notice the enhanced shadows and sense of illumination from above.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_Ambient: Story = {
|
||||
name: '2. Realistic - Ambient Lighting',
|
||||
render: () => (
|
||||
<Wrapper3D level="realistic" lighting="ambient">
|
||||
<AbacusReact
|
||||
value={13579}
|
||||
columns={5}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
beadShape="diamond"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Realistic 3D with ambient lighting. Softer, more even illumination creates a cozy feel.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_Dramatic: Story = {
|
||||
name: '2. Realistic - Dramatic Lighting',
|
||||
render: () => (
|
||||
<Wrapper3D level="realistic" lighting="dramatic">
|
||||
<AbacusReact
|
||||
value={99999}
|
||||
columns={5}
|
||||
showNumbers
|
||||
colorScheme="heaven-earth"
|
||||
beadShape="square"
|
||||
colorPalette="colorblind"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Realistic 3D with dramatic lighting. Strong directional light creates bold shadows and depth.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_Interactive: Story = {
|
||||
name: '2. Realistic - Interactive',
|
||||
render: () => (
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={555}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
soundEnabled
|
||||
colorScheme="place-value"
|
||||
colorPalette="nature"
|
||||
scaleFactor={1.3}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Realistic 3D + interaction. Click beads and watch the enhanced shadows and lighting respond. Feel that satisfaction!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_AllShapes: Story = {
|
||||
name: '2. Realistic - All Bead Shapes',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={777}
|
||||
columns={3}
|
||||
showNumbers
|
||||
beadShape="diamond"
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ marginTop: '12px', fontSize: '14px' }}>Diamond</p>
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Subtle</h3>
|
||||
<AbacusReact
|
||||
value={4242}
|
||||
columns={4}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
enhanced3d="subtle"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={777}
|
||||
columns={3}
|
||||
showNumbers
|
||||
beadShape="circle"
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ marginTop: '12px', fontSize: '14px' }}>Circle</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={777}
|
||||
columns={3}
|
||||
showNumbers
|
||||
beadShape="square"
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ marginTop: '12px', fontSize: '14px' }}>Square</p>
|
||||
|
||||
<div>
|
||||
<h3 style={{ marginBottom: '10px', textAlign: 'center' }}>Realistic (Satin Beads + Wood Frame)</h3>
|
||||
<AbacusReact
|
||||
value={4242}
|
||||
columns={4}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
enhanced3d="realistic"
|
||||
material3d={{
|
||||
heavenBeads: 'satin',
|
||||
earthBeads: 'satin',
|
||||
lighting: 'top-down',
|
||||
woodGrain: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Realistic 3D works beautifully with all three bead shapes.'
|
||||
story: 'Side-by-side comparison of both enhancement levels. **Click beads** to see how they move!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// PROPOSAL 3: DELIGHTFUL
|
||||
// PROPOSAL 1: SUBTLE
|
||||
// ============================================
|
||||
|
||||
export const Delightful_Static: Story = {
|
||||
name: '3. Delightful - Maximum Depth',
|
||||
render: () => (
|
||||
<Wrapper3D level="delightful">
|
||||
<AbacusReact
|
||||
value={11111}
|
||||
columns={5}
|
||||
showNumbers
|
||||
colorScheme="alternating"
|
||||
beadShape="circle"
|
||||
colorPalette="mnemonic"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Delightful 3D with maximum depth and richness. The beads really pop off the page!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Delightful_Interactive: Story = {
|
||||
name: '3. Delightful - Interactive (Physics Ready)',
|
||||
render: () => (
|
||||
<Wrapper3D level="delightful">
|
||||
<AbacusReact
|
||||
value={987}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
soundEnabled
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.3}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Delightful 3D + interaction. This is the CSS foundation - physics effects (wobble, clack ripple) will be added in the next iteration. Already feels great!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Delightful_LargeScale: Story = {
|
||||
name: '3. Delightful - Large Scale',
|
||||
render: () => (
|
||||
<Wrapper3D level="delightful">
|
||||
<AbacusReact
|
||||
value={9876543210}
|
||||
columns={10}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Delightful 3D scales beautifully even with many columns. The depth hierarchy helps organize the visual.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// COMPARISON VIEWS
|
||||
// ============================================
|
||||
|
||||
export const CompareAllLevels: Story = {
|
||||
name: 'Compare All Three Levels',
|
||||
render: () => {
|
||||
const value = 4242;
|
||||
const columns = 4;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '60px', padding: '20px' }}>
|
||||
{/* No 3D */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
|
||||
No Enhancement (Current)
|
||||
</h3>
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={columns}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Subtle */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
|
||||
Proposal 1: Subtle 😊
|
||||
</h3>
|
||||
<Wrapper3D level="subtle">
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={columns}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
|
||||
Light tilt + depth shadows
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Realistic */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
|
||||
Proposal 2: Realistic 😍
|
||||
</h3>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={columns}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
|
||||
Lighting + materials + ambient occlusion
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Delightful */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h3 style={{ fontSize: '16px', marginBottom: '20px', color: '#666' }}>
|
||||
Proposal 3: Delightful 🤩
|
||||
</h3>
|
||||
<Wrapper3D level="delightful">
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={columns}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ fontSize: '12px', color: '#888', marginTop: '12px' }}>
|
||||
Maximum depth + enhanced lighting (physics effects coming next!)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export const Subtle_Basic: Story = {
|
||||
name: '1️⃣ Subtle - Basic',
|
||||
args: {
|
||||
value: 12345,
|
||||
columns: 5,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'place-value',
|
||||
scaleFactor: 1.2,
|
||||
enhanced3d: 'subtle'
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Side-by-side comparison of all three enhancement levels. Which feels best to you?'
|
||||
story: 'Subtle 3D with light perspective tilt and depth shadows. Click beads to interact!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const CompareInteractive: Story = {
|
||||
name: 'Compare Interactive (Side-by-Side)',
|
||||
render: () => {
|
||||
const [value1, setValue1] = React.useState(123);
|
||||
const [value2, setValue2] = React.useState(456);
|
||||
const [value3, setValue3] = React.useState(789);
|
||||
// ============================================
|
||||
// PROPOSAL 2: REALISTIC (Materials)
|
||||
// ============================================
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Subtle</h4>
|
||||
<Wrapper3D level="subtle">
|
||||
<AbacusReact
|
||||
value={value1}
|
||||
onValueChange={(v) => setValue1(Number(v))}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Realistic</h4>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={value2}
|
||||
onValueChange={(v) => setValue2(Number(v))}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<h4 style={{ fontSize: '14px', marginBottom: '12px' }}>Delightful</h4>
|
||||
<Wrapper3D level="delightful">
|
||||
<AbacusReact
|
||||
value={value3}
|
||||
onValueChange={(v) => setValue3(Number(v))}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
/>
|
||||
</Wrapper3D>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export const Realistic_GlossyBeads: Story = {
|
||||
name: '2️⃣ Realistic - Glossy Beads',
|
||||
args: {
|
||||
value: 7890,
|
||||
columns: 4,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'heaven-earth',
|
||||
scaleFactor: 1.3,
|
||||
enhanced3d: 'realistic',
|
||||
material3d: {
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'glossy',
|
||||
lighting: 'top-down'
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Try all three side-by-side! Click beads and feel the difference in satisfaction.'
|
||||
story: '**Glossy material** with high shine and strong highlights. Notice the radial gradients on the beads!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_SatinBeads: Story = {
|
||||
name: '2️⃣ Realistic - Satin Beads',
|
||||
args: {
|
||||
value: 7890,
|
||||
columns: 4,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'heaven-earth',
|
||||
scaleFactor: 1.3,
|
||||
enhanced3d: 'realistic',
|
||||
material3d: {
|
||||
heavenBeads: 'satin',
|
||||
earthBeads: 'satin',
|
||||
lighting: 'top-down'
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '**Satin material** (default) with balanced shine. Medium highlights, smooth appearance.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_MatteBeads: Story = {
|
||||
name: '2️⃣ Realistic - Matte Beads',
|
||||
args: {
|
||||
value: 7890,
|
||||
columns: 4,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'heaven-earth',
|
||||
scaleFactor: 1.3,
|
||||
enhanced3d: 'realistic',
|
||||
material3d: {
|
||||
heavenBeads: 'matte',
|
||||
earthBeads: 'matte',
|
||||
lighting: 'ambient'
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '**Matte material** with subtle shading, no shine. Flat, understated appearance.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_MixedMaterials: Story = {
|
||||
name: '2️⃣ Realistic - Mixed Materials',
|
||||
args: {
|
||||
value: 5678,
|
||||
columns: 4,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'heaven-earth',
|
||||
scaleFactor: 1.3,
|
||||
enhanced3d: 'realistic',
|
||||
material3d: {
|
||||
heavenBeads: 'glossy', // Heaven beads are shiny
|
||||
earthBeads: 'matte', // Earth beads are flat
|
||||
lighting: 'dramatic'
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '**Mixed materials**: Glossy heaven beads (5-value) + Matte earth beads (1-value). Different visual weight!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_WoodGrain: Story = {
|
||||
name: '2️⃣ Realistic - Wood Grain Frame',
|
||||
args: {
|
||||
value: 3456,
|
||||
columns: 4,
|
||||
showNumbers: true,
|
||||
interactive: true,
|
||||
animated: true,
|
||||
colorScheme: 'monochrome',
|
||||
scaleFactor: 1.3,
|
||||
enhanced3d: 'realistic',
|
||||
material3d: {
|
||||
heavenBeads: 'satin',
|
||||
earthBeads: 'satin',
|
||||
lighting: 'top-down',
|
||||
woodGrain: true // Enable wood texture on frame
|
||||
}
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '**Wood grain texture** overlaid on the frame (rods and reckoning bar). Traditional soroban aesthetic!'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Realistic_LightingComparison: Story = {
|
||||
name: '2️⃣ Realistic - Lighting Comparison',
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Top-Down Lighting</h4>
|
||||
<AbacusReact
|
||||
value={999}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
enhanced3d="realistic"
|
||||
material3d={{
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'glossy',
|
||||
lighting: 'top-down'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Ambient Lighting</h4>
|
||||
<AbacusReact
|
||||
value={999}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
enhanced3d="realistic"
|
||||
material3d={{
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'glossy',
|
||||
lighting: 'ambient'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ marginBottom: '10px', textAlign: 'center' }}>Dramatic Lighting</h4>
|
||||
<AbacusReact
|
||||
value={999}
|
||||
columns={3}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
colorScheme="place-value"
|
||||
scaleFactor={1.2}
|
||||
enhanced3d="realistic"
|
||||
material3d={{
|
||||
heavenBeads: 'glossy',
|
||||
earthBeads: 'glossy',
|
||||
lighting: 'dramatic'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Compare different **lighting styles**: top-down (balanced), ambient (soft all around), dramatic (strong directional).'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// FEATURE TESTS
|
||||
// INTERACTIVE PLAYGROUND
|
||||
// ============================================
|
||||
|
||||
export const ColorSchemes_With3D: Story = {
|
||||
name: '3D Works With All Color Schemes',
|
||||
export const Playground: Story = {
|
||||
name: '🎮 Interactive Playground',
|
||||
render: () => {
|
||||
const value = 333;
|
||||
const schemes: Array<'monochrome' | 'place-value' | 'alternating' | 'heaven-earth'> = [
|
||||
'monochrome',
|
||||
'place-value',
|
||||
'alternating',
|
||||
'heaven-earth'
|
||||
];
|
||||
const [level, setLevel] = React.useState<'subtle' | 'realistic'>('realistic');
|
||||
const [material, setMaterial] = React.useState<'glossy' | 'satin' | 'matte'>('glossy');
|
||||
const [lighting, setLighting] = React.useState<'top-down' | 'ambient' | 'dramatic'>('dramatic');
|
||||
const [woodGrain, setWoodGrain] = React.useState(true);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
{schemes.map(scheme => (
|
||||
<div key={scheme} style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={3}
|
||||
showNumbers
|
||||
colorScheme={scheme}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
|
||||
{scheme}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '30px', alignItems: 'center' }}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '20px',
|
||||
padding: '20px',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: '8px',
|
||||
maxWidth: '500px'
|
||||
}}>
|
||||
<div>
|
||||
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Enhancement Level</label>
|
||||
<select value={level} onChange={e => setLevel(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
|
||||
<option value="subtle">Subtle</option>
|
||||
<option value="realistic">Realistic</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'The 3D effects work seamlessly with all existing color schemes.'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ColorPalettes_With3D: Story = {
|
||||
name: '3D Works With All Palettes',
|
||||
render: () => {
|
||||
const value = 555;
|
||||
const palettes: Array<'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature'> = [
|
||||
'default',
|
||||
'colorblind',
|
||||
'mnemonic',
|
||||
'grayscale',
|
||||
'nature'
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
{palettes.map(palette => (
|
||||
<div key={palette} style={{ textAlign: 'center' }}>
|
||||
<Wrapper3D level="realistic" lighting="top-down">
|
||||
<AbacusReact
|
||||
value={value}
|
||||
columns={3}
|
||||
showNumbers
|
||||
colorScheme="place-value"
|
||||
colorPalette={palette}
|
||||
/>
|
||||
</Wrapper3D>
|
||||
<p style={{ fontSize: '12px', marginTop: '8px', textTransform: 'capitalize' }}>
|
||||
{palette}
|
||||
</p>
|
||||
<div>
|
||||
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Bead Material</label>
|
||||
<select value={material} onChange={e => setMaterial(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
|
||||
<option value="glossy">Glossy</option>
|
||||
<option value="satin">Satin</option>
|
||||
<option value="matte">Matte</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<label style={{ fontWeight: 'bold', display: 'block', marginBottom: '5px' }}>Lighting</label>
|
||||
<select value={lighting} onChange={e => setLighting(e.target.value as any)} style={{ width: '100%', padding: '5px' }}>
|
||||
<option value="top-down">Top-Down</option>
|
||||
<option value="ambient">Ambient</option>
|
||||
<option value="dramatic">Dramatic</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<input type="checkbox" checked={woodGrain} onChange={e => setWoodGrain(e.target.checked)} />
|
||||
<span>Wood Grain</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AbacusReact
|
||||
value={6789}
|
||||
columns={4}
|
||||
showNumbers
|
||||
interactive
|
||||
animated
|
||||
soundEnabled
|
||||
colorScheme="rainbow"
|
||||
scaleFactor={1.4}
|
||||
enhanced3d={level}
|
||||
material3d={{
|
||||
heavenBeads: material,
|
||||
earthBeads: material,
|
||||
lighting: lighting,
|
||||
woodGrain: woodGrain
|
||||
}}
|
||||
/>
|
||||
|
||||
<p style={{ maxWidth: '500px', textAlign: 'center', color: '#666' }}>
|
||||
Click beads to interact! Try different combinations above to find your favorite look and feel.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'The 3D effects enhance all color palettes beautifully.'
|
||||
story: 'Experiment with all the 3D options! Mix and match materials, lighting, and physics to find your perfect configuration.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -253,14 +253,6 @@ export interface Abacus3DMaterial {
|
||||
woodGrain?: boolean; // Add wood texture to frame
|
||||
}
|
||||
|
||||
export interface Abacus3DPhysics {
|
||||
wobble?: boolean; // Beads rotate slightly during movement
|
||||
clackEffect?: boolean; // Visual ripple when beads snap
|
||||
hoverParallax?: boolean; // Beads lift on hover
|
||||
particleSnap?: "off" | "subtle" | "sparkle"; // Particle effects on snap
|
||||
hapticFeedback?: boolean; // Trigger haptic feedback on mobile
|
||||
}
|
||||
|
||||
export interface AbacusConfig {
|
||||
// Basic configuration
|
||||
value?: number | bigint;
|
||||
@@ -278,10 +270,13 @@ export interface AbacusConfig {
|
||||
soundEnabled?: boolean;
|
||||
soundVolume?: number;
|
||||
|
||||
// Layout & Frame
|
||||
frameVisible?: boolean; // Show/hide column posts and reckoning bar
|
||||
compact?: boolean; // Compact layout for inline display (implies frameVisible=false, optimized spacing)
|
||||
|
||||
// 3D Enhancement
|
||||
enhanced3d?: boolean | "subtle" | "realistic" | "delightful";
|
||||
enhanced3d?: boolean | "subtle" | "realistic";
|
||||
material3d?: Abacus3DMaterial;
|
||||
physics3d?: Abacus3DPhysics;
|
||||
|
||||
// Advanced customization
|
||||
customStyles?: AbacusCustomStyles;
|
||||
@@ -290,6 +285,7 @@ export interface AbacusConfig {
|
||||
|
||||
// Tutorial and accessibility features
|
||||
highlightColumns?: number[]; // Highlight specific columns (legacy - array indices)
|
||||
columnLabels?: string[]; // Optional labels for columns (indexed by column index, left to right)
|
||||
highlightBeads?: BeadHighlight[]; // Support both place-value and column-index based highlighting
|
||||
stepBeadHighlights?: StepBeadHighlight[]; // Progressive step-based highlighting with directions
|
||||
currentStep?: number; // Current step index for progressive highlighting
|
||||
@@ -1247,6 +1243,10 @@ interface BeadProps {
|
||||
colorScheme?: string;
|
||||
colorPalette?: string;
|
||||
totalColumns?: number;
|
||||
// 3D Enhancement
|
||||
enhanced3d?: boolean | "subtle" | "realistic";
|
||||
material3d?: Abacus3DMaterial;
|
||||
columnIndex?: number;
|
||||
}
|
||||
|
||||
const Bead: React.FC<BeadProps> = ({
|
||||
@@ -1275,16 +1275,25 @@ const Bead: React.FC<BeadProps> = ({
|
||||
colorScheme = "monochrome",
|
||||
colorPalette = "default",
|
||||
totalColumns = 1,
|
||||
enhanced3d,
|
||||
material3d,
|
||||
columnIndex,
|
||||
}) => {
|
||||
// Detect server-side rendering
|
||||
const isServer = typeof window === 'undefined';
|
||||
|
||||
// Use springs only if not on server and animations are enabled
|
||||
// Even on server, we must call hooks unconditionally, so we provide static values
|
||||
// Enhanced physics config for 3D modes
|
||||
const physicsConfig = React.useMemo(() => {
|
||||
if (!enableAnimation || isServer) return { duration: 0 };
|
||||
if (!enhanced3d || enhanced3d === true || enhanced3d === 'subtle') return config.default;
|
||||
return Abacus3DUtils.getPhysicsConfig(enhanced3d);
|
||||
}, [enableAnimation, isServer, enhanced3d]);
|
||||
|
||||
const [{ x: springX, y: springY }, api] = useSpring(() => ({
|
||||
x,
|
||||
y,
|
||||
config: enableAnimation && !isServer ? config.default : { duration: 0 }
|
||||
config: physicsConfig
|
||||
}));
|
||||
|
||||
// Arrow pulse animation for urgency indication
|
||||
@@ -1363,11 +1372,11 @@ const Bead: React.FC<BeadProps> = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enableAnimation) {
|
||||
api.start({ x, y, config: { tension: 400, friction: 30, mass: 0.8 } });
|
||||
api.start({ x, y, config: physicsConfig });
|
||||
} else {
|
||||
api.set({ x, y });
|
||||
}
|
||||
}, [x, y, enableAnimation, api]);
|
||||
}, [x, y, enableAnimation, api, physicsConfig]);
|
||||
|
||||
// Pulse animation for direction arrows to indicate urgency
|
||||
React.useEffect(() => {
|
||||
@@ -1396,12 +1405,22 @@ const Bead: React.FC<BeadProps> = ({
|
||||
const renderShape = () => {
|
||||
const halfSize = size / 2;
|
||||
|
||||
// Determine fill - use gradient for realistic mode, otherwise use color
|
||||
let fillValue = color;
|
||||
if (enhanced3d === 'realistic' && columnIndex !== undefined) {
|
||||
if (bead.type === 'heaven') {
|
||||
fillValue = `url(#bead-gradient-${columnIndex}-heaven)`;
|
||||
} else {
|
||||
fillValue = `url(#bead-gradient-${columnIndex}-earth-${bead.position})`;
|
||||
}
|
||||
}
|
||||
|
||||
switch (shape) {
|
||||
case "diamond":
|
||||
return (
|
||||
<polygon
|
||||
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
|
||||
fill={color}
|
||||
fill={fillValue}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
@@ -1411,7 +1430,7 @@ const Bead: React.FC<BeadProps> = ({
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
fill={color}
|
||||
fill={fillValue}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
rx="1"
|
||||
@@ -1424,7 +1443,7 @@ const Bead: React.FC<BeadProps> = ({
|
||||
cx={halfSize}
|
||||
cy={halfSize}
|
||||
r={halfSize}
|
||||
fill={color}
|
||||
fill={fillValue}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
@@ -1458,8 +1477,7 @@ const Bead: React.FC<BeadProps> = ({
|
||||
? {
|
||||
transform: to(
|
||||
[springX, springY],
|
||||
(sx, sy) =>
|
||||
`translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
|
||||
(sx, sy) => `translate(${sx - getXOffset()}px, ${sy - getYOffset()}px)`,
|
||||
),
|
||||
cursor: enableGestures ? "grab" : onClick ? "pointer" : "default",
|
||||
touchAction: "none" as const,
|
||||
@@ -1568,15 +1586,18 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
showNumbers,
|
||||
soundEnabled,
|
||||
soundVolume,
|
||||
// Layout & Frame props
|
||||
frameVisible,
|
||||
compact,
|
||||
// 3D enhancement props
|
||||
enhanced3d,
|
||||
material3d,
|
||||
physics3d,
|
||||
// Advanced customization props
|
||||
customStyles,
|
||||
callbacks,
|
||||
overlays = [],
|
||||
highlightColumns = [],
|
||||
columnLabels = [],
|
||||
highlightBeads = [],
|
||||
stepBeadHighlights = [],
|
||||
currentStep = 0,
|
||||
@@ -1597,6 +1618,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
}
|
||||
|
||||
// Use props if provided, otherwise fall back to context config
|
||||
// Apply compact preset logic
|
||||
const effectiveFrameVisible = compact ? false : (frameVisible ?? true);
|
||||
|
||||
const finalConfig = {
|
||||
hideInactiveBeads: hideInactiveBeads ?? contextConfig.hideInactiveBeads,
|
||||
beadShape: beadShape ?? contextConfig.beadShape,
|
||||
@@ -1609,6 +1633,8 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
showNumbers: showNumbers ?? contextConfig.showNumbers,
|
||||
soundEnabled: soundEnabled ?? contextConfig.soundEnabled,
|
||||
soundVolume: soundVolume ?? contextConfig.soundVolume,
|
||||
frameVisible: effectiveFrameVisible,
|
||||
compact: compact ?? false,
|
||||
};
|
||||
// Calculate effective columns first, without depending on columnStates
|
||||
const effectiveColumns = useMemo(() => {
|
||||
@@ -1992,9 +2018,15 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
// console.log(`🎯 activeColumn changed to: ${activeColumn}`);
|
||||
}, [activeColumn]);
|
||||
|
||||
// 3D Enhancement: Calculate container classes
|
||||
const containerClasses = Abacus3DUtils.get3DContainerClasses(
|
||||
enhanced3d,
|
||||
material3d?.lighting
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="abacus-container"
|
||||
className={containerClasses}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
textAlign: "center",
|
||||
@@ -2053,6 +2085,68 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
opacity: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* 3D Enhancement: Material gradients for beads */}
|
||||
{enhanced3d === 'realistic' && material3d && (
|
||||
<>
|
||||
{/* Generate gradients for all beads based on material type */}
|
||||
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
const placeValue = (effectiveColumns - 1 - colIndex) as ValidPlaceValues;
|
||||
|
||||
// Create dummy beads to get their colors
|
||||
const heavenBead: BeadConfig = {
|
||||
type: 'heaven',
|
||||
value: 5,
|
||||
active: true,
|
||||
position: 0,
|
||||
placeValue
|
||||
};
|
||||
const earthBead: BeadConfig = {
|
||||
type: 'earth',
|
||||
value: 1,
|
||||
active: true,
|
||||
position: 0,
|
||||
placeValue
|
||||
};
|
||||
|
||||
const heavenColor = getBeadColor(heavenBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
|
||||
const earthColor = getBeadColor(earthBead, effectiveColumns, finalConfig.colorScheme, finalConfig.colorPalette, false);
|
||||
|
||||
return (
|
||||
<React.Fragment key={`gradients-col-${colIndex}`}>
|
||||
{/* Heaven bead gradient */}
|
||||
<defs dangerouslySetInnerHTML={{
|
||||
__html: Abacus3DUtils.getBeadGradient(
|
||||
`bead-gradient-${colIndex}-heaven`,
|
||||
heavenColor,
|
||||
material3d.heavenBeads || 'satin',
|
||||
true
|
||||
)
|
||||
}} />
|
||||
|
||||
{/* Earth bead gradients */}
|
||||
{[0, 1, 2, 3].map(pos => (
|
||||
<defs key={`earth-${pos}`} dangerouslySetInnerHTML={{
|
||||
__html: Abacus3DUtils.getBeadGradient(
|
||||
`bead-gradient-${colIndex}-earth-${pos}`,
|
||||
earthColor,
|
||||
material3d.earthBeads || 'satin',
|
||||
true
|
||||
)
|
||||
}} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
);
|
||||
}).filter(Boolean)}
|
||||
|
||||
{/* Wood grain texture pattern */}
|
||||
{material3d.woodGrain && (
|
||||
<defs dangerouslySetInnerHTML={{
|
||||
__html: Abacus3DUtils.getWoodGrainPattern('wood-grain-pattern')
|
||||
}} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</defs>
|
||||
|
||||
{/* Background glow effects - rendered behind everything */}
|
||||
@@ -2087,8 +2181,55 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Column highlights - rendered behind everything for tutorial/educational purposes */}
|
||||
{highlightColumns.map((colIndex) => {
|
||||
if (colIndex < 0 || colIndex >= effectiveColumns) return null;
|
||||
|
||||
const x = colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
const highlightWidth = dimensions.rodSpacing * 0.9; // Slightly narrower than full column
|
||||
const highlightHeight = dimensions.height;
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`column-highlight-${colIndex}`}
|
||||
x={x - highlightWidth / 2}
|
||||
y={0}
|
||||
width={highlightWidth}
|
||||
height={highlightHeight}
|
||||
fill="rgba(59, 130, 246, 0.15)" // Light blue highlight
|
||||
stroke="rgba(59, 130, 246, 0.4)" // Slightly darker blue border
|
||||
strokeWidth={2}
|
||||
rx={6}
|
||||
style={{ pointerEvents: "none" }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Column labels - rendered above columns for tutorial/educational purposes */}
|
||||
{columnLabels.map((label, colIndex) => {
|
||||
if (!label || colIndex >= effectiveColumns) return null;
|
||||
|
||||
const x = colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
const labelY = -20; // Position above the abacus
|
||||
|
||||
return (
|
||||
<text
|
||||
key={`column-label-${colIndex}`}
|
||||
x={x}
|
||||
y={labelY}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fontWeight="600"
|
||||
fill="rgba(0, 0, 0, 0.7)"
|
||||
style={{ pointerEvents: "none", userSelect: "none" }}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Rods - positioned as rectangles like in Typst */}
|
||||
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
{finalConfig.frameVisible && Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex;
|
||||
const x =
|
||||
colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
@@ -2120,33 +2261,66 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`rod-pv${placeValue}`}
|
||||
x={x - dimensions.rodWidth / 2}
|
||||
y={rodStartY}
|
||||
width={dimensions.rodWidth}
|
||||
height={rodEndY - rodStartY}
|
||||
fill={rodStyle.fill}
|
||||
stroke={rodStyle.stroke}
|
||||
strokeWidth={rodStyle.strokeWidth}
|
||||
opacity={rodStyle.opacity}
|
||||
/>
|
||||
<React.Fragment key={`rod-pv${placeValue}`}>
|
||||
<rect
|
||||
x={x - dimensions.rodWidth / 2}
|
||||
y={rodStartY}
|
||||
width={dimensions.rodWidth}
|
||||
height={rodEndY - rodStartY}
|
||||
fill={rodStyle.fill}
|
||||
stroke={rodStyle.stroke}
|
||||
strokeWidth={rodStyle.strokeWidth}
|
||||
opacity={rodStyle.opacity}
|
||||
className="column-post"
|
||||
/>
|
||||
{/* Wood grain texture overlay for column posts */}
|
||||
{enhanced3d === 'realistic' && material3d?.woodGrain && (
|
||||
<rect
|
||||
x={x - dimensions.rodWidth / 2}
|
||||
y={rodStartY}
|
||||
width={dimensions.rodWidth}
|
||||
height={rodEndY - rodStartY}
|
||||
fill="url(#wood-grain-pattern)"
|
||||
className="frame-wood"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Reckoning bar - spans from leftmost to rightmost bead */}
|
||||
<rect
|
||||
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
|
||||
y={barY}
|
||||
width={
|
||||
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
|
||||
}
|
||||
height={dimensions.barThickness}
|
||||
fill={customStyles?.reckoningBar?.fill || "black"} // Typst default is black
|
||||
stroke={customStyles?.reckoningBar?.stroke || "none"}
|
||||
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
|
||||
opacity={customStyles?.reckoningBar?.opacity ?? 1}
|
||||
/>
|
||||
{finalConfig.frameVisible && (
|
||||
<>
|
||||
<rect
|
||||
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
|
||||
y={barY}
|
||||
width={
|
||||
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
|
||||
}
|
||||
height={dimensions.barThickness}
|
||||
fill={customStyles?.reckoningBar?.fill || "black"} // Typst default is black
|
||||
stroke={customStyles?.reckoningBar?.stroke || "none"}
|
||||
strokeWidth={customStyles?.reckoningBar?.strokeWidth ?? 0}
|
||||
opacity={customStyles?.reckoningBar?.opacity ?? 1}
|
||||
className="reckoning-bar"
|
||||
/>
|
||||
{/* Wood grain texture overlay for reckoning bar */}
|
||||
{enhanced3d === 'realistic' && material3d?.woodGrain && (
|
||||
<rect
|
||||
x={dimensions.rodSpacing / 2 - dimensions.beadSize / 2}
|
||||
y={barY}
|
||||
width={
|
||||
(effectiveColumns - 1) * dimensions.rodSpacing + dimensions.beadSize
|
||||
}
|
||||
height={dimensions.barThickness}
|
||||
fill="url(#wood-grain-pattern)"
|
||||
className="frame-wood"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Beads */}
|
||||
{beadStates.map((columnBeads, colIndex) =>
|
||||
@@ -2329,6 +2503,9 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
colorScheme={finalConfig.colorScheme}
|
||||
colorPalette={finalConfig.colorPalette}
|
||||
totalColumns={effectiveColumns}
|
||||
enhanced3d={enhanced3d}
|
||||
material3d={material3d}
|
||||
columnIndex={colIndex}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
|
||||
264
packages/abacus-react/src/AbacusStatic.stories.tsx
Normal file
264
packages/abacus-react/src/AbacusStatic.stories.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { AbacusStatic } from './AbacusStatic'
|
||||
import { ABACUS_THEMES } from './AbacusThemes'
|
||||
|
||||
/**
|
||||
* AbacusStatic - Server Component compatible static abacus
|
||||
*
|
||||
* ## Key Features:
|
||||
* - ✅ Works in React Server Components (no "use client")
|
||||
* - ✅ Shares core utilities with AbacusReact (numberToAbacusState, color logic)
|
||||
* - ✅ No animations, hooks, or client-side JavaScript
|
||||
* - ✅ Lightweight rendering for static displays
|
||||
*
|
||||
* ## Shared Code (No Duplication!):
|
||||
* - Uses `numberToAbacusState()` from AbacusUtils
|
||||
* - Uses same color scheme logic as AbacusReact
|
||||
* - Uses same bead positioning concepts
|
||||
* - Accepts same `customStyles` prop structure
|
||||
*
|
||||
* ## When to Use:
|
||||
* - React Server Components (Next.js App Router)
|
||||
* - Static site generation
|
||||
* - Non-interactive previews
|
||||
* - Server-side rendering without hydration
|
||||
*/
|
||||
const meta = {
|
||||
title: 'AbacusStatic/Server Component Ready',
|
||||
component: AbacusStatic,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof AbacusStatic>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 123,
|
||||
columns: 'auto',
|
||||
},
|
||||
}
|
||||
|
||||
export const DifferentValues: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap' }}>
|
||||
{[1, 5, 10, 25, 50, 100, 456, 789].map((value) => (
|
||||
<div key={value} style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={value} columns="auto" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ColorSchemes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={456} colorScheme="place-value" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Place Value</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={456} colorScheme="monochrome" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Monochrome</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={456} colorScheme="heaven-earth" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Heaven-Earth</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={456} colorScheme="alternating" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Alternating</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const BeadShapes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={42} beadShape="circle" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Circle</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={42} beadShape="diamond" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Diamond</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={42} beadShape="square" />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Square</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const CompactMode: Story = {
|
||||
render: () => (
|
||||
<div style={{ fontSize: '24px', display: 'flex', alignItems: 'center', gap: '15px' }}>
|
||||
<span>The equation:</span>
|
||||
<AbacusStatic value={5} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
|
||||
<span>+</span>
|
||||
<AbacusStatic value={3} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
|
||||
<span>=</span>
|
||||
<AbacusStatic value={8} columns={1} compact hideInactiveBeads scaleFactor={0.7} />
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const HideInactiveBeads: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={25} hideInactiveBeads={false} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Show All</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={25} hideInactiveBeads />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Hide Inactive</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithThemes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px', flexWrap: 'wrap' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={123} customStyles={ABACUS_THEMES.light} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Light</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', padding: '20px', background: '#1e293b', borderRadius: '8px' }}>
|
||||
<AbacusStatic value={123} customStyles={ABACUS_THEMES.dark} />
|
||||
<p style={{ marginTop: '10px', color: '#cbd5e1' }}>Dark</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={123} customStyles={ABACUS_THEMES.trophy} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Trophy</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ColumnHighlightingAndLabels: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '40px' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic
|
||||
value={456}
|
||||
highlightColumns={[1]}
|
||||
columnLabels={['ones', 'tens', 'hundreds']}
|
||||
/>
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Highlighting tens place</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic
|
||||
value={789}
|
||||
highlightColumns={[0, 2]}
|
||||
columnLabels={['ones', 'tens', 'hundreds']}
|
||||
/>
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>Multiple highlights</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Scaling: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '40px', alignItems: 'flex-end' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={9} scaleFactor={0.5} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>0.5x</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={9} scaleFactor={1} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>1x</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<AbacusStatic value={9} scaleFactor={1.5} />
|
||||
<p style={{ marginTop: '10px', color: '#64748b' }}>1.5x</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ServerComponentExample: Story = {
|
||||
render: () => (
|
||||
<div style={{ maxWidth: '700px', padding: '20px', background: '#f8fafc', borderRadius: '8px' }}>
|
||||
<h3 style={{ marginTop: 0 }}>React Server Component Usage</h3>
|
||||
<pre
|
||||
style={{
|
||||
background: '#1e293b',
|
||||
color: '#e2e8f0',
|
||||
padding: '15px',
|
||||
borderRadius: '6px',
|
||||
overflow: 'auto',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
{`// app/flashcards/page.tsx (Server Component)
|
||||
import { AbacusStatic } from '@soroban/abacus-react'
|
||||
|
||||
export default function FlashcardsPage() {
|
||||
const numbers = [1, 5, 10, 25, 50, 100]
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{numbers.map(num => (
|
||||
<div key={num} className="card">
|
||||
<AbacusStatic
|
||||
value={num}
|
||||
columns="auto"
|
||||
hideInactiveBeads
|
||||
compact
|
||||
/>
|
||||
<p>{num}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ No "use client" needed!
|
||||
// ✅ Rendered on server
|
||||
// ✅ Zero client JavaScript`}
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const PreviewCards: Story = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
||||
gap: '20px',
|
||||
maxWidth: '900px',
|
||||
}}
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 25, 50].map((value) => (
|
||||
<div
|
||||
key={value}
|
||||
style={{
|
||||
padding: '15px',
|
||||
background: 'white',
|
||||
border: '2px solid #e2e8f0',
|
||||
borderRadius: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<AbacusStatic value={value} columns="auto" scaleFactor={0.8} hideInactiveBeads />
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#475569' }}>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
366
packages/abacus-react/src/AbacusStatic.tsx
Normal file
366
packages/abacus-react/src/AbacusStatic.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* AbacusStatic - Server Component compatible static abacus
|
||||
*
|
||||
* Shares core logic with AbacusReact but uses static rendering without hooks/animations.
|
||||
* Reuses: numberToAbacusState, getBeadColor logic, positioning calculations
|
||||
* Different: No hooks, no animations, no interactions, simplified rendering
|
||||
*/
|
||||
|
||||
import { numberToAbacusState } from './AbacusUtils'
|
||||
import { AbacusStaticBead } from './AbacusStaticBead'
|
||||
import type {
|
||||
AbacusCustomStyles,
|
||||
BeadConfig,
|
||||
ValidPlaceValues
|
||||
} from './AbacusReact'
|
||||
|
||||
export interface AbacusStaticConfig {
|
||||
value: number | bigint
|
||||
columns?: number | 'auto'
|
||||
beadShape?: 'circle' | 'diamond' | 'square'
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'alternating' | 'heaven-earth'
|
||||
colorPalette?: 'default' | 'pastel' | 'vibrant' | 'earth-tones'
|
||||
showNumbers?: boolean | 'always' | 'never'
|
||||
hideInactiveBeads?: boolean
|
||||
scaleFactor?: number
|
||||
frameVisible?: boolean
|
||||
compact?: boolean
|
||||
customStyles?: AbacusCustomStyles
|
||||
highlightColumns?: number[]
|
||||
columnLabels?: string[]
|
||||
}
|
||||
|
||||
// Shared color logic from AbacusReact (simplified for static use)
|
||||
function getBeadColor(
|
||||
bead: BeadConfig,
|
||||
totalColumns: number,
|
||||
colorScheme: string,
|
||||
colorPalette: string
|
||||
): string {
|
||||
const placeValue = bead.placeValue
|
||||
|
||||
// Place-value coloring
|
||||
if (colorScheme === 'place-value') {
|
||||
const colors: Record<string, string[]> = {
|
||||
default: [
|
||||
'#ef4444', // red - ones
|
||||
'#f59e0b', // amber - tens
|
||||
'#10b981', // emerald - hundreds
|
||||
'#3b82f6', // blue - thousands
|
||||
'#8b5cf6', // purple - ten thousands
|
||||
'#ec4899', // pink - hundred thousands
|
||||
'#14b8a6', // teal - millions
|
||||
'#f97316', // orange - ten millions
|
||||
'#6366f1', // indigo - hundred millions
|
||||
'#84cc16', // lime - billions
|
||||
],
|
||||
pastel: [
|
||||
'#fca5a5', '#fcd34d', '#6ee7b7', '#93c5fd', '#c4b5fd',
|
||||
'#f9a8d4', '#5eead4', '#fdba74', '#a5b4fc', '#bef264',
|
||||
],
|
||||
vibrant: [
|
||||
'#dc2626', '#d97706', '#059669', '#2563eb', '#7c3aed',
|
||||
'#db2777', '#0d9488', '#ea580c', '#4f46e5', '#65a30d',
|
||||
],
|
||||
'earth-tones': [
|
||||
'#92400e', '#78350f', '#365314', '#1e3a8a', '#4c1d95',
|
||||
'#831843', '#134e4a', '#7c2d12', '#312e81', '#3f6212',
|
||||
],
|
||||
}
|
||||
|
||||
const palette = colors[colorPalette] || colors.default
|
||||
return palette[placeValue % palette.length]
|
||||
}
|
||||
|
||||
// Heaven-earth coloring
|
||||
if (colorScheme === 'heaven-earth') {
|
||||
return bead.type === 'heaven' ? '#3b82f6' : '#10b981'
|
||||
}
|
||||
|
||||
// Alternating coloring
|
||||
if (colorScheme === 'alternating') {
|
||||
const columnIndex = totalColumns - 1 - placeValue
|
||||
return columnIndex % 2 === 0 ? '#3b82f6' : '#10b981'
|
||||
}
|
||||
|
||||
// Monochrome (default)
|
||||
return '#3b82f6'
|
||||
}
|
||||
|
||||
// Calculate bead positions (simplified from AbacusReact)
|
||||
function calculateBeadPosition(
|
||||
bead: BeadConfig,
|
||||
dimensions: { beadSize: number; rodSpacing: number; heavenY: number; earthY: number; barY: number; totalColumns: number }
|
||||
): { x: number; y: number } {
|
||||
const { beadSize, rodSpacing, heavenY, earthY, barY, totalColumns } = dimensions
|
||||
|
||||
// X position based on place value (rightmost = ones place)
|
||||
const columnIndex = totalColumns - 1 - bead.placeValue
|
||||
const x = columnIndex * rodSpacing + rodSpacing / 2
|
||||
|
||||
// Y position based on bead type and active state
|
||||
if (bead.type === 'heaven') {
|
||||
// Heaven bead: if active, near bar; if inactive, at top
|
||||
const y = bead.active ? barY - beadSize - 5 : heavenY
|
||||
return { x, y }
|
||||
} else {
|
||||
// Earth bead: if active, stack up from bar; if inactive, at bottom
|
||||
const earthSpacing = beadSize + 4
|
||||
if (bead.active) {
|
||||
// Active earth beads stack upward from the bar
|
||||
const y = barY + beadSize / 2 + 10 + bead.position * earthSpacing
|
||||
return { x, y }
|
||||
} else {
|
||||
// Inactive earth beads rest at the bottom
|
||||
const y = earthY + (bead.position - 2) * earthSpacing
|
||||
return { x, y }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AbacusStatic - Pure static abacus component (Server Component compatible)
|
||||
*/
|
||||
export function AbacusStatic({
|
||||
value,
|
||||
columns = 'auto',
|
||||
beadShape = 'circle',
|
||||
colorScheme = 'place-value',
|
||||
colorPalette = 'default',
|
||||
showNumbers = true,
|
||||
hideInactiveBeads = false,
|
||||
scaleFactor = 1,
|
||||
frameVisible = true,
|
||||
compact = false,
|
||||
customStyles,
|
||||
highlightColumns = [],
|
||||
columnLabels = [],
|
||||
}: AbacusStaticConfig) {
|
||||
// Calculate columns
|
||||
const valueStr = value.toString().replace('-', '')
|
||||
const minColumns = Math.max(1, valueStr.length)
|
||||
const effectiveColumns = columns === 'auto' ? minColumns : Math.max(columns, minColumns)
|
||||
|
||||
// Use shared utility to convert value to bead states
|
||||
const state = numberToAbacusState(value, effectiveColumns)
|
||||
|
||||
// Generate bead configs (matching AbacusReact's structure)
|
||||
const beadConfigs: BeadConfig[][] = []
|
||||
for (let colIndex = 0; colIndex < effectiveColumns; colIndex++) {
|
||||
const placeValue = (effectiveColumns - 1 - colIndex) as ValidPlaceValues
|
||||
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
|
||||
|
||||
const beads: BeadConfig[] = []
|
||||
|
||||
// Heaven bead
|
||||
beads.push({
|
||||
type: 'heaven',
|
||||
value: 5,
|
||||
active: columnState.heavenActive,
|
||||
position: 0,
|
||||
placeValue,
|
||||
})
|
||||
|
||||
// Earth beads
|
||||
for (let i = 0; i < 4; i++) {
|
||||
beads.push({
|
||||
type: 'earth',
|
||||
value: 1,
|
||||
active: i < columnState.earthActive,
|
||||
position: i,
|
||||
placeValue,
|
||||
})
|
||||
}
|
||||
|
||||
beadConfigs.push(beads)
|
||||
}
|
||||
|
||||
// Calculate dimensions (matching AbacusReact)
|
||||
const beadSize = 20
|
||||
const rodSpacing = 40
|
||||
const heavenHeight = 60
|
||||
const earthHeight = 120
|
||||
const barHeight = 10
|
||||
const padding = 20
|
||||
const numberHeightCalc = showNumbers ? 30 : 0
|
||||
const labelHeight = columnLabels.length > 0 ? 30 : 0
|
||||
|
||||
const width = effectiveColumns * rodSpacing + padding * 2
|
||||
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
|
||||
|
||||
const dimensions = {
|
||||
width,
|
||||
height,
|
||||
beadSize,
|
||||
rodSpacing,
|
||||
heavenY: padding + labelHeight + heavenHeight / 3,
|
||||
earthY: padding + labelHeight + heavenHeight + barHeight + earthHeight * 0.7,
|
||||
barY: padding + labelHeight + heavenHeight,
|
||||
padding,
|
||||
totalColumns: effectiveColumns,
|
||||
}
|
||||
|
||||
// Compact mode hides frame
|
||||
const effectiveFrameVisible = compact ? false : frameVisible
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width * scaleFactor}
|
||||
height={height * scaleFactor}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className={`abacus-svg ${hideInactiveBeads ? 'hide-inactive-mode' : ''}`}
|
||||
style={{ overflow: 'visible', display: 'block' }}
|
||||
>
|
||||
<defs>
|
||||
<style>{`
|
||||
.abacus-bead {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
.hide-inactive-mode .abacus-bead.hidden-inactive {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
</defs>
|
||||
|
||||
{/* Column highlights */}
|
||||
{highlightColumns.map((colIndex) => {
|
||||
if (colIndex < 0 || colIndex >= effectiveColumns) return null
|
||||
|
||||
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
|
||||
const highlightWidth = rodSpacing * 0.9
|
||||
const highlightHeight = height - padding * 2 - numberHeightCalc - labelHeight
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`column-highlight-${colIndex}`}
|
||||
x={x - highlightWidth / 2}
|
||||
y={padding + labelHeight}
|
||||
width={highlightWidth}
|
||||
height={highlightHeight}
|
||||
fill="rgba(59, 130, 246, 0.15)"
|
||||
stroke="rgba(59, 130, 246, 0.4)"
|
||||
strokeWidth={2}
|
||||
rx={6}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Column labels */}
|
||||
{columnLabels.map((label, colIndex) => {
|
||||
if (!label || colIndex >= effectiveColumns) return null
|
||||
|
||||
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
|
||||
|
||||
return (
|
||||
<text
|
||||
key={`column-label-${colIndex}`}
|
||||
x={x}
|
||||
y={padding + 15}
|
||||
textAnchor="middle"
|
||||
fontSize="14"
|
||||
fontWeight="600"
|
||||
fill="rgba(0, 0, 0, 0.7)"
|
||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||
>
|
||||
{label}
|
||||
</text>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Rods (column posts) */}
|
||||
{effectiveFrameVisible && beadConfigs.map((_, colIndex) => {
|
||||
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`rod-${colIndex}`}
|
||||
x={x - 3}
|
||||
y={padding + labelHeight}
|
||||
width={6}
|
||||
height={heavenHeight + earthHeight + barHeight}
|
||||
fill={customStyles?.columnPosts?.fill || 'rgb(0, 0, 0, 0.1)'}
|
||||
stroke={customStyles?.columnPosts?.stroke || 'rgba(0, 0, 0, 0.2)'}
|
||||
strokeWidth={customStyles?.columnPosts?.strokeWidth || 1}
|
||||
opacity={customStyles?.columnPosts?.opacity ?? 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Reckoning bar */}
|
||||
{effectiveFrameVisible && (
|
||||
<rect
|
||||
x={padding}
|
||||
y={dimensions.barY}
|
||||
width={effectiveColumns * rodSpacing}
|
||||
height={barHeight}
|
||||
fill={customStyles?.reckoningBar?.fill || 'rgb(0, 0, 0, 0.15)'}
|
||||
stroke={customStyles?.reckoningBar?.stroke || 'rgba(0, 0, 0, 0.3)'}
|
||||
strokeWidth={customStyles?.reckoningBar?.strokeWidth || 2}
|
||||
opacity={customStyles?.reckoningBar?.opacity ?? 1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Beads */}
|
||||
{beadConfigs.map((columnBeads, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex
|
||||
|
||||
return (
|
||||
<g key={`column-${colIndex}`}>
|
||||
{columnBeads.map((bead, beadIndex) => {
|
||||
const position = calculateBeadPosition(bead, dimensions)
|
||||
|
||||
// Adjust X for padding
|
||||
position.x += padding
|
||||
|
||||
const color = getBeadColor(bead, effectiveColumns, colorScheme, colorPalette)
|
||||
|
||||
return (
|
||||
<AbacusStaticBead
|
||||
key={`bead-${colIndex}-${beadIndex}`}
|
||||
bead={bead}
|
||||
x={position.x}
|
||||
y={position.y}
|
||||
size={beadSize}
|
||||
shape={beadShape}
|
||||
color={color}
|
||||
hideInactiveBeads={hideInactiveBeads}
|
||||
customStyle={
|
||||
bead.type === 'heaven'
|
||||
? customStyles?.heavenBeads
|
||||
: customStyles?.earthBeads
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Column numbers */}
|
||||
{showNumbers && beadConfigs.map((_, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex
|
||||
const columnState = state[placeValue] || { heavenActive: false, earthActive: 0 }
|
||||
const digit = (columnState.heavenActive ? 5 : 0) + columnState.earthActive
|
||||
const x = colIndex * rodSpacing + rodSpacing / 2 + padding
|
||||
|
||||
return (
|
||||
<text
|
||||
key={`number-${colIndex}`}
|
||||
x={x}
|
||||
y={height - padding + 5}
|
||||
textAnchor="middle"
|
||||
fontSize={customStyles?.numerals?.fontSize || 16}
|
||||
fontWeight={customStyles?.numerals?.fontWeight || '600'}
|
||||
fill={customStyles?.numerals?.color || 'rgba(0, 0, 0, 0.8)'}
|
||||
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||
>
|
||||
{digit}
|
||||
</text>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default AbacusStatic
|
||||
100
packages/abacus-react/src/AbacusStaticBead.tsx
Normal file
100
packages/abacus-react/src/AbacusStaticBead.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
115
packages/abacus-react/src/AbacusThemes.ts
Normal file
115
packages/abacus-react/src/AbacusThemes.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Pre-defined theme presets for AbacusReact component
|
||||
* These eliminate the need for manual style object creation
|
||||
*/
|
||||
|
||||
import type { AbacusCustomStyles } from './AbacusReact'
|
||||
|
||||
export const ABACUS_THEMES = {
|
||||
/**
|
||||
* Light theme - solid white frame with subtle gray accents
|
||||
* Best for: Clean, minimalist designs on light backgrounds
|
||||
*/
|
||||
light: {
|
||||
columnPosts: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
|
||||
/**
|
||||
* Dark theme - translucent white with subtle glow
|
||||
* Best for: Dark backgrounds, hero sections, dramatic presentations
|
||||
*/
|
||||
dark: {
|
||||
columnPosts: {
|
||||
fill: 'rgba(255, 255, 255, 0.3)',
|
||||
stroke: 'rgba(255, 255, 255, 0.2)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgba(255, 255, 255, 0.4)',
|
||||
stroke: 'rgba(255, 255, 255, 0.25)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
|
||||
/**
|
||||
* Trophy/Premium theme - golden frame with warm tones
|
||||
* Best for: Achievements, rewards, celebration contexts
|
||||
*/
|
||||
trophy: {
|
||||
columnPosts: {
|
||||
fill: '#fbbf24',
|
||||
stroke: '#f59e0b',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: '#fbbf24',
|
||||
stroke: '#f59e0b',
|
||||
strokeWidth: 4,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
|
||||
/**
|
||||
* Translucent theme - subtle, nearly invisible frame
|
||||
* Best for: Inline displays, minimal UI, focus on beads
|
||||
*/
|
||||
translucent: {
|
||||
columnPosts: {
|
||||
fill: 'rgba(0, 0, 0, 0.05)',
|
||||
stroke: 'rgba(0, 0, 0, 0.1)',
|
||||
strokeWidth: 1,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgba(0, 0, 0, 0.1)',
|
||||
stroke: 'none',
|
||||
strokeWidth: 0,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
|
||||
/**
|
||||
* Solid/High-contrast theme - black frame for maximum visibility
|
||||
* Best for: Educational contexts, high visibility requirements
|
||||
*/
|
||||
solid: {
|
||||
columnPosts: {
|
||||
fill: 'rgb(0, 0, 0)',
|
||||
stroke: 'rgb(0, 0, 0)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgb(0, 0, 0)',
|
||||
stroke: 'none',
|
||||
strokeWidth: 0,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
|
||||
/**
|
||||
* Traditional/Natural theme - brown wooden appearance
|
||||
* Best for: Traditional soroban aesthetic, cultural contexts
|
||||
*/
|
||||
traditional: {
|
||||
columnPosts: {
|
||||
fill: '#8B5A2B',
|
||||
stroke: '#654321',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: '#8B5A2B',
|
||||
stroke: '#654321',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
} as AbacusCustomStyles,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Theme names type for TypeScript autocomplete
|
||||
*/
|
||||
export type AbacusThemeName = keyof typeof ABACUS_THEMES
|
||||
358
packages/abacus-react/src/AbacusUtils.ts
Normal file
358
packages/abacus-react/src/AbacusUtils.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* Utility functions for working with abacus states and calculations
|
||||
* These help convert between numbers and bead positions, calculate diffs, etc.
|
||||
*/
|
||||
|
||||
import type { ValidPlaceValues, BeadHighlight } from './AbacusReact'
|
||||
|
||||
/**
|
||||
* Represents the state of beads in a single column
|
||||
*/
|
||||
export interface BeadState {
|
||||
heavenActive: boolean
|
||||
earthActive: number // 0-4
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the complete state of an abacus
|
||||
* Key is the place value (0 = ones, 1 = tens, etc.)
|
||||
*/
|
||||
export interface AbacusState {
|
||||
[placeValue: number]: BeadState
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number to abacus state representation
|
||||
* @param value - The number to convert
|
||||
* @param maxPlaces - Maximum number of place values to include
|
||||
* @returns AbacusState object representing the bead positions
|
||||
*/
|
||||
export function numberToAbacusState(value: number | bigint, maxPlaces: number = 5): AbacusState {
|
||||
const state: AbacusState = {}
|
||||
const valueNum = typeof value === 'bigint' ? Number(value) : value
|
||||
|
||||
for (let place = 0; place < maxPlaces; place++) {
|
||||
const placeValueNum = 10 ** place
|
||||
const digit = Math.floor(valueNum / placeValueNum) % 10
|
||||
|
||||
state[place] = {
|
||||
heavenActive: digit >= 5,
|
||||
earthActive: digit >= 5 ? digit - 5 : digit,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert abacus state to a number
|
||||
* @param state - The abacus state to convert
|
||||
* @returns The numeric value represented by the abacus
|
||||
*/
|
||||
export function abacusStateToNumber(state: AbacusState): number {
|
||||
let total = 0
|
||||
|
||||
for (const placeStr in state) {
|
||||
const place = parseInt(placeStr, 10)
|
||||
const beadState = state[place]
|
||||
const digit = (beadState.heavenActive ? 5 : 0) + beadState.earthActive
|
||||
total += digit * (10 ** place)
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
/**
|
||||
* Bead highlight with place value (internal type for calculations)
|
||||
*/
|
||||
export interface PlaceValueBasedBead {
|
||||
placeValue: ValidPlaceValues
|
||||
beadType: 'heaven' | 'earth'
|
||||
position?: 0 | 1 | 2 | 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate which beads need to change between two abacus states
|
||||
* @param startState - The starting abacus state
|
||||
* @param targetState - The target abacus state
|
||||
* @returns Object with arrays of bead additions and removals
|
||||
*/
|
||||
export function calculateBeadChanges(
|
||||
startState: AbacusState,
|
||||
targetState: AbacusState
|
||||
): {
|
||||
additions: PlaceValueBasedBead[]
|
||||
removals: PlaceValueBasedBead[]
|
||||
placeValue: number
|
||||
} {
|
||||
const additions: PlaceValueBasedBead[] = []
|
||||
const removals: PlaceValueBasedBead[] = []
|
||||
let mainPlaceValue = 0
|
||||
|
||||
for (const placeStr in targetState) {
|
||||
const place = parseInt(placeStr, 10) as ValidPlaceValues
|
||||
const start = startState[place] || { heavenActive: false, earthActive: 0 }
|
||||
const target = targetState[place]
|
||||
|
||||
// Check heaven bead changes
|
||||
if (!start.heavenActive && target.heavenActive) {
|
||||
additions.push({ placeValue: place, beadType: 'heaven' })
|
||||
mainPlaceValue = place
|
||||
} else if (start.heavenActive && !target.heavenActive) {
|
||||
removals.push({ placeValue: place, beadType: 'heaven' })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
|
||||
// Check earth bead changes
|
||||
if (target.earthActive > start.earthActive) {
|
||||
// Adding earth beads
|
||||
for (let pos = start.earthActive; pos < target.earthActive; pos++) {
|
||||
additions.push({ placeValue: place, beadType: 'earth', position: pos as 0 | 1 | 2 | 3 })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
} else if (target.earthActive < start.earthActive) {
|
||||
// Removing earth beads
|
||||
for (let pos = start.earthActive - 1; pos >= target.earthActive; pos--) {
|
||||
removals.push({ placeValue: place, beadType: 'earth', position: pos as 0 | 1 | 2 | 3 })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { additions, removals, placeValue: mainPlaceValue }
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a bead diff calculation
|
||||
*/
|
||||
export interface BeadDiffResult {
|
||||
placeValue: ValidPlaceValues
|
||||
beadType: 'heaven' | 'earth'
|
||||
position?: number
|
||||
direction: 'activate' | 'deactivate'
|
||||
order: number // Order of operations for animations
|
||||
}
|
||||
|
||||
/**
|
||||
* Output of calculateBeadDiff function
|
||||
*/
|
||||
export interface BeadDiffOutput {
|
||||
changes: BeadDiffResult[]
|
||||
highlights: PlaceValueBasedBead[]
|
||||
hasChanges: boolean
|
||||
summary: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the diff between two abacus states
|
||||
* Returns exactly which beads need to move with directions and order
|
||||
* @param fromState - Starting state
|
||||
* @param toState - Target state
|
||||
* @returns BeadDiffOutput with changes, highlights, and summary
|
||||
*/
|
||||
export function calculateBeadDiff(fromState: AbacusState, toState: AbacusState): BeadDiffOutput {
|
||||
const { additions, removals } = calculateBeadChanges(fromState, toState)
|
||||
|
||||
const changes: BeadDiffResult[] = []
|
||||
const highlights: PlaceValueBasedBead[] = []
|
||||
let order = 0
|
||||
|
||||
// Process removals first (pedagogical order: clear before adding)
|
||||
removals.forEach((removal) => {
|
||||
changes.push({
|
||||
placeValue: removal.placeValue,
|
||||
beadType: removal.beadType,
|
||||
position: removal.position,
|
||||
direction: 'deactivate',
|
||||
order: order++,
|
||||
})
|
||||
|
||||
highlights.push({
|
||||
placeValue: removal.placeValue,
|
||||
beadType: removal.beadType,
|
||||
position: removal.position,
|
||||
})
|
||||
})
|
||||
|
||||
// Process additions second (pedagogical order: add after clearing)
|
||||
additions.forEach((addition) => {
|
||||
changes.push({
|
||||
placeValue: addition.placeValue,
|
||||
beadType: addition.beadType,
|
||||
position: addition.position,
|
||||
direction: 'activate',
|
||||
order: order++,
|
||||
})
|
||||
|
||||
highlights.push({
|
||||
placeValue: addition.placeValue,
|
||||
beadType: addition.beadType,
|
||||
position: addition.position,
|
||||
})
|
||||
})
|
||||
|
||||
// Generate summary
|
||||
const summary = generateDiffSummary(changes)
|
||||
|
||||
return {
|
||||
changes,
|
||||
highlights,
|
||||
hasChanges: changes.length > 0,
|
||||
summary,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bead diff from numeric values
|
||||
* Convenience function for when you have numbers instead of states
|
||||
* @param fromValue - Starting numeric value
|
||||
* @param toValue - Target numeric value
|
||||
* @param maxPlaces - Maximum number of place values to consider
|
||||
* @returns BeadDiffOutput
|
||||
*/
|
||||
export function calculateBeadDiffFromValues(
|
||||
fromValue: number | bigint,
|
||||
toValue: number | bigint,
|
||||
maxPlaces: number = 5
|
||||
): BeadDiffOutput {
|
||||
const fromState = numberToAbacusState(fromValue, maxPlaces)
|
||||
const toState = numberToAbacusState(toValue, maxPlaces)
|
||||
return calculateBeadDiff(fromState, toState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an abacus value is within the supported range
|
||||
* @param value - The value to validate
|
||||
* @param maxPlaces - Maximum number of place values supported
|
||||
* @returns Object with isValid boolean and optional error message
|
||||
*/
|
||||
export function validateAbacusValue(
|
||||
value: number | bigint,
|
||||
maxPlaces: number = 5
|
||||
): { isValid: boolean; error?: string } {
|
||||
const valueNum = typeof value === 'bigint' ? Number(value) : value
|
||||
|
||||
if (valueNum < 0) {
|
||||
return { isValid: false, error: 'Negative values are not supported' }
|
||||
}
|
||||
|
||||
const maxValue = 10 ** maxPlaces - 1
|
||||
if (valueNum > maxValue) {
|
||||
return { isValid: false, error: `Value exceeds maximum for ${maxPlaces} columns (max: ${maxValue})` }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two abacus states are equal
|
||||
* @param state1 - First state
|
||||
* @param state2 - Second state
|
||||
* @returns true if states are equal
|
||||
*/
|
||||
export function areStatesEqual(state1: AbacusState, state2: AbacusState): boolean {
|
||||
const places1 = Object.keys(state1)
|
||||
.map((k) => parseInt(k, 10))
|
||||
.sort()
|
||||
const places2 = Object.keys(state2)
|
||||
.map((k) => parseInt(k, 10))
|
||||
.sort()
|
||||
|
||||
if (places1.length !== places2.length) return false
|
||||
|
||||
for (const place of places1) {
|
||||
const bead1 = state1[place]
|
||||
const bead2 = state2[place]
|
||||
|
||||
if (!bead2) return false
|
||||
if (bead1.heavenActive !== bead2.heavenActive) return false
|
||||
if (bead1.earthActive !== bead2.earthActive) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Internal helper functions
|
||||
|
||||
function generateDiffSummary(changes: BeadDiffResult[]): string {
|
||||
if (changes.length === 0) {
|
||||
return 'No changes needed'
|
||||
}
|
||||
|
||||
// Sort by order to respect pedagogical sequence
|
||||
const sortedChanges = [...changes].sort((a, b) => a.order - b.order)
|
||||
|
||||
const deactivations = sortedChanges.filter((c) => c.direction === 'deactivate')
|
||||
const activations = sortedChanges.filter((c) => c.direction === 'activate')
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// Process deactivations first (pedagogical order)
|
||||
if (deactivations.length > 0) {
|
||||
const deactivationsByPlace = groupByPlace(deactivations)
|
||||
Object.entries(deactivationsByPlace).forEach(([place, beads]) => {
|
||||
const placeName = getPlaceName(parseInt(place, 10))
|
||||
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
|
||||
const earthBeads = beads.filter((b) => b.beadType === 'earth')
|
||||
|
||||
if (heavenBeads.length > 0) {
|
||||
parts.push(`remove heaven bead in ${placeName}`)
|
||||
}
|
||||
if (earthBeads.length > 0) {
|
||||
const count = earthBeads.length
|
||||
parts.push(`remove ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Process activations second (pedagogical order)
|
||||
if (activations.length > 0) {
|
||||
const activationsByPlace = groupByPlace(activations)
|
||||
Object.entries(activationsByPlace).forEach(([place, beads]) => {
|
||||
const placeName = getPlaceName(parseInt(place, 10))
|
||||
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
|
||||
const earthBeads = beads.filter((b) => b.beadType === 'earth')
|
||||
|
||||
if (heavenBeads.length > 0) {
|
||||
parts.push(`add heaven bead in ${placeName}`)
|
||||
}
|
||||
if (earthBeads.length > 0) {
|
||||
const count = earthBeads.length
|
||||
parts.push(`add ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return parts.join(', then ')
|
||||
}
|
||||
|
||||
function groupByPlace(changes: BeadDiffResult[]): {
|
||||
[place: string]: BeadDiffResult[]
|
||||
} {
|
||||
return changes.reduce(
|
||||
(groups, change) => {
|
||||
const place = change.placeValue.toString()
|
||||
if (!groups[place]) {
|
||||
groups[place] = []
|
||||
}
|
||||
groups[place].push(change)
|
||||
return groups
|
||||
},
|
||||
{} as { [place: string]: BeadDiffResult[] }
|
||||
)
|
||||
}
|
||||
|
||||
function getPlaceName(place: number): string {
|
||||
switch (place) {
|
||||
case 0:
|
||||
return 'ones column'
|
||||
case 1:
|
||||
return 'tens column'
|
||||
case 2:
|
||||
return 'hundreds column'
|
||||
case 3:
|
||||
return 'thousands column'
|
||||
default:
|
||||
return `place ${place} column`
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
export { default as AbacusReact } from "./AbacusReact";
|
||||
export type { AbacusConfig, BeadConfig, AbacusDimensions } from "./AbacusReact";
|
||||
export type {
|
||||
AbacusConfig,
|
||||
BeadConfig,
|
||||
AbacusDimensions,
|
||||
AbacusCustomStyles,
|
||||
BeadStyle,
|
||||
ColumnPostStyle,
|
||||
ReckoningBarStyle,
|
||||
NumeralStyle,
|
||||
ValidPlaceValues,
|
||||
BeadHighlight,
|
||||
StepBeadHighlight,
|
||||
BeadClickEvent,
|
||||
AbacusCallbacks,
|
||||
AbacusOverlay,
|
||||
} from "./AbacusReact";
|
||||
|
||||
export {
|
||||
useAbacusConfig,
|
||||
@@ -17,3 +32,28 @@ export type {
|
||||
|
||||
export { StandaloneBead } from "./StandaloneBead";
|
||||
export type { StandaloneBeadProps } from "./StandaloneBead";
|
||||
|
||||
export { AbacusStatic } from "./AbacusStatic";
|
||||
export type { AbacusStaticConfig } from "./AbacusStatic";
|
||||
|
||||
export { ABACUS_THEMES } from "./AbacusThemes";
|
||||
export type { AbacusThemeName } from "./AbacusThemes";
|
||||
|
||||
export {
|
||||
numberToAbacusState,
|
||||
abacusStateToNumber,
|
||||
calculateBeadChanges,
|
||||
calculateBeadDiff,
|
||||
calculateBeadDiffFromValues,
|
||||
validateAbacusValue,
|
||||
areStatesEqual,
|
||||
} from "./AbacusUtils";
|
||||
export type {
|
||||
BeadState,
|
||||
AbacusState,
|
||||
BeadDiffResult,
|
||||
BeadDiffOutput,
|
||||
PlaceValueBasedBead,
|
||||
} from "./AbacusUtils";
|
||||
|
||||
export { useAbacusDiff, useAbacusState } from "./AbacusHooks";
|
||||
|
||||
Reference in New Issue
Block a user