Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d1bad142b | ||
|
|
1869216d2f | ||
|
|
e4ae3aefef | ||
|
|
d018b699c4 | ||
|
|
be323bfbc5 | ||
|
|
50fc3fdf7f | ||
|
|
e52d907087 | ||
|
|
c0fa926d16 | ||
|
|
1fd0474cd5 | ||
|
|
bf37eb1928 | ||
|
|
9f56c9728c | ||
|
|
0f51366fd5 | ||
|
|
fe1e8979c8 | ||
|
|
92148a4cf8 | ||
|
|
7088a7096a | ||
|
|
5f0ad14133 | ||
|
|
73f8f637cd | ||
|
|
f32480a0f9 | ||
|
|
11aa44d882 | ||
|
|
30e16c8e5a | ||
|
|
86357b3d7a | ||
|
|
ad1ad690f0 | ||
|
|
53475cf40e | ||
|
|
424f41d4bf | ||
|
|
4c6939807e | ||
|
|
b8235be612 | ||
|
|
86dee31c9a | ||
|
|
b401bb5fa4 | ||
|
|
7666b0aea9 | ||
|
|
39afa455de | ||
|
|
a58f7b78b0 | ||
|
|
1c001e07b7 | ||
|
|
8893675b36 | ||
|
|
4254459238 | ||
|
|
89b90723b7 | ||
|
|
6e5aec858f | ||
|
|
5611d148aa | ||
|
|
d5f60ce9d2 | ||
|
|
284fc90a53 | ||
|
|
e54ea20dbe | ||
|
|
415a1fb1fa | ||
|
|
60b3a788b3 | ||
|
|
62ff067bb9 | ||
|
|
3d774c8d82 | ||
|
|
61403f2f50 | ||
|
|
aa297d4ef7 | ||
|
|
712d318e7c | ||
|
|
cd3eb61cb5 | ||
|
|
8871050990 | ||
|
|
a6ac55b7b1 | ||
|
|
64e2464ec1 | ||
|
|
422bf3d968 | ||
|
|
bc1ad3a43a | ||
|
|
3b5d14765d | ||
|
|
9847f8f461 | ||
|
|
57c212f4f5 | ||
|
|
b2f5c19ce3 | ||
|
|
e65e96952f | ||
|
|
556a0eb194 | ||
|
|
5070d8d64f | ||
|
|
54cedbe03a | ||
|
|
fa26acfbae | ||
|
|
9ec0a71546 | ||
|
|
6448249512 | ||
|
|
a537bc18c3 | ||
|
|
33ab7aaaf0 | ||
|
|
f47b172f66 | ||
|
|
9d25e1dd35 | ||
|
|
015e30b085 | ||
|
|
8a2d5ae319 | ||
|
|
d2ff2c6a29 | ||
|
|
3cf4f92643 | ||
|
|
074488349a | ||
|
|
30a5587bca | ||
|
|
4082a246a3 | ||
|
|
9703fed94c | ||
|
|
5dc636a71c | ||
|
|
16d978db9a | ||
|
|
e711c52757 | ||
|
|
009162e22c | ||
|
|
cd30944c5e | ||
|
|
3e58cb5f92 | ||
|
|
aba9f8a94d | ||
|
|
dc19622bbb | ||
|
|
1babfde328 | ||
|
|
76d6f19d51 | ||
|
|
9ad35e65d3 | ||
|
|
d362a770d6 | ||
|
|
095cdda4ca | ||
|
|
a1a135a858 | ||
|
|
7f516526fb | ||
|
|
9f706e9dce | ||
|
|
6410b21f82 | ||
|
|
3dc9f48d12 | ||
|
|
b6410c7c22 | ||
|
|
b54aaf1a67 | ||
|
|
c6dc210bf8 | ||
|
|
c89aea7444 | ||
|
|
3564bd51dc | ||
|
|
cc315645de | ||
|
|
035d8312c7 | ||
|
|
5f9b2dfe2b | ||
|
|
1bfde8fb25 | ||
|
|
48647e4fb5 | ||
|
|
318f9469a0 | ||
|
|
4bace36561 | ||
|
|
8c2ddca28d | ||
|
|
eff44b3ad1 | ||
|
|
f81b88ae30 | ||
|
|
71b1b933b5 | ||
|
|
92d50673e5 | ||
|
|
c229faffac | ||
|
|
463841e191 | ||
|
|
3a3706cc6f | ||
|
|
721dfe426d | ||
|
|
9dba75c9d9 | ||
|
|
f9a7cb7f05 | ||
|
|
ae7463d917 | ||
|
|
02b6c70b7a | ||
|
|
4b72e0c561 | ||
|
|
e0b6a2e88b | ||
|
|
230f1dcd86 | ||
|
|
d8ec64280e | ||
|
|
1d4419364a | ||
|
|
cce8980e17 | ||
|
|
4bcce2a8db | ||
|
|
2818fd15ca | ||
|
|
29d20a6c07 | ||
|
|
be2c3f63b0 | ||
|
|
aa0bdcf686 | ||
|
|
baea602000 | ||
|
|
05dd0b30d3 | ||
|
|
4febf5905b | ||
|
|
6739d59f2b | ||
|
|
cb20019c16 | ||
|
|
d90b5d5532 | ||
|
|
7028db0263 | ||
|
|
fa3b73c691 | ||
|
|
fd4d25c2d1 | ||
|
|
6501b073b1 | ||
|
|
9b4d9c21df | ||
|
|
53d23f19bc | ||
|
|
6c14012b97 | ||
|
|
c650ffa193 | ||
|
|
28834e8a3e | ||
|
|
8681b17340 | ||
|
|
d52cc608eb | ||
|
|
6f89d9e274 | ||
|
|
a8cc2bc0f0 | ||
|
|
9dff3e7b7b | ||
|
|
92fedb698d | ||
|
|
1e90d6c620 | ||
|
|
5fcb7925eb | ||
|
|
41eaed24fc | ||
|
|
de5f36481b | ||
|
|
e5ffe3927e | ||
|
|
7c47fcdc54 | ||
|
|
4f4c73577a | ||
|
|
1e6459f9c1 | ||
|
|
477a0b367e | ||
|
|
8751649233 | ||
|
|
07c783a794 | ||
|
|
ca8cef1c36 | ||
|
|
0d47664f9f | ||
|
|
1e5467fad4 | ||
|
|
44f8b27fa1 | ||
|
|
0146ce1e67 | ||
|
|
acfb0dac0a | ||
|
|
0fbde53039 | ||
|
|
90bbe6fbb7 | ||
|
|
a03e73c849 | ||
|
|
f3dce84532 | ||
|
|
3b6284ae18 | ||
|
|
563136fb79 | ||
|
|
ead9ee9589 | ||
|
|
a12ae969be | ||
|
|
abb647ce40 | ||
|
|
d6c28f7ede | ||
|
|
cd5c15aeb2 | ||
|
|
ccaad3abc8 | ||
|
|
22f00f59f5 | ||
|
|
a85815fdf9 | ||
|
|
09004dc2c0 | ||
|
|
22c8a57a16 | ||
|
|
2d8bb4ab88 | ||
|
|
8e345cfb4c | ||
|
|
200b26c2cd | ||
|
|
66e38af457 | ||
|
|
c80477d248 | ||
|
|
9a688c1574 | ||
|
|
fd2b6338a8 | ||
|
|
2cfde18414 | ||
|
|
0ab4cc2880 | ||
|
|
6b7c455315 | ||
|
|
c38767f4d3 | ||
|
|
321d9aea10 | ||
|
|
92e1e62132 | ||
|
|
84d980bb24 | ||
|
|
892b377eb3 | ||
|
|
bc21095fa1 | ||
|
|
eb3b100056 |
689
CHANGELOG.md
689
CHANGELOG.md
@@ -1,3 +1,692 @@
|
||||
## [4.63.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.3...v4.63.4) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** keep grab point under cursor with proper coordinate conversion ([1869216](https://github.com/antialias/soroban-abacus-flashcards/commit/1869216d2fda77303c0b79d4f613c6dcdaf5324b))
|
||||
|
||||
## [4.63.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.2...v4.63.3) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** revert to simple delta positioning to prevent card jumping ([d018b69](https://github.com/antialias/soroban-abacus-flashcards/commit/d018b699c46aea90e9cdc3309e797ff2d7447ecf))
|
||||
|
||||
## [4.63.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.1...v4.63.2) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** correct pivot point to rotate around card center ([50fc3fd](https://github.com/antialias/soroban-abacus-flashcards/commit/50fc3fdf7f2c9b7412f6d7d890f5e0d52cb86a9b))
|
||||
|
||||
## [4.63.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.0...v4.63.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** increase rotation sensitivity 10x for visible grab point physics ([c0fa926](https://github.com/antialias/soroban-abacus-flashcards/commit/c0fa926d16d02c1bfe880b7f0056a760e8461b3b))
|
||||
|
||||
## [4.63.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.1...v4.63.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** add grab point physics for realistic rotation ([bf37eb1](https://github.com/antialias/soroban-abacus-flashcards/commit/bf37eb1928de8d07673234e2faa1fa6268c45686))
|
||||
|
||||
## [4.62.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.0...v4.62.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** improve shadow speed logging with separate throttling ([0f51366](https://github.com/antialias/soroban-abacus-flashcards/commit/0f51366fd56540e691df4931b6350c03043484f1))
|
||||
|
||||
## [4.62.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.3...v4.62.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** add dynamic shadow based on drag speed ([92148a4](https://github.com/antialias/soroban-abacus-flashcards/commit/92148a4cf87e828ba2e5ec1740fb51d9667c1d73))
|
||||
|
||||
## [4.61.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.2...v4.61.3) (2025-10-21)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **flashcards:** completely rewrite drag-drop with simple approach ([5f0ad14](https://github.com/antialias/soroban-abacus-flashcards/commit/5f0ad14133340d073e861f5721cb48e1abab03ff))
|
||||
|
||||
## [4.61.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.1...v4.61.2) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** use explicit per-property configs to fix decay physics ([f32480a](https://github.com/antialias/soroban-abacus-flashcards/commit/f32480a0f9153285341e5a28078840abc0590873))
|
||||
|
||||
## [4.61.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.0...v4.61.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** fix position snap-back by using api.set before decay ([30e16c8](https://github.com/antialias/soroban-abacus-flashcards/commit/30e16c8e5ac3bb25f2d54cf715dc6fb45adc4fcc))
|
||||
|
||||
## [4.61.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.60.0...v4.61.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** enable unbounded drag and position persistence ([ad1ad69](https://github.com/antialias/soroban-abacus-flashcards/commit/ad1ad690f014257b5a3c3f599e794205a11d286f))
|
||||
|
||||
## [4.60.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.59.1...v4.60.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** significantly increase mobile hero abacus size ([424f41d](https://github.com/antialias/soroban-abacus-flashcards/commit/424f41d4bfc1ddea068f8c110b495ebd5c0bb455))
|
||||
|
||||
## [4.59.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.59.0...v4.59.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** adjust hero abacus scale for optimal sizing across devices ([86dee31](https://github.com/antialias/soroban-abacus-flashcards/commit/86dee31c9a51ca0712f1b4181a4899d25374d403))
|
||||
* **homepage:** reduce mobile abacus scale to prevent scroll hint overlap ([b8235be](https://github.com/antialias/soroban-abacus-flashcards/commit/b8235be612c3f1dbd0da2b6cd1a935001b7dac9b))
|
||||
|
||||
## [4.59.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.2...v4.59.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** increase hero abacus size for better visibility ([7666b0a](https://github.com/antialias/soroban-abacus-flashcards/commit/7666b0aea949f2432a4d0f4648c1a366af3ea6d2))
|
||||
|
||||
## [4.58.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.1...v4.58.2) (2025-10-21)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **navbar:** prevent subtitle wrap and remove abacus emoji ([a58f7b7](https://github.com/antialias/soroban-abacus-flashcards/commit/a58f7b78b0020c85da523c36fdf6d70ad069736a))
|
||||
|
||||
## [4.58.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.0...v4.58.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **navbar:** apply glassmorphism to transparent mode, not scrolled mode ([8893675](https://github.com/antialias/soroban-abacus-flashcards/commit/8893675b36b1c1534c6fe7e57fa7e0cc55f198d6))
|
||||
|
||||
## [4.58.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.10...v4.58.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **navbar:** add glassmorphism effect to nav links when scrolled ([89b9072](https://github.com/antialias/soroban-abacus-flashcards/commit/89b90723b7a3fc9ed12da3ba8718fccb6ce0760f))
|
||||
|
||||
## [4.57.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.9...v4.57.10) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **navbar:** remove border artifact and add 10px bottom fade ([d5f60ce](https://github.com/antialias/soroban-abacus-flashcards/commit/d5f60ce9d2fbc2a870b3bb96f5365a0e04e0afc4))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **navbar:** improve theming to match homepage dark aesthetic ([284fc90](https://github.com/antialias/soroban-abacus-flashcards/commit/284fc90a53f5f4868a3e41421760ebc813be12b5))
|
||||
|
||||
## [4.57.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.8...v4.57.9) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** add overflow hidden to Your Journey section ([415a1fb](https://github.com/antialias/soroban-abacus-flashcards/commit/415a1fb1faa263c9d69b4e781ce22da235ca2b66))
|
||||
|
||||
## [4.57.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.7...v4.57.8) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** adjust responsive breakpoints to prevent skill card clipping ([62ff067](https://github.com/antialias/soroban-abacus-flashcards/commit/62ff067bb956b17a9b3569eadc2a32abd24c27b8))
|
||||
|
||||
## [4.57.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.6...v4.57.7) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** restructure layout to center 1400px wide demo section ([61403f2](https://github.com/antialias/soroban-abacus-flashcards/commit/61403f2f506557b57716a298d4dc481d7853552f))
|
||||
* **homepage:** set min-width 1400px on container and remove max-width ([aa297d4](https://github.com/antialias/soroban-abacus-flashcards/commit/aa297d4ef7559473a147934766bfa3868552f58d))
|
||||
|
||||
## [4.57.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.5...v4.57.6) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** increase skill cards container width to prevent title wrapping ([cd3eb61](https://github.com/antialias/soroban-abacus-flashcards/commit/cd3eb61cb59e6faef37fbf609f37f7e2dc302e72))
|
||||
|
||||
## [4.57.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.4...v4.57.5) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** prevent text overflow in skill cards ([a6ac55b](https://github.com/antialias/soroban-abacus-flashcards/commit/a6ac55b7b161e0dd33a4dd5acc0df647b2a513aa))
|
||||
|
||||
## [4.57.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.3...v4.57.4) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** align container width breakpoint with grid columns ([422bf3d](https://github.com/antialias/soroban-abacus-flashcards/commit/422bf3d968b67e4683ac7ea7e487a84513f992f9))
|
||||
* **homepage:** make MiniAbacus fill container properly ([3b5d147](https://github.com/antialias/soroban-abacus-flashcards/commit/3b5d14765dfb2d61a76f66ba3ae09695ce88bb6d))
|
||||
* **homepage:** widen skill cards container to 650px ([bc1ad3a](https://github.com/antialias/soroban-abacus-flashcards/commit/bc1ad3a43a79570e1f9c61d5118d14ac4c201d71))
|
||||
|
||||
## [4.57.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.2...v4.57.3) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** increase abacus container width to 120px/150px ([57c212f](https://github.com/antialias/soroban-abacus-flashcards/commit/57c212f4f5be591f712e1c5610e1f323e56e15dd))
|
||||
|
||||
## [4.57.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.1...v4.57.2) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** increase skill card abacus container width ([e65e969](https://github.com/antialias/soroban-abacus-flashcards/commit/e65e96952f4e631722c73fc56d088fa3ff1ba858))
|
||||
|
||||
## [4.57.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.0...v4.57.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** add overflow hidden to skill cards ([5070d8d](https://github.com/antialias/soroban-abacus-flashcards/commit/5070d8d64f7f58887ff7259bee9ce5166c4f8af8))
|
||||
|
||||
## [4.57.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.56.0...v4.57.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** make skills section responsive with emojis ([9ec0a71](https://github.com/antialias/soroban-abacus-flashcards/commit/9ec0a71546ee483233ed7866dae97345bf2384d7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** prevent skill card overflow ([fa26acf](https://github.com/antialias/soroban-abacus-flashcards/commit/fa26acfbaef1a04bb225956b2f684cd5023b56fa))
|
||||
|
||||
## [4.56.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.55.0...v4.56.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** emphasize single-player and observer modes ([a537bc1](https://github.com/antialias/soroban-abacus-flashcards/commit/a537bc18c34d94ca931e483ea01e497d6f5d4e5b))
|
||||
|
||||
## [4.55.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.54.0...v4.55.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** update section title to "The Arcade" ([f47b172](https://github.com/antialias/soroban-abacus-flashcards/commit/f47b172f66bee0017c11d8f129f5b83f2ef3dcd9))
|
||||
|
||||
## [4.54.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.53.0...v4.54.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add interactive levels slider to replace static progression ([8a2d5ae](https://github.com/antialias/soroban-abacus-flashcards/commit/8a2d5ae319af8fd66010dd5538e4b82f7fb35d40))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** extract levels slider into shared component ([015e30b](https://github.com/antialias/soroban-abacus-flashcards/commit/015e30b085ad2ef798ffd6f7f6716269e3256651))
|
||||
|
||||
## [4.53.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.52.2...v4.53.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add interactive draggable flashcards with physics ([0744883](https://github.com/antialias/soroban-abacus-flashcards/commit/074488349a3ec480548223c313006aa1e9e64e5c))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** merge flashcard display with create button section ([3cf4f92](https://github.com/antialias/soroban-abacus-flashcards/commit/3cf4f92643306f055188ede508557515ef5efe98))
|
||||
|
||||
## [4.52.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.52.1...v4.52.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** use actual container dimensions for flashcard positioning ([4082a24](https://github.com/antialias/soroban-abacus-flashcards/commit/4082a246a33ea67617b762d5b7490a8c9af0ad49))
|
||||
|
||||
## [4.52.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.52.0...v4.52.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** correct flashcard transform rendering ([5dc636a](https://github.com/antialias/soroban-abacus-flashcards/commit/5dc636a71c15db28c029fd4f60e4a6c95620f953))
|
||||
|
||||
## [4.52.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.51.0...v4.52.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add interactive draggable flashcards with physics ([e711c52](https://github.com/antialias/soroban-abacus-flashcards/commit/e711c527574412de2f9d451c7985c4f8667d269a))
|
||||
|
||||
## [4.51.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.1...v4.51.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** create fancy flashcard display with spread-out cards ([cd30944](https://github.com/antialias/soroban-abacus-flashcards/commit/cd30944c5e067f84d00dfdf41c37580acc589548))
|
||||
|
||||
## [4.50.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.50.0...v4.50.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** set fixed width for learning panel to prevent layout shift ([dc19622](https://github.com/antialias/soroban-abacus-flashcards/commit/dc19622bbba2fead8cd9c0b2bda3a38abba0bd41))
|
||||
* **homepage:** set fixed width for tutorial panel to prevent layout shift ([aba9f8a](https://github.com/antialias/soroban-abacus-flashcards/commit/aba9f8a94d50590cf94b6cd87f85b497e89045e7))
|
||||
|
||||
## [4.50.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.1...v4.50.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add interactive learning panels with animated mini-tutorials ([76d6f19](https://github.com/antialias/soroban-abacus-flashcards/commit/76d6f19d51fe4b9594998ae4e0a8823aff389854))
|
||||
|
||||
## [4.49.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.49.0...v4.49.1) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** streamline homepage sections ([d362a77](https://github.com/antialias/soroban-abacus-flashcards/commit/d362a770d63405efee5ef8a896d34e783dd11de2))
|
||||
|
||||
## [4.49.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.5...v4.49.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add vibrant gradients and equal heights to game cards ([a1a135a](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a135a8586e314c9d695bec6c4e58ec24e5c9cb)), closes [#4](https://github.com/antialias/soroban-abacus-flashcards/issues/4) [#00f2](https://github.com/antialias/soroban-abacus-flashcards/issues/00f2) [#667](https://github.com/antialias/soroban-abacus-flashcards/issues/667) [#764ba2](https://github.com/antialias/soroban-abacus-flashcards/issues/764ba2) [#f093](https://github.com/antialias/soroban-abacus-flashcards/issues/f093) [#f5576](https://github.com/antialias/soroban-abacus-flashcards/issues/f5576) [#43e97](https://github.com/antialias/soroban-abacus-flashcards/issues/43e97) [#38f9d7](https://github.com/antialias/soroban-abacus-flashcards/issues/38f9d7)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* make homepage game cards dynamic from game registry ([7f51652](https://github.com/antialias/soroban-abacus-flashcards/commit/7f516526fb5f5b60c1782db5c8c3e29f05caafa7))
|
||||
|
||||
## [4.48.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.4...v4.48.5) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** add dark gradient overlay for better text contrast on game cards ([6410b21](https://github.com/antialias/soroban-abacus-flashcards/commit/6410b21f829810af27e42d188295630bd67d6b6b))
|
||||
|
||||
## [4.48.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.3...v4.48.4) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** improve text contrast on game cards with text shadows ([b6410c7](https://github.com/antialias/soroban-abacus-flashcards/commit/b6410c7c225f01f42d095ca270b8da7903cbfbb0))
|
||||
|
||||
## [4.48.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.2...v4.48.3) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** display gradient backgrounds on all game cards ([c6dc210](https://github.com/antialias/soroban-abacus-flashcards/commit/c6dc210bf8e3a5b4d7d6e53f2a7427d335c65322))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **z-index:** add comprehensive z-index and stacking context documentation ([c89aea7](https://github.com/antialias/soroban-abacus-flashcards/commit/c89aea744478696b6f812fe53311a2dba210540f))
|
||||
|
||||
## [4.48.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.1...v4.48.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nav:** ensure nav bar appears above tutorial tooltips ([cc31564](https://github.com/antialias/soroban-abacus-flashcards/commit/cc315645de30218d1b034da3e130458fe2961a69))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero:** unify background with rest of homepage ([035d831](https://github.com/antialias/soroban-abacus-flashcards/commit/035d8312c707cbf5b0e2a725d7b1d8ff406f842d))
|
||||
|
||||
## [4.48.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.0...v4.48.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** prevent SSR hydration mismatch for subtitle ([1bfde8f](https://github.com/antialias/soroban-abacus-flashcards/commit/1bfde8fb251b227ccd2528bfe1c47acffd79fa49))
|
||||
|
||||
## [4.48.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.2...v4.48.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **hero:** persist random subtitle per-session ([318f946](https://github.com/antialias/soroban-abacus-flashcards/commit/318f9469a0805c200c55ce4024a95fd7b8dbe6a2))
|
||||
|
||||
## [4.47.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.1...v4.47.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nav:** prevent thrashing by using fixed position always ([eff44b3](https://github.com/antialias/soroban-abacus-flashcards/commit/eff44b3ad1ea0535c6965ad58012f9275cb143ec))
|
||||
* **nav:** remove unnecessary borders from transparent nav ([8c2ddca](https://github.com/antialias/soroban-abacus-flashcards/commit/8c2ddca28dbdd7743227eed4d19a9a8f662a72b5))
|
||||
|
||||
## [4.47.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.0...v4.47.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** prevent nav thrashing with hysteresis ([71b1b93](https://github.com/antialias/soroban-abacus-flashcards/commit/71b1b933b598c0a6a8aef1bc9f8c598c1871b2eb))
|
||||
|
||||
## [4.47.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.2...v4.47.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **nav:** add transparent nav bar with borders when hero visible ([463841e](https://github.com/antialias/soroban-abacus-flashcards/commit/463841e1910f4ddb9af662f036e4efb867836a83))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** use Number.isNaN instead of global isNaN ([c229faf](https://github.com/antialias/soroban-abacus-flashcards/commit/c229faffac525f3eebeb12155cb5ca4dff744472))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero-abacus:** add purple bead colors for dark theme ([721dfe4](https://github.com/antialias/soroban-abacus-flashcards/commit/721dfe426db4fe259f6cdeac587d008339df769b))
|
||||
* **hero:** adjust spacing between title, subtitle, and abacus ([3a3706c](https://github.com/antialias/soroban-abacus-flashcards/commit/3a3706cc6fb694c7762f065f4ab4996bb8608dc4))
|
||||
|
||||
## [4.46.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.1...v4.46.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** properly type HomeHeroContext in AppNavBar ([f9a7cb7](https://github.com/antialias/soroban-abacus-flashcards/commit/f9a7cb7f05dfddf291d89212a77ba1c11c00c9c7))
|
||||
|
||||
## [4.46.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.0...v4.46.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero-abacus:** restructure layout to prevent visual overlap ([02b6c70](https://github.com/antialias/soroban-abacus-flashcards/commit/02b6c70b7a52f7de2954e5e0efddbed64d419d6c))
|
||||
|
||||
## [4.46.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.45.0...v4.46.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add full-page hero abacus with scroll-based nav transition ([d8ec642](https://github.com/antialias/soroban-abacus-flashcards/commit/d8ec64280ec0c2f44f2fd9c72a93a882481f650b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** improve hero abacus sizing and layout ([230f1dc](https://github.com/antialias/soroban-abacus-flashcards/commit/230f1dcd866e5b3625e19f7400f5eae478fe7d0c))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero-abacus:** apply dark theme to match homepage styling ([e0b6a2e](https://github.com/antialias/soroban-abacus-flashcards/commit/e0b6a2e88b3ebbaae41ed54f23f9e514604d2262))
|
||||
|
||||
## [4.45.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.3...v4.45.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **branding:** rebrand navigation from 'Soroban Generator' to 'Abaci One' ([cce8980](https://github.com/antialias/soroban-abacus-flashcards/commit/cce8980e177da1b3c344e46561d928ed98b86f6c))
|
||||
|
||||
## [4.44.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.2...v4.44.3) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** reduce operator box sizes and remove divider line ([29d20a6](https://github.com/antialias/soroban-abacus-flashcards/commit/29d20a6c0741e7427f2bb64bc9c3e950b1a3238a))
|
||||
* **levels:** use uniform padding on operator box grid ([2818fd1](https://github.com/antialias/soroban-abacus-flashcards/commit/2818fd15cacac78de6d86ba769b9b2a02800ed1e))
|
||||
|
||||
## [4.44.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.1...v4.44.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** match top/bottom margins to left padding on kyu detail boxes ([aa0bdcf](https://github.com/antialias/soroban-abacus-flashcards/commit/aa0bdcf686adcbfd1a145cf67121181d1f1194d9))
|
||||
|
||||
## [4.44.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.0...v4.44.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add fixed dimensions and margins to kyu detail boxes ([05dd0b3](https://github.com/antialias/soroban-abacus-flashcards/commit/05dd0b30d3c397b82b7b7cc93a5ea575f3aada6d))
|
||||
|
||||
## [4.44.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.2...v4.44.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** redesign kyu details with larger operators and prominent digits ([6739d59](https://github.com/antialias/soroban-abacus-flashcards/commit/6739d59f2b6189a98570e23e04c20d86d774ccce))
|
||||
|
||||
## [4.43.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.1...v4.43.2) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** remove Time and Pass sections from kyu details ([d90b5d5](https://github.com/antialias/soroban-abacus-flashcards/commit/d90b5d55322e75dd28b95376614663a506c829d4))
|
||||
|
||||
## [4.43.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.0...v4.43.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use two-column grid for kyu details to prevent clipping ([fa3b73c](https://github.com/antialias/soroban-abacus-flashcards/commit/fa3b73c69169b4694201ffa19ae3f8b5a68dfe32))
|
||||
|
||||
## [4.43.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.42.1...v4.43.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add structured kyu exam details with card UI ([6501b07](https://github.com/antialias/soroban-abacus-flashcards/commit/6501b073b100a00982cff1ca3140921e74f31a9c))
|
||||
|
||||
## [4.42.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.42.0...v4.42.1) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** store kyu data verbatim, add formatting layer ([53d23f1](https://github.com/antialias/soroban-abacus-flashcards/commit/53d23f19bc06459462afb76ed94d9b99d583a32d))
|
||||
|
||||
## [4.42.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.41.0...v4.42.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add kyu level details display with English translations ([c650ffa](https://github.com/antialias/soroban-abacus-flashcards/commit/c650ffa1935fe370d37190b2843c0deecdcce8e7))
|
||||
|
||||
## [4.41.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.1...v4.41.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** right-align abacus display ([8681b17](https://github.com/antialias/soroban-abacus-flashcards/commit/8681b17340e757cf04d17f884a780a251645bb33))
|
||||
|
||||
## [4.40.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.0...v4.40.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** increase animation speed to 10ms for 10th Dan ([6f89d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/6f89d9e274082908fc090a9c0ba310f2cb06f014))
|
||||
|
||||
## [4.40.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.1...v4.40.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** progressive animation speed for Dan levels ([9dff3e7](https://github.com/antialias/soroban-abacus-flashcards/commit/9dff3e7b7b1ca46ea7f19a48135124b80c5182c0))
|
||||
|
||||
## [4.39.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.0...v4.39.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** improve slider tick spacing to use full width ([1e90d6c](https://github.com/antialias/soroban-abacus-flashcards/commit/1e90d6c6207f29084a8dc96ccfbb1013a1a62271))
|
||||
|
||||
## [4.39.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.1...v4.39.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add auto-advance slider with hover pause ([41eaed2](https://github.com/antialias/soroban-abacus-flashcards/commit/41eaed24fce510bab7fd03fa2e39e829b33a7346))
|
||||
|
||||
## [4.38.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.0...v4.38.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** adjust slider text positioning to prevent emoji overlap ([e5ffe39](https://github.com/antialias/soroban-abacus-flashcards/commit/e5ffe3927edfb1baea7ddd216507e081f50e5d2c))
|
||||
|
||||
## [4.38.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.37.0...v4.38.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add animated calculation effect to abacus display ([4f4c735](https://github.com/antialias/soroban-abacus-flashcards/commit/4f4c73577a944518c093b3208a85482909fe3064))
|
||||
|
||||
## [4.37.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.36.0...v4.37.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add hover tracking to slider for real-time level preview ([477a0b3](https://github.com/antialias/soroban-abacus-flashcards/commit/477a0b367e32749b865b5a5405846e86d5bcef6a))
|
||||
|
||||
## [4.36.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.35.1...v4.36.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** make emoji tick marks clickable and remove redundant UI ([07c783a](https://github.com/antialias/soroban-abacus-flashcards/commit/07c783a79454f50e7302b19684be6d2e5930154d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add smooth CSS transitions for slider thumb movement ([ca8cef1](https://github.com/antialias/soroban-abacus-flashcards/commit/ca8cef1c36efeb1c8c214c74f8bd383f9295be3b))
|
||||
|
||||
## [4.35.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.35.0...v4.35.1) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** export StandaloneBead component wired to AbacusDisplayContext ([0146ce1](https://github.com/antialias/soroban-abacus-flashcards/commit/0146ce1e67da27a24cbaa8338ba6a1a6befd6bd3))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **levels:** speed up slider animations for more responsive feel ([1e5467f](https://github.com/antialias/soroban-abacus-flashcards/commit/1e5467fad4e27b832300c49b4f73547dc47598b0))
|
||||
|
||||
## [4.35.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.34.0...v4.35.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** replace slider thumb with diamond-shaped abacus beads ([0fbde53](https://github.com/antialias/soroban-abacus-flashcards/commit/0fbde53039d3ea000c6a3be492b733479e7bf47c))
|
||||
|
||||
## [4.34.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.8...v4.34.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** redesign slider with abacus-themed beads ([f3dce84](https://github.com/antialias/soroban-abacus-flashcards/commit/f3dce84532fa706e4ec9551facde2055a060ee13))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** convert to Radix UI Slider with abacus theme ([a03e73c](https://github.com/antialias/soroban-abacus-flashcards/commit/a03e73c849c5da4337f26a74b8f12b617c66068e))
|
||||
|
||||
## [4.33.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.7...v4.33.8) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** reduce Dan scale and container height to prevent clipping ([563136f](https://github.com/antialias/soroban-abacus-flashcards/commit/563136fb79fa10b2af3a119bf0f861e3b0812b2e))
|
||||
* **levels:** reduce max scale factor to allow more compact container ([ead9ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/ead9ee9589aa4d7376e9385da5da53a6b444858a))
|
||||
|
||||
## [4.33.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.6...v4.33.7) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** reduce scale factor variation to minimize margin differences ([abb647c](https://github.com/antialias/soroban-abacus-flashcards/commit/abb647ce40b8f9d0c8268ab18c139324ae3195c5))
|
||||
|
||||
## [4.33.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.5...v4.33.6) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** increase container height to prevent abacus clipping ([cd5c15a](https://github.com/antialias/soroban-abacus-flashcards/commit/cd5c15aeb260c568fe7ad9b6a4f51c4d6498b2b8))
|
||||
|
||||
## [4.33.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.4...v4.33.5) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** revert delayed column change, keep overflow hidden ([22f00f5](https://github.com/antialias/soroban-abacus-flashcards/commit/22f00f59f5facc36a846408dcd196ec54ea676b1))
|
||||
|
||||
## [4.33.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.3...v4.33.4) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** stabilize slider position and prevent abacus clipping ([09004dc](https://github.com/antialias/soroban-abacus-flashcards/commit/09004dc2c055031ee2f71c964ceee6f7b1d42ecd))
|
||||
|
||||
## [4.33.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.2...v4.33.3) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** move slider into level display pane above abacus ([2d8bb4a](https://github.com/antialias/soroban-abacus-flashcards/commit/2d8bb4ab8804f399d1ccc8a18feff9f09eca8029))
|
||||
|
||||
## [4.33.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.1...v4.33.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add fixed height to entire level display pane ([200b26c](https://github.com/antialias/soroban-abacus-flashcards/commit/200b26c2cd35d1d637ede9dcfc3dbbc7f3f19320))
|
||||
|
||||
## [4.33.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.0...v4.33.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** only animate abacus, not container with background/border ([c80477d](https://github.com/antialias/soroban-abacus-flashcards/commit/c80477d24877ddada5f3f4405abbf05e1d753b5d))
|
||||
|
||||
## [4.33.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.32.1...v4.33.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** add BigInt support for 30-digit Dan level abacuses ([0ab4cc2](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab4cc288066b75a6ea4371f65098db5c0fc8847))
|
||||
* **levels:** add hover interaction and smooth React Spring transitions ([fd2b633](https://github.com/antialias/soroban-abacus-flashcards/commit/fd2b6338a84c3bbc683eff216a8da3b155749f0f))
|
||||
|
||||
## [4.32.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.32.0...v4.32.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use correct dark mode styling from homepage + docs update ([c38767f](https://github.com/antialias/soroban-abacus-flashcards/commit/c38767f4d399fa2caa5cd4e0185689d0207fbdaf))
|
||||
|
||||
## [4.32.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.1...v4.32.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add dark mode styling and responsive scaling to abacus ([92e1e62](https://github.com/antialias/soroban-abacus-flashcards/commit/92e1e621321039206f65b3605f5797bbdc6beafc))
|
||||
|
||||
## [4.31.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.0...v4.31.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use correct AbacusReact API with direct props ([892b377](https://github.com/antialias/soroban-abacus-flashcards/commit/892b377eb3bbd555dd2566bf58e946e9faa7b9f6))
|
||||
|
||||
## [4.31.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.30.0...v4.31.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** implement interactive slider for exploring kyu & dan ranks ([eb3b100](https://github.com/antialias/soroban-abacus-flashcards/commit/eb3b1000563536d4143ba1f4ec04e59e8dd2e608))
|
||||
|
||||
## [4.30.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.29.0...v4.30.0) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -138,12 +138,43 @@ See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
|
||||
- ✅ Use `useAbacusConfig` for abacus configuration
|
||||
- ✅ Use `useAbacusDisplay` for reading abacus state
|
||||
|
||||
**MANDATORY: Read the Docs Before Customizing**
|
||||
|
||||
**ALWAYS read the full README documentation before customizing or styling AbacusReact:**
|
||||
- Location: `packages/abacus-react/README.md`
|
||||
- Check homepage implementation: `src/app/page.tsx` (MiniAbacus component)
|
||||
- Check storybook examples: `src/stories/AbacusReact.*.stories.tsx`
|
||||
|
||||
**Key Documentation Points:**
|
||||
1. **Custom Styles**: Use `fill` (not just `stroke`) for columnPosts and reckoningBar
|
||||
2. **Props**: Use direct props like `value`, `columns`, `scaleFactor` (not config objects)
|
||||
3. **Example from Homepage:**
|
||||
```typescript
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
<AbacusReact
|
||||
value={123}
|
||||
columns={3}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```typescript
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
const config = useAbacusConfig({ columns: 5 })
|
||||
<AbacusReact config={config} initialNumber={123} />
|
||||
<AbacusReact value={123} columns={5} scaleFactor={1.5} showNumbers={true} />
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
@@ -192,3 +223,48 @@ Three places must handle settings correctly:
|
||||
3. **Validator** (`{Game}Validator.ts`) - `getInitialState()` must accept ALL settings
|
||||
|
||||
If a setting doesn't persist, check all three locations.
|
||||
|
||||
## Z-Index and Stacking Context Management
|
||||
|
||||
When working with z-index values or encountering layering issues, refer to:
|
||||
|
||||
- **`.claude/Z_INDEX_MANAGEMENT.md`** - Complete z-index documentation
|
||||
- Z-index layering hierarchy (0-20000+)
|
||||
- Stacking context rules and gotchas
|
||||
- Current z-index audit of all components
|
||||
- Guidelines for choosing z-index values
|
||||
- Migration plan to use constants file
|
||||
- Debugging checklist for layering issues
|
||||
|
||||
**Quick Reference:**
|
||||
|
||||
**ALWAYS use the constants file:**
|
||||
```typescript
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
|
||||
// ✅ Good
|
||||
zIndex: Z_INDEX.NAV_BAR
|
||||
zIndex: Z_INDEX.MODAL
|
||||
zIndex: Z_INDEX.TOOLTIP
|
||||
|
||||
// ❌ Bad - magic numbers!
|
||||
zIndex: 100
|
||||
zIndex: 10000
|
||||
zIndex: 500
|
||||
```
|
||||
|
||||
**Layering hierarchy:**
|
||||
- Base content: 0-99
|
||||
- Navigation/UI chrome: 100-999
|
||||
- Overlays/dropdowns/tooltips: 1000-9999
|
||||
- Modals/dialogs: 10000-19999
|
||||
- Toasts: 20000+
|
||||
|
||||
**Critical reminder about stacking contexts:**
|
||||
|
||||
Z-index values are only compared within the same stacking context! Elements with `position + zIndex`, `opacity < 1`, `transform`, or `filter` create new stacking contexts where child z-indexes are relative, not global.
|
||||
|
||||
Before setting a z-index, always check:
|
||||
1. What stacking context is this element in?
|
||||
2. Am I comparing against siblings or global elements?
|
||||
3. Does my parent create a stacking context?
|
||||
|
||||
392
apps/web/.claude/Z_INDEX_MANAGEMENT.md
Normal file
392
apps/web/.claude/Z_INDEX_MANAGEMENT.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Z-Index & Stacking Context Management
|
||||
|
||||
## Overview
|
||||
|
||||
This document tracks z-index values and stacking contexts across the application to prevent layering conflicts and make reasoning about visual hierarchy easy.
|
||||
|
||||
## The Z-Index Constants System
|
||||
|
||||
**Location:** `src/constants/zIndex.ts`
|
||||
|
||||
All z-index values should be defined in this file and imported where needed:
|
||||
|
||||
```typescript
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
|
||||
// Use it like this:
|
||||
zIndex: Z_INDEX.NAV_BAR
|
||||
zIndex: Z_INDEX.MODAL
|
||||
zIndex: Z_INDEX.GAME_NAV.HAMBURGER_MENU
|
||||
```
|
||||
|
||||
## Z-Index Layering Hierarchy
|
||||
|
||||
From lowest to highest:
|
||||
|
||||
| Layer | Range | Purpose | Examples |
|
||||
|-------|-------|---------|----------|
|
||||
| **Base Content** | 0-99 | Default page content, game elements | Background elements, game tracks, cards |
|
||||
| **Navigation & UI Chrome** | 100-999 | Fixed navigation, sticky headers | AppNavBar, page headers |
|
||||
| **Overlays & Dropdowns** | 1000-9999 | Tooltips, popovers, dropdowns, tutorial tooltips | Tutorial tooltips (50-100), ConfigForm (50), dropdowns (999-1000) |
|
||||
| **Modals & Dialogs** | 10000-19999 | Modal dialogs, confirmation dialogs | Modal backdrop (10000), Modal content (10001) |
|
||||
| **Top-Level Overlays** | 20000+ | Toasts, critical notifications | Toast notifications (20000) |
|
||||
|
||||
## Stacking Context Rules
|
||||
|
||||
### What Creates a Stacking Context?
|
||||
|
||||
These CSS properties create new stacking contexts (z-index values are relative within them):
|
||||
|
||||
1. `position: fixed` or `position: sticky` with z-index
|
||||
2. `position: absolute` or `position: relative` with z-index
|
||||
3. `opacity` < 1
|
||||
4. `transform` (any value)
|
||||
5. `filter` (any value except none)
|
||||
6. `isolation: isolate`
|
||||
|
||||
### Key Insight
|
||||
|
||||
**Z-index values are only compared within the same stacking context!**
|
||||
|
||||
If Element A creates a stacking context with `z-index: 1` and Element B is outside that context with `z-index: 999`, Element B will be on top regardless of child z-indexes inside Element A.
|
||||
|
||||
### Example
|
||||
|
||||
```tsx
|
||||
// Parent creates stacking context
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* This child's z-index is relative to parent, not global! */}
|
||||
<div style={{ position: 'absolute', zIndex: 999999 }}>
|
||||
I'm still under elements with zIndex: 2 outside my parent!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 2 }}>
|
||||
I'm on top of the z-index: 999999 element above!
|
||||
</div>
|
||||
```
|
||||
|
||||
## Current Z-Index Audit (2025-10-20)
|
||||
|
||||
### ✅ Using Z_INDEX Constants (Good!)
|
||||
|
||||
| Component | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| AppNavBar (Panda section) | `Z_INDEX.NAV_BAR` (100) | `src/components/AppNavBar.tsx:464` |
|
||||
| AppNavBar hamburger | `Z_INDEX.GAME_NAV.HAMBURGER_MENU` (9999) | `src/components/AppNavBar.tsx:165` |
|
||||
| AbacusDisplayDropdown | `Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN` (10000) | `src/components/AbacusDisplayDropdown.tsx:99` |
|
||||
|
||||
### ⚠️ Hardcoded Z-Index Values (Need Migration)
|
||||
|
||||
#### Critical Navigation Issues
|
||||
|
||||
| Component | Line | Value | Issue | Fix |
|
||||
|-----------|------|-------|-------|-----|
|
||||
| **AppNavBar (fixed section)** | 587 | `1000` | ❌ Should use `Z_INDEX.NAV_BAR` (100), but increased to 1000 to fix tutorial tooltip overlap | Define `TUTORIAL_TOOLTIP` in constants, set nav to proper layer |
|
||||
| AppNavBar (badge) | 645 | `50` | Should use constant | Add `Z_INDEX.BADGE` |
|
||||
|
||||
#### Tutorial System
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| TutorialPlayer | 643 | `50` | Tooltip container |
|
||||
| Tutorial shared/EditorComponents | 569, 590 | `50` | Tooltip button |
|
||||
| Tutorial shared/EditorComponents | 612 | `100` | Dropdown content (must be above tooltip) |
|
||||
| Tutorial decomposition CSS | 73 | `50` | Legacy CSS |
|
||||
| TutorialEditor | 65, 812, 2339 | `1000`, `10` | Various overlays |
|
||||
|
||||
#### Modals & Overlays
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| Modal (common) | 59 | `10000` | Modal backdrop |
|
||||
| ModerationPanel | 1994, 2009 | `10001`, `10002` | Moderation overlays |
|
||||
| ToastContext | 171 | `10001` | Toast notifications (should be 20000!) |
|
||||
| Join page | 35 | `10000` | Join page overlay |
|
||||
| EmojiPicker | 636 | `10000` | Emoji picker modal |
|
||||
|
||||
#### Dropdowns & Popovers
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| FormatSelectField | 115 | `999` | Dropdown |
|
||||
| DeploymentInfoModal | 37, 55 | `9998`, `9999` | Info modal layers |
|
||||
| RoomInfo | 338, 562 | `9999`, `10000` | Room tooltips |
|
||||
| GameTitleMenu | 119 | `9999` | Game menu |
|
||||
| PlayerTooltip | 69 | `9999` | Player tooltip |
|
||||
|
||||
#### Game Elements
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| Complement Race Game | Multiple | `0`, `1` | Base game layers |
|
||||
| Complement Race Track | 118, 140, 151 | `10`, `5`, `20` | Track, AI racers, player |
|
||||
| Complement Race HUD | 51, 106, 119, 137, 168 | `10`, `1000` | HUD elements |
|
||||
| GameCountdown | 58 | `1000` | Countdown overlay |
|
||||
| RouteCelebration | 31 | `9999` | Celebration overlay |
|
||||
| Matching GameCard | 203, 229, 243, 272, 386 | `9`, `10`, `-1`, `8`, `1` | Card layers |
|
||||
| Matching PlayerStatusBar | 154, 181, 202 | `10`, `10`, `5` | Status bars |
|
||||
|
||||
#### Misc UI
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| HeroAbacus | 89, 127, 163 | `10` | Hero section layers |
|
||||
| ChampionArena | 425, 514, 554, 614 | `10`, `1`, `1`, `10` | Arena layers |
|
||||
| NetworkPlayerIndicator | 118, 145, 169, 192, 275 | `-1`, `2`, `1`, `2`, `10` | Player avatars |
|
||||
| ConfigurationForm | 521, 502 | `50` | Config overlays |
|
||||
|
||||
## The Recent Bug: Tutorial Tooltips Over Nav Bar
|
||||
|
||||
**Problem:** Tutorial tooltips (z-index: 50, 100) were appearing over the navigation bar.
|
||||
|
||||
**Root Cause:**
|
||||
- Nav bar was using `Z_INDEX.NAV_BAR` = 100 in one place
|
||||
- But also hardcoded `zIndex: 30` in the fixed positioning section (line 587)
|
||||
- Tutorial tooltips use hardcoded `zIndex: 50` and `zIndex: 100`
|
||||
- Since 50 and 100 > 30, tooltips appeared on top
|
||||
|
||||
**Temporary Fix:** Increased nav bar's hardcoded value from 30 to 1000
|
||||
|
||||
**Proper Fix Needed:**
|
||||
1. Define tutorial tooltip z-indexes in constants file
|
||||
2. Update nav bar to consistently use `Z_INDEX.NAV_BAR`
|
||||
3. Ensure NAV_BAR > TUTORIAL_TOOLTIP in the hierarchy
|
||||
4. Consider: Should tutorial tooltips be in the 1000-9999 range (overlays) rather than 50-100?
|
||||
|
||||
## Guidelines for Choosing Z-Index Values
|
||||
|
||||
### 1. **Always Import and Use Z_INDEX Constants**
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
zIndex: Z_INDEX.NAV_BAR
|
||||
|
||||
// ❌ Bad
|
||||
zIndex: 100 // Magic number!
|
||||
```
|
||||
|
||||
### 2. **Add New Values to Constants File First**
|
||||
|
||||
Before using a new z-index value, add it to `src/constants/zIndex.ts`:
|
||||
|
||||
```typescript
|
||||
export const Z_INDEX = {
|
||||
// ... existing values ...
|
||||
|
||||
TUTORIAL: {
|
||||
TOOLTIP: 500, // Tutorial tooltips (overlays layer)
|
||||
DROPDOWN: 600, // Tutorial dropdown (above tooltip)
|
||||
},
|
||||
} as const
|
||||
```
|
||||
|
||||
### 3. **Choose the Right Layer**
|
||||
|
||||
Ask yourself:
|
||||
- Is this base content? → Use 0-99
|
||||
- Is this navigation/UI chrome? → Use 100-999
|
||||
- Is this a dropdown/tooltip/overlay? → Use 1000-9999
|
||||
- Is this a modal dialog? → Use 10000-19999
|
||||
- Is this a toast notification? → Use 20000+
|
||||
|
||||
### 4. **Understand Your Stacking Context**
|
||||
|
||||
Before setting z-index, ask:
|
||||
- What is my parent's stacking context?
|
||||
- Am I comparing against siblings or global elements?
|
||||
- Does my element create a new stacking context?
|
||||
|
||||
### 5. **Document Special Cases**
|
||||
|
||||
If you must deviate from the constants, document why:
|
||||
|
||||
```typescript
|
||||
// HACK: Needs to be above tutorial tooltips (50) but below modals (10000)
|
||||
// TODO: Migrate to Z_INDEX.TUTORIAL.TOOLTIP system
|
||||
zIndex: 100
|
||||
```
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Update Constants File ✅ TODO
|
||||
|
||||
Add missing constants to `src/constants/zIndex.ts`:
|
||||
|
||||
```typescript
|
||||
export const Z_INDEX = {
|
||||
// Base content layer (0-99)
|
||||
BASE: 0,
|
||||
CONTENT: 1,
|
||||
HERO_SECTION: 10, // Hero abacus components
|
||||
|
||||
// Game content layers (0-99)
|
||||
GAME_CONTENT: {
|
||||
TRACK: 0,
|
||||
CONTROLS: 1,
|
||||
RACER_AI: 5,
|
||||
RACER_PLAYER: 10,
|
||||
RACER_FLAG: 20,
|
||||
HUD: 50,
|
||||
},
|
||||
|
||||
// Navigation and UI chrome (100-999)
|
||||
NAV_BAR: 1000, // ⚠️ Currently needs to be 1000 due to tutorial tooltips
|
||||
STICKY_HEADER: 100,
|
||||
BADGE: 50,
|
||||
|
||||
// Overlays and dropdowns (1000-9999)
|
||||
TUTORIAL: {
|
||||
TOOLTIP: 500, // Tutorial tooltips
|
||||
DROPDOWN: 600, // Tutorial dropdowns (must be > tooltip)
|
||||
EDITOR: 700, // Tutorial editor
|
||||
},
|
||||
DROPDOWN: 1000,
|
||||
TOOLTIP: 1000,
|
||||
POPOVER: 1000,
|
||||
CONFIG_FORM: 1000,
|
||||
PLAYER_TOOLTIP: 1000,
|
||||
GAME_COUNTDOWN: 1000,
|
||||
|
||||
// High overlays (9000-9999)
|
||||
CELEBRATION: 9000,
|
||||
INFO_MODAL: 9998,
|
||||
|
||||
// Modal and dialog layers (10000-19999)
|
||||
MODAL_BACKDROP: 10000,
|
||||
MODAL: 10001,
|
||||
MODERATION_PANEL: 10001,
|
||||
EMOJI_PICKER: 10000,
|
||||
|
||||
// Top-level overlays (20000+)
|
||||
TOAST: 20000,
|
||||
|
||||
// Special navigation layers for game pages
|
||||
GAME_NAV: {
|
||||
HAMBURGER_MENU: 9999,
|
||||
HAMBURGER_NESTED_DROPDOWN: 10000,
|
||||
},
|
||||
} as const
|
||||
```
|
||||
|
||||
### Phase 2: Migrate High-Priority Components
|
||||
|
||||
Priority order:
|
||||
1. **Navigation components** (AppNavBar, etc.) - most critical for user experience
|
||||
2. **Tutorial system** (TutorialPlayer, tooltips) - currently conflicting
|
||||
3. **Modals and overlays** - ensure they're always on top
|
||||
4. **Game HUDs** - ensure proper layering
|
||||
5. **Everything else**
|
||||
|
||||
### Phase 3: Add Linting Rule
|
||||
|
||||
Consider adding an ESLint rule to prevent raw z-index numbers:
|
||||
|
||||
```javascript
|
||||
// Warn when zIndex is used with a number literal
|
||||
'no-magic-numbers': ['warn', {
|
||||
ignore: [0, 1, -1],
|
||||
ignoreArrayIndexes: true,
|
||||
enforceConst: true,
|
||||
}]
|
||||
```
|
||||
|
||||
## Debugging Z-Index Issues
|
||||
|
||||
### Checklist
|
||||
|
||||
When elements aren't layering correctly:
|
||||
|
||||
1. **Check the value**
|
||||
- [ ] What z-index does each element have?
|
||||
- [ ] Are they using constants or magic numbers?
|
||||
|
||||
2. **Check the stacking context**
|
||||
- [ ] What are the parent elements?
|
||||
- [ ] Do any parents create stacking contexts? (position + z-index, opacity, transform, etc.)
|
||||
- [ ] Are we comparing siblings or elements in different contexts?
|
||||
|
||||
3. **Verify the DOM hierarchy**
|
||||
- [ ] Use browser DevTools to inspect the DOM tree
|
||||
- [ ] Check the "Layers" panel in Chrome DevTools
|
||||
- [ ] Look for transforms, opacity, filters on parent elements
|
||||
|
||||
4. **Test the fix**
|
||||
- [ ] Does the fix work in all scenarios?
|
||||
- [ ] Did we introduce new conflicts?
|
||||
- [ ] Should we update the constants file?
|
||||
|
||||
### DevTools Tips
|
||||
|
||||
**Chrome DevTools:**
|
||||
1. Open DevTools → More Tools → Layers
|
||||
2. Select an element and see its stacking context
|
||||
3. View the 3D layer composition
|
||||
|
||||
**Firefox DevTools:**
|
||||
1. Inspector → Layout → scroll to "Z-index"
|
||||
2. Shows the stacking context parent
|
||||
|
||||
## Examples
|
||||
|
||||
### Good: Using Constants
|
||||
|
||||
```typescript
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
|
||||
export function MyTooltip() {
|
||||
return (
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: Z_INDEX.TOOLTIP, // ✅ Clear and maintainable
|
||||
})}>
|
||||
Tooltip content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Bad: Magic Numbers
|
||||
|
||||
```typescript
|
||||
export function MyTooltip() {
|
||||
return (
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: 500, // ❌ Where did 500 come from? How does it relate to other elements?
|
||||
})}>
|
||||
Tooltip content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Good: Documenting Stacking Context
|
||||
|
||||
```typescript
|
||||
// Creates a new stacking context for card contents
|
||||
<div className={css({
|
||||
position: 'relative',
|
||||
zIndex: Z_INDEX.BASE,
|
||||
transform: 'translateZ(0)', // ⚠️ Creates stacking context!
|
||||
})}>
|
||||
{/* Child z-indexes are relative to this context */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: Z_INDEX.CONTENT, // Relative to parent, not global
|
||||
})}>
|
||||
Card face
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [MDN: CSS Stacking Context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context)
|
||||
- [What The Heck, z-index??](https://www.joshwcomeau.com/css/stacking-contexts/) by Josh Comeau
|
||||
- [Z-Index Playground](https://thirumanikandan.com/posts/learn-z-index-using-a-visualization-tool)
|
||||
|
||||
## Last Updated
|
||||
|
||||
2025-10-20 - Initial audit and documentation created
|
||||
@@ -103,7 +103,8 @@
|
||||
"Bash(node -e:*)",
|
||||
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
|
||||
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')",
|
||||
"Bash(git rev-parse HEAD)"
|
||||
"Bash(git rev-parse HEAD)",
|
||||
"Bash(gh run watch --exit-status 18662351595)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"@tanstack/react-form": "^0.19.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
|
||||
@@ -16,7 +16,11 @@ function GamesPageContent() {
|
||||
const router = useRouter()
|
||||
|
||||
// Get all players sorted by creation time
|
||||
const allPlayers = getAllPlayers().sort((a, b) => a.createdAt - b.createdAt)
|
||||
const allPlayers = getAllPlayers().sort((a, b) => {
|
||||
const aTime = a.createdAt instanceof Date ? a.createdAt.getTime() : a.createdAt
|
||||
const bTime = b.createdAt instanceof Date ? b.createdAt.getTime() : b.createdAt
|
||||
return aTime - bTime
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,190 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { LevelSliderDisplay } from '@/components/LevelSliderDisplay'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { container, stack } from '../../../styled-system/patterns'
|
||||
|
||||
// Kyu level data from the Japan Abacus Federation
|
||||
const kyuLevels = [
|
||||
{ level: '10th Kyu', emoji: '🧒', color: 'green', digits: 2 },
|
||||
{ level: '9th Kyu', emoji: '🧒', color: 'green', digits: 2 },
|
||||
{ level: '8th Kyu', emoji: '🧒', color: 'green', digits: 3 },
|
||||
{ level: '7th Kyu', emoji: '🧒', color: 'green', digits: 4 },
|
||||
{ level: '6th Kyu', emoji: '🧑', color: 'blue', digits: 5 },
|
||||
{ level: '5th Kyu', emoji: '🧑', color: 'blue', digits: 6 },
|
||||
{ level: '4th Kyu', emoji: '🧑', color: 'blue', digits: 7 },
|
||||
{ level: '3rd Kyu', emoji: '🧔', color: 'violet', digits: 8 },
|
||||
{ level: '2nd Kyu', emoji: '🧔', color: 'violet', digits: 9 },
|
||||
{ level: '1st Kyu', emoji: '🧔', color: 'violet', digits: 10 },
|
||||
] as const
|
||||
|
||||
// Dan level data - all use 30-digit calculations
|
||||
const danLevels = [
|
||||
{ level: 'Pre-1st Dan', name: 'Jun-Shodan', minScore: 90, emoji: '🧙', digits: 30 },
|
||||
{ level: '1st Dan', name: 'Shodan', minScore: 100, emoji: '🧙', digits: 30 },
|
||||
{ level: '2nd Dan', name: 'Nidan', minScore: 120, emoji: '🧙♂️', digits: 30 },
|
||||
{ level: '3rd Dan', name: 'Sandan', minScore: 140, emoji: '🧙♂️', digits: 30 },
|
||||
{ level: '4th Dan', name: 'Yondan', minScore: 160, emoji: '🧙♀️', digits: 30 },
|
||||
{ level: '5th Dan', name: 'Godan', minScore: 180, emoji: '🧙♀️', digits: 30 },
|
||||
{ level: '6th Dan', name: 'Rokudan', minScore: 200, emoji: '🧝', digits: 30 },
|
||||
{ level: '7th Dan', name: 'Nanadan', minScore: 220, emoji: '🧝', digits: 30 },
|
||||
{ level: '8th Dan', name: 'Hachidan', minScore: 250, emoji: '🧝♂️', digits: 30 },
|
||||
{ level: '9th Dan', name: 'Kudan', minScore: 270, emoji: '🧝♀️', digits: 30 },
|
||||
{ level: '10th Dan', name: 'Judan', minScore: 290, emoji: '👑', digits: 30 },
|
||||
] as const
|
||||
|
||||
// Compact abacus column component
|
||||
function AbacusColumn({ color }: { color: string }) {
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '0.5',
|
||||
w: '2',
|
||||
})}
|
||||
>
|
||||
{/* Top bead */}
|
||||
<div
|
||||
className={css({
|
||||
w: '2',
|
||||
h: '2',
|
||||
rounded: 'full',
|
||||
bg:
|
||||
color === 'green'
|
||||
? 'green.500'
|
||||
: color === 'blue'
|
||||
? 'blue.500'
|
||||
: color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
opacity: 0.7,
|
||||
})}
|
||||
/>
|
||||
{/* Divider */}
|
||||
<div className={css({ w: '2', h: '0.5', bg: 'gray.600' })} />
|
||||
{/* Bottom beads */}
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
w: '2',
|
||||
h: '2',
|
||||
rounded: 'full',
|
||||
bg:
|
||||
color === 'green'
|
||||
? 'green.500'
|
||||
: color === 'blue'
|
||||
? 'blue.500'
|
||||
: color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
opacity: 0.7,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Level card component for the slider
|
||||
function LevelCard({
|
||||
level,
|
||||
emoji,
|
||||
color,
|
||||
digits,
|
||||
subtitle,
|
||||
}: {
|
||||
level: string
|
||||
emoji: string
|
||||
color: string
|
||||
digits: number
|
||||
subtitle?: string
|
||||
}) {
|
||||
// Limit display for very high digit counts
|
||||
const displayDigits = Math.min(digits, 15)
|
||||
const showEllipsis = digits > 15
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
minW: { base: '64', md: '80' },
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
color === 'green'
|
||||
? 'green.500'
|
||||
: color === 'blue'
|
||||
? 'blue.500'
|
||||
: color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
rounded: 'xl',
|
||||
p: { base: '4', md: '6' },
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
transform: 'translateY(-4px)',
|
||||
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={css({ textAlign: 'center', mb: '4' })}>
|
||||
<div className={css({ fontSize: { base: '3xl', md: '4xl' }, mb: '2' })}>{emoji}</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
color === 'green'
|
||||
? 'green.400'
|
||||
: color === 'blue'
|
||||
? 'blue.400'
|
||||
: color === 'violet'
|
||||
? 'violet.400'
|
||||
: 'amber.400',
|
||||
mb: '1',
|
||||
})}
|
||||
>
|
||||
{level}
|
||||
</div>
|
||||
{subtitle && <div className={css({ fontSize: 'sm', color: 'gray.400' })}>{subtitle}</div>}
|
||||
</div>
|
||||
|
||||
{/* Abacus Visualization */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.5',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
p: '4',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
minH: '32',
|
||||
})}
|
||||
>
|
||||
{Array.from({ length: displayDigits }).map((_, i) => (
|
||||
<AbacusColumn key={i} color={color} />
|
||||
))}
|
||||
{showEllipsis && (
|
||||
<div className={css({ color: 'gray.500', fontSize: '2xl', px: '2' })}>...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Digit count label */}
|
||||
<div className={css({ textAlign: 'center', mt: '3', fontSize: 'sm', color: 'gray.400' })}>
|
||||
{digits} {digits === 1 ? 'digit' : 'digits'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LevelsPage() {
|
||||
return (
|
||||
<PageWithNav navTitle="Kyu & Dan Levels" navEmoji="📊">
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh', pb: '12' })}>
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className={css({
|
||||
@@ -207,7 +31,13 @@ export default function LevelsPage() {
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
|
||||
<div
|
||||
className={container({
|
||||
maxW: '6xl',
|
||||
px: '4',
|
||||
position: 'relative',
|
||||
})}
|
||||
>
|
||||
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
|
||||
<h1
|
||||
className={css({
|
||||
@@ -233,123 +63,18 @@ export default function LevelsPage() {
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
Explore the progression from beginner to master
|
||||
Slide through the complete progression from beginner to master
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content container */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
|
||||
{/* Journey Slider */}
|
||||
{/* Main content */}
|
||||
<div className={container({ maxW: '6xl', px: '4', py: '12' })}>
|
||||
<section className={stack({ gap: '8' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
The Complete Journey
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', mb: '8' })}>
|
||||
Slide through all ranks from 10th Kyu to 10th Dan
|
||||
</p>
|
||||
</div>
|
||||
<LevelSliderDisplay />
|
||||
|
||||
{/* Horizontal Slider */}
|
||||
<div
|
||||
className={css({
|
||||
w: '100%',
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
pb: '4',
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', gap: '4', pb: '2' })}>
|
||||
{/* Kyu Levels */}
|
||||
{kyuLevels.map((kyu, index) => (
|
||||
<LevelCard
|
||||
key={index}
|
||||
level={kyu.level}
|
||||
emoji={kyu.emoji}
|
||||
color={kyu.color}
|
||||
digits={kyu.digits}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Transition marker */}
|
||||
<div
|
||||
className={css({
|
||||
minW: '20',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '4xl',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
|
||||
{/* Dan Levels */}
|
||||
{danLevels.map((dan, index) => (
|
||||
<LevelCard
|
||||
key={index}
|
||||
level={dan.level}
|
||||
emoji={dan.emoji}
|
||||
color="amber"
|
||||
digits={dan.digits}
|
||||
subtitle={`${dan.minScore}+ pts`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6',
|
||||
justifyContent: 'center',
|
||||
mt: '6',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'green.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Beginner (10-7 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'blue.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Intermediate (6-4 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'violet.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Advanced (3-1 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'amber.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Master (Dan ranks)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Additional Information Section */}
|
||||
<section className={stack({ gap: '8', mt: '16', pb: '12' })}>
|
||||
{/* Info Section */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
@@ -382,21 +107,6 @@ export default function LevelsPage() {
|
||||
levels all require mastery of 30-digit calculations, with ranks awarded based on
|
||||
exam scores.
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
mt: '4',
|
||||
pt: '4',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
Note: This page provides information about the official Japanese ranking system
|
||||
for educational purposes. This application does not administer official
|
||||
examinations or certifications.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 1, // Single player only
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
|
||||
...getGameTheme('teal'),
|
||||
...getGameTheme('green'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 4,
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
|
||||
...getGameTheme('purple'),
|
||||
...getGameTheme('pink'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 8,
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
|
||||
...getGameTheme('blue'),
|
||||
...getGameTheme('purple'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useMemo, useState } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack } from '../../styled-system/patterns'
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
import { useFullscreen } from '../contexts/FullscreenContext'
|
||||
import { getRandomSubtitle } from '../data/abaciOneSubtitles'
|
||||
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
|
||||
|
||||
// Import HomeHeroContext for optional usage
|
||||
import type { Subtitle } from '../data/abaciOneSubtitles'
|
||||
|
||||
type HomeHeroContextValue = {
|
||||
subtitle: Subtitle
|
||||
isHeroVisible: boolean
|
||||
} | null
|
||||
|
||||
// HomeHeroContext - imported dynamically to avoid circular deps
|
||||
let HomeHeroContextModule: any = null
|
||||
try {
|
||||
HomeHeroContextModule = require('../contexts/HomeHeroContext')
|
||||
} catch {
|
||||
// Context not available
|
||||
}
|
||||
|
||||
const HomeHeroContext: React.Context<HomeHeroContextValue> =
|
||||
HomeHeroContextModule?.HomeHeroContext || React.createContext<HomeHeroContextValue>(null)
|
||||
|
||||
// Use HomeHeroContext without requiring it
|
||||
function useOptionalHomeHero(): HomeHeroContextValue {
|
||||
return useContext(HomeHeroContext)
|
||||
}
|
||||
|
||||
interface AppNavBarProps {
|
||||
variant?: 'full' | 'minimal'
|
||||
navSlot?: React.ReactNode
|
||||
@@ -514,6 +540,17 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
const isArcadePage = pathname?.startsWith('/arcade')
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
|
||||
|
||||
// Try to get home hero context (if on homepage)
|
||||
const homeHero = useOptionalHomeHero()
|
||||
|
||||
// Select a random subtitle once on mount (performance: won't change on re-renders)
|
||||
// Use homeHero subtitle if available, otherwise generate one
|
||||
const fallbackSubtitle = useMemo(() => getRandomSubtitle(), [])
|
||||
const subtitle = homeHero?.subtitle || fallbackSubtitle
|
||||
|
||||
// Show branding unless we're on homepage with visible hero
|
||||
const showBranding = !homeHero || !homeHero.isHeroVisible
|
||||
|
||||
// Auto-detect variant based on context
|
||||
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
|
||||
|
||||
@@ -532,54 +569,137 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we should use transparent styling (when hero is visible)
|
||||
const isTransparent = homeHero?.isHeroVisible
|
||||
|
||||
return (
|
||||
<header
|
||||
className={css({
|
||||
bg: 'white',
|
||||
shadow: 'sm',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 30,
|
||||
})}
|
||||
>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
textDecoration: 'none',
|
||||
_hover: { color: 'brand.900' },
|
||||
})}
|
||||
>
|
||||
🧮 Soroban Generator
|
||||
</Link>
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<header
|
||||
className={css({
|
||||
bg: isTransparent ? 'transparent' : 'rgba(0, 0, 0, 0.5)',
|
||||
backdropFilter: isTransparent ? 'none' : 'blur(12px)',
|
||||
shadow: isTransparent ? 'none' : 'lg',
|
||||
borderBottom: isTransparent ? 'none' : '1px solid',
|
||||
borderColor: isTransparent ? 'transparent' : 'rgba(139, 92, 246, 0.2)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
transition: 'all 0.3s ease',
|
||||
})}
|
||||
>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
{/* Logo - conditionally shown based on hero visibility */}
|
||||
{showBranding ? (
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0',
|
||||
textDecoration: 'none',
|
||||
_hover: { '& > .brand-name': { color: 'rgba(196, 181, 253, 1)' } },
|
||||
opacity: 0,
|
||||
animation: 'fadeIn 0.3s ease-out forwards',
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
})}
|
||||
>
|
||||
Abaci One
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'medium',
|
||||
color: 'rgba(196, 181, 253, 0.8)',
|
||||
fontStyle: 'italic',
|
||||
cursor: 'help',
|
||||
whiteSpace: 'nowrap',
|
||||
_hover: { color: 'rgba(196, 181, 253, 1)' },
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className={css({
|
||||
bg: 'gray.900',
|
||||
color: 'white',
|
||||
px: '3',
|
||||
py: '2',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm',
|
||||
maxW: '250px',
|
||||
shadow: 'lg',
|
||||
zIndex: 50,
|
||||
})}
|
||||
>
|
||||
{subtitle.description}
|
||||
<Tooltip.Arrow
|
||||
className={css({
|
||||
fill: 'gray.900',
|
||||
})}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
<div className={hstack({ gap: '6', alignItems: 'center' })}>
|
||||
{/* Navigation Links */}
|
||||
<nav className={hstack({ gap: '4' })}>
|
||||
<NavLink href="/create" currentPath={pathname}>
|
||||
Create
|
||||
</NavLink>
|
||||
<NavLink href="/guide" currentPath={pathname}>
|
||||
Guide
|
||||
</NavLink>
|
||||
<NavLink href="/games" currentPath={pathname}>
|
||||
Games
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div className={hstack({ gap: '6', alignItems: 'center' })}>
|
||||
{/* Navigation Links */}
|
||||
<nav className={hstack({ gap: '4' })}>
|
||||
<NavLink href="/create" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Create
|
||||
</NavLink>
|
||||
<NavLink href="/guide" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Guide
|
||||
</NavLink>
|
||||
<NavLink href="/games" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Games
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown isFullscreen={false} />
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown isFullscreen={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
{/* Keyframes for fade-in animation */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -587,16 +707,21 @@ function NavLink({
|
||||
href,
|
||||
currentPath,
|
||||
children,
|
||||
isTransparent,
|
||||
}: {
|
||||
href: string
|
||||
currentPath: string | null
|
||||
children: React.ReactNode
|
||||
isTransparent?: boolean
|
||||
}) {
|
||||
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
backdropFilter: isTransparent ? 'blur(8px)' : 'none',
|
||||
}}
|
||||
className={css({
|
||||
px: { base: '4', md: '3' },
|
||||
py: { base: '3', md: '2' },
|
||||
@@ -604,17 +729,38 @@ function NavLink({
|
||||
minW: { base: '44px', md: 'auto' },
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: isActive ? 'brand.700' : 'gray.600',
|
||||
bg: isActive ? 'brand.50' : 'transparent',
|
||||
color: isTransparent
|
||||
? isActive
|
||||
? 'white'
|
||||
: 'rgba(255, 255, 255, 0.8)'
|
||||
: isActive
|
||||
? 'rgba(196, 181, 253, 1)'
|
||||
: 'rgba(209, 213, 219, 0.9)',
|
||||
bg: isTransparent
|
||||
? isActive
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.08)'
|
||||
: isActive
|
||||
? 'rgba(139, 92, 246, 0.2)'
|
||||
: 'transparent',
|
||||
border: isTransparent ? '1px solid' : 'none',
|
||||
borderColor: isTransparent
|
||||
? isActive
|
||||
? 'rgba(255, 255, 255, 0.3)'
|
||||
: 'rgba(255, 255, 255, 0.15)'
|
||||
: 'transparent',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: isTransparent ? '0 2px 8px rgba(0, 0, 0, 0.2)' : 'none',
|
||||
_hover: {
|
||||
color: isActive ? 'brand.800' : 'gray.900',
|
||||
bg: isActive ? 'brand.100' : 'gray.50',
|
||||
color: isTransparent ? 'white' : 'rgba(196, 181, 253, 1)',
|
||||
bg: isTransparent ? 'rgba(255, 255, 255, 0.25)' : 'rgba(139, 92, 246, 0.25)',
|
||||
borderColor: isTransparent ? 'rgba(255, 255, 255, 0.4)' : 'transparent',
|
||||
boxShadow: isTransparent ? '0 4px 12px rgba(0, 0, 0, 0.3)' : 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
||||
189
apps/web/src/components/HeroAbacus.tsx
Normal file
189
apps/web/src/components/HeroAbacus.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useHomeHero } from '../contexts/HomeHeroContext'
|
||||
|
||||
export function HeroAbacus() {
|
||||
const {
|
||||
subtitle,
|
||||
abacusValue,
|
||||
setAbacusValue,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
} = useHomeHero()
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
// Detect when hero scrolls out of view
|
||||
useEffect(() => {
|
||||
if (!heroRef.current) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
// Hero is visible if more than 20% is in viewport
|
||||
setIsHeroVisible(entry.intersectionRatio > 0.2)
|
||||
},
|
||||
{
|
||||
threshold: [0, 0.2, 0.5, 1],
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(heroRef.current)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [setIsHeroVisible])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={heroRef}
|
||||
className={css({
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
bg: 'gray.900',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
px: '4',
|
||||
py: '12',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.1,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
|
||||
backgroundSize: '40px 40px',
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Title and Subtitle Section - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
zIndex: 10,
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '4xl', md: '6xl', lg: '7xl' },
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
})}
|
||||
>
|
||||
Abaci One
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'xl', md: '2xl' },
|
||||
fontWeight: 'medium',
|
||||
color: 'purple.300',
|
||||
fontStyle: 'italic',
|
||||
marginBottom: '8',
|
||||
opacity: isSubtitleLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Large Interactive Abacus - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: '1',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
opacity: isAbacusLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
transform: { base: 'scale(3.5)', md: 'scale(3.5)', lg: 'scale(4.25)' },
|
||||
transformOrigin: 'center center',
|
||||
transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
value={abacusValue}
|
||||
columns={4}
|
||||
beadShape={appConfig.beadShape}
|
||||
showNumbers={true}
|
||||
interactive={true}
|
||||
animated={true}
|
||||
customStyles={structuralStyles}
|
||||
onValueChange={setAbacusValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle hint to scroll - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
animation: 'bounce 2s ease-in-out infinite',
|
||||
zIndex: 10,
|
||||
})}
|
||||
>
|
||||
<span>Scroll to explore</span>
|
||||
<span>↓</span>
|
||||
</div>
|
||||
|
||||
{/* Keyframes for bounce animation */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
347
apps/web/src/components/InteractiveFlashcards.tsx
Normal file
347
apps/web/src/components/InteractiveFlashcards.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
'use client'
|
||||
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
|
||||
interface Flashcard {
|
||||
id: number
|
||||
number: number
|
||||
initialX: number
|
||||
initialY: number
|
||||
initialRotation: number
|
||||
zIndex: number
|
||||
}
|
||||
|
||||
/**
|
||||
* InteractiveFlashcards - A fun flashcard display where you can drag cards around
|
||||
* Cards stay where you drop them - simple and intuitive
|
||||
*/
|
||||
export function InteractiveFlashcards() {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [cards, setCards] = useState<Flashcard[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
// Double rAF pattern - ensures layout is fully complete
|
||||
const frameId1 = requestAnimationFrame(() => {
|
||||
const frameId2 = requestAnimationFrame(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
const containerWidth = containerRef.current.offsetWidth
|
||||
const containerHeight = containerRef.current.offsetHeight
|
||||
|
||||
// Only generate cards once we have proper dimensions
|
||||
if (containerWidth < 100 || containerHeight < 100) {
|
||||
return
|
||||
}
|
||||
|
||||
const count = Math.floor(Math.random() * 8) + 8 // 8-15 cards
|
||||
const generated: Flashcard[] = []
|
||||
|
||||
// Position cards within the actual container bounds
|
||||
const cardWidth = 120 // approximate card width
|
||||
const cardHeight = 200 // approximate card height
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const card = {
|
||||
id: i,
|
||||
number: Math.floor(Math.random() * 900) + 100, // 100-999
|
||||
initialX: Math.random() * (containerWidth - cardWidth - 40) + 20,
|
||||
initialY: Math.random() * (containerHeight - cardHeight - 40) + 20,
|
||||
initialRotation: Math.random() * 40 - 20, // -20 to 20 degrees
|
||||
zIndex: i,
|
||||
}
|
||||
generated.push(card)
|
||||
}
|
||||
|
||||
setCards(generated)
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
// Note: can't cancel nested rAF properly, but component cleanup will prevent state updates
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={css({
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxW: '1200px',
|
||||
mx: 'auto',
|
||||
height: { base: '400px', md: '500px' },
|
||||
overflow: 'visible',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'xl',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
})}
|
||||
>
|
||||
{cards.map((card) => (
|
||||
<DraggableCard key={card.id} card={card} containerRef={containerRef} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface DraggableCardProps {
|
||||
card: Flashcard
|
||||
containerRef: React.RefObject<HTMLDivElement>
|
||||
}
|
||||
|
||||
function DraggableCard({ card, containerRef }: DraggableCardProps) {
|
||||
// Track position - starts at initial, updates when dragged
|
||||
const [position, setPosition] = useState({ x: card.initialX, y: card.initialY })
|
||||
const [rotation, setRotation] = useState(card.initialRotation) // Now dynamic!
|
||||
const [zIndex, setZIndex] = useState(card.zIndex)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragSpeed, setDragSpeed] = useState(0) // Speed for dynamic shadow
|
||||
|
||||
// Track drag state
|
||||
const dragStartRef = useRef<{ x: number; y: number; cardX: number; cardY: number } | null>(null)
|
||||
const grabOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) // Offset from card center where grabbed
|
||||
const baseRotationRef = useRef(card.initialRotation) // Starting rotation
|
||||
const lastMoveTimeRef = useRef<number>(0)
|
||||
const lastMovePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const lastLogTimeRef = useRef<number>(0) // Separate throttling for logging
|
||||
const cardRef = useRef<HTMLDivElement>(null) // Reference to card element
|
||||
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
setIsDragging(true)
|
||||
setZIndex(1000) // Bring to front
|
||||
setDragSpeed(0)
|
||||
|
||||
// Capture the pointer
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
|
||||
// Record where the drag started (pointer position and card position)
|
||||
dragStartRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
cardX: position.x,
|
||||
cardY: position.y,
|
||||
}
|
||||
|
||||
// Calculate grab offset from card center
|
||||
if (cardRef.current) {
|
||||
const rect = cardRef.current.getBoundingClientRect()
|
||||
const cardCenterX = rect.left + rect.width / 2
|
||||
const cardCenterY = rect.top + rect.height / 2
|
||||
grabOffsetRef.current = {
|
||||
x: e.clientX - cardCenterX,
|
||||
y: e.clientY - cardCenterY,
|
||||
}
|
||||
console.log(
|
||||
`[GrabPoint] Grabbed at offset: (${grabOffsetRef.current.x.toFixed(0)}, ${grabOffsetRef.current.y.toFixed(0)})px from center`
|
||||
)
|
||||
}
|
||||
|
||||
// Store the current rotation as the base for this drag
|
||||
baseRotationRef.current = rotation
|
||||
|
||||
// Initialize velocity tracking
|
||||
const now = Date.now()
|
||||
lastMoveTimeRef.current = now
|
||||
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
|
||||
lastLogTimeRef.current = now
|
||||
|
||||
console.log('[Shadow] Drag started, speed reset to 0')
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (!isDragging || !dragStartRef.current) return
|
||||
|
||||
// Calculate how far the pointer has moved since drag started
|
||||
const deltaX = e.clientX - dragStartRef.current.x
|
||||
const deltaY = e.clientY - dragStartRef.current.y
|
||||
|
||||
// Calculate velocity for dynamic shadow
|
||||
const now = Date.now()
|
||||
const timeDelta = now - lastMoveTimeRef.current
|
||||
|
||||
if (timeDelta > 0) {
|
||||
// Distance moved since last frame
|
||||
const distX = e.clientX - lastMovePositionRef.current.x
|
||||
const distY = e.clientY - lastMovePositionRef.current.y
|
||||
const distance = Math.sqrt(distX * distX + distY * distY)
|
||||
|
||||
// Speed in pixels per millisecond, then convert to reasonable scale
|
||||
const speed = distance / timeDelta
|
||||
const scaledSpeed = Math.min(speed * 100, 100) // Cap at 100 for reasonable shadow size
|
||||
|
||||
setDragSpeed(scaledSpeed)
|
||||
|
||||
// Log occasionally (every ~200ms) to avoid console spam
|
||||
const timeSinceLastLog = now - lastLogTimeRef.current
|
||||
if (timeSinceLastLog > 200) {
|
||||
console.log(
|
||||
`[Shadow] Speed: ${scaledSpeed.toFixed(1)}, distance: ${distance.toFixed(0)}px, timeDelta: ${timeDelta}ms`
|
||||
)
|
||||
lastLogTimeRef.current = now
|
||||
}
|
||||
|
||||
lastMoveTimeRef.current = now
|
||||
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
// Calculate rotation based on grab point physics
|
||||
// Cross product of grab offset and drag direction determines rotation
|
||||
// If grabbed on left and dragged right → clockwise rotation
|
||||
// If grabbed on right and dragged left → counter-clockwise rotation
|
||||
const crossProduct = grabOffsetRef.current.x * deltaY - grabOffsetRef.current.y * deltaX
|
||||
const rotationInfluence = crossProduct / 500 // Reduced scale factor for more visible rotation
|
||||
const newRotation = baseRotationRef.current + rotationInfluence
|
||||
|
||||
// Clamp rotation to prevent excessive spinning
|
||||
const clampedRotation = Math.max(-45, Math.min(45, newRotation))
|
||||
setRotation(clampedRotation)
|
||||
|
||||
// Log rotation changes occasionally (same throttle as shadow logging)
|
||||
const timeSinceLastLog = now - lastLogTimeRef.current
|
||||
if (timeSinceLastLog > 200) {
|
||||
console.log(
|
||||
`[GrabPoint] Rotation: ${clampedRotation.toFixed(1)}° (influence: ${rotationInfluence.toFixed(1)}°, cross: ${crossProduct.toFixed(0)})`
|
||||
)
|
||||
}
|
||||
|
||||
// Update card position - keep grab point under cursor while rotating
|
||||
// Calculate the rotated grab offset
|
||||
const rotationRad = (clampedRotation * Math.PI) / 180
|
||||
const cosRot = Math.cos(rotationRad)
|
||||
const sinRot = Math.sin(rotationRad)
|
||||
|
||||
// Rotate the grab offset by the current rotation angle
|
||||
const rotatedGrabX = grabOffsetRef.current.x * cosRot - grabOffsetRef.current.y * sinRot
|
||||
const rotatedGrabY = grabOffsetRef.current.x * sinRot + grabOffsetRef.current.y * cosRot
|
||||
|
||||
// Get container bounds for coordinate conversion
|
||||
if (!containerRef.current || !cardRef.current) {
|
||||
// Fallback to simple delta if refs not ready
|
||||
setPosition({
|
||||
x: dragStartRef.current.cardX + deltaX,
|
||||
y: dragStartRef.current.cardY + deltaY,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const containerRect = containerRef.current.getBoundingClientRect()
|
||||
const cardRect = cardRef.current.getBoundingClientRect()
|
||||
|
||||
// Current cursor position in viewport space
|
||||
const cursorViewportX = e.clientX
|
||||
const cursorViewportY = e.clientY
|
||||
|
||||
// Card center should be at: cursor - rotated grab offset (viewport space)
|
||||
const cardCenterViewportX = cursorViewportX - rotatedGrabX
|
||||
const cardCenterViewportY = cursorViewportY - rotatedGrabY
|
||||
|
||||
// Convert card center from viewport space to container space
|
||||
const cardCenterContainerX = cardCenterViewportX - containerRect.left
|
||||
const cardCenterContainerY = cardCenterViewportY - containerRect.top
|
||||
|
||||
// position.x/y represents translate() which positions the top-left corner
|
||||
// So we need: top-left = center - (width/2, height/2)
|
||||
setPosition({
|
||||
x: cardCenterContainerX - cardRect.width / 2,
|
||||
y: cardCenterContainerY - cardRect.height / 2,
|
||||
})
|
||||
}
|
||||
|
||||
const handlePointerUp = (e: React.PointerEvent) => {
|
||||
setIsDragging(false)
|
||||
dragStartRef.current = null
|
||||
|
||||
console.log('[Shadow] Drag released, speed decaying to 0')
|
||||
console.log(
|
||||
`[GrabPoint] Final rotation: ${rotation.toFixed(1)}° (base was ${baseRotationRef.current.toFixed(1)}°)`
|
||||
)
|
||||
|
||||
// Gradually decay speed back to 0 for smooth shadow transition
|
||||
const decayInterval = setInterval(() => {
|
||||
setDragSpeed((prev) => {
|
||||
const newSpeed = prev * 0.8 // Decay by 20% each frame
|
||||
if (newSpeed < 1) {
|
||||
clearInterval(decayInterval)
|
||||
return 0
|
||||
}
|
||||
return newSpeed
|
||||
})
|
||||
}, 50) // Update every 50ms
|
||||
|
||||
// Release the pointer capture
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
// Calculate dynamic shadow based on drag speed
|
||||
// Base shadow: 0 8px 24px rgba(0, 0, 0, 0.3)
|
||||
// Fast drag: 0 32px 64px rgba(0, 0, 0, 0.6)
|
||||
const shadowY = 8 + (dragSpeed / 100) * 24 // 8px to 32px
|
||||
const shadowBlur = 24 + (dragSpeed / 100) * 40 // 24px to 64px
|
||||
const shadowOpacity = 0.3 + (dragSpeed / 100) * 0.3 // 0.3 to 0.6
|
||||
const boxShadow = `0 ${shadowY}px ${shadowBlur}px rgba(0, 0, 0, ${shadowOpacity})`
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${isDragging ? 1.05 : 1})`,
|
||||
zIndex,
|
||||
touchAction: 'none',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transition: isDragging ? 'none' : 'transform 0.2s ease-out',
|
||||
}}
|
||||
className={css({
|
||||
userSelect: 'none',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
boxShadow, // Dynamic shadow based on drag speed
|
||||
}}
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'lg',
|
||||
p: '4',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
minW: '120px',
|
||||
border: '2px solid rgba(0, 0, 0, 0.1)',
|
||||
transition: 'box-shadow 0.1s', // Quick transition for responsive feel
|
||||
})}
|
||||
>
|
||||
{/* Abacus visualization */}
|
||||
<div
|
||||
className={css({
|
||||
transform: 'scale(0.6)',
|
||||
transformOrigin: 'center',
|
||||
})}
|
||||
>
|
||||
<AbacusReact value={card.number} columns={3} beadShape="circle" />
|
||||
</div>
|
||||
|
||||
{/* Number display */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800',
|
||||
fontFamily: 'mono',
|
||||
})}
|
||||
>
|
||||
{card.number}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
862
apps/web/src/components/LevelSliderDisplay.tsx
Normal file
862
apps/web/src/components/LevelSliderDisplay.tsx
Normal file
@@ -0,0 +1,862 @@
|
||||
'use client'
|
||||
|
||||
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 { css } from '../../styled-system/css'
|
||||
import { stack } from '../../styled-system/patterns'
|
||||
import { kyuLevelDetails } from '@/data/kyuLevelDetails'
|
||||
|
||||
// Combine all levels into one array for the slider
|
||||
const allLevels = [
|
||||
{
|
||||
level: '10th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
digits: 2,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '9th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
digits: 2,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '8th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
digits: 3,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '7th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
digits: 4,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '6th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
digits: 5,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '5th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
digits: 6,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '4th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
digits: 7,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '3rd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
digits: 8,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '2nd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
digits: 9,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: '1st Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
digits: 10,
|
||||
type: 'kyu' as const,
|
||||
},
|
||||
{
|
||||
level: 'Pre-1st Dan',
|
||||
name: 'Jun-Shodan',
|
||||
minScore: 90,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '1st Dan',
|
||||
name: 'Shodan',
|
||||
minScore: 100,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '2nd Dan',
|
||||
name: 'Nidan',
|
||||
minScore: 120,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '3rd Dan',
|
||||
name: 'Sandan',
|
||||
minScore: 140,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '4th Dan',
|
||||
name: 'Yondan',
|
||||
minScore: 160,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '5th Dan',
|
||||
name: 'Godan',
|
||||
minScore: 180,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '6th Dan',
|
||||
name: 'Rokudan',
|
||||
minScore: 200,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '7th Dan',
|
||||
name: 'Nanadan',
|
||||
minScore: 220,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '8th Dan',
|
||||
name: 'Hachidan',
|
||||
minScore: 250,
|
||||
emoji: '🧝♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '9th Dan',
|
||||
name: 'Kudan',
|
||||
minScore: 270,
|
||||
emoji: '🧝♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '10th Dan',
|
||||
name: 'Judan',
|
||||
minScore: 290,
|
||||
emoji: '👑',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
] as const
|
||||
|
||||
// Helper function to map level names to kyuLevelDetails keys
|
||||
function getLevelDetailsKey(levelName: string): string | null {
|
||||
// Convert "10th Kyu" → "10-kyu", "3rd Kyu" → "3-kyu", etc.
|
||||
const match = levelName.match(/^(\d+)(?:st|nd|rd|th)\s+Kyu$/)
|
||||
if (match) {
|
||||
return `${match[1]}-kyu`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse and format kyu level details into structured sections with icons
|
||||
function parseKyuDetails(rawText: string) {
|
||||
const lines = rawText.split('\n').filter((line) => line.trim() && !line.includes('shuzan.jp'))
|
||||
|
||||
// Always return sections in consistent order: Add/Sub, Multiply, Divide
|
||||
const sections: Array<{
|
||||
type: 'addSub' | 'multiply' | 'divide'
|
||||
icon: string
|
||||
label: string
|
||||
digits: string | null
|
||||
rows: string | null
|
||||
chars: string | null
|
||||
problems: string | null
|
||||
}> = [
|
||||
{
|
||||
type: 'addSub',
|
||||
icon: '➕➖',
|
||||
label: 'Add/Sub',
|
||||
digits: null,
|
||||
rows: null,
|
||||
chars: null,
|
||||
problems: null,
|
||||
},
|
||||
{
|
||||
type: 'multiply',
|
||||
icon: '✖️',
|
||||
label: 'Multiply',
|
||||
digits: null,
|
||||
rows: null,
|
||||
chars: null,
|
||||
problems: null,
|
||||
},
|
||||
{
|
||||
type: 'divide',
|
||||
icon: '➗',
|
||||
label: 'Divide',
|
||||
digits: null,
|
||||
rows: null,
|
||||
chars: null,
|
||||
problems: null,
|
||||
},
|
||||
]
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Add/Sub:')) {
|
||||
const match = line.match(/(\d+)-digit.*?(\d+)口.*?(\d+)字/)
|
||||
if (match) {
|
||||
sections[0].digits = match[1]
|
||||
sections[0].rows = match[2]
|
||||
sections[0].chars = match[3]
|
||||
}
|
||||
} else if (line.includes('×:')) {
|
||||
const match = line.match(/(\d+) digits.*?\((\d+)/)
|
||||
if (match) {
|
||||
sections[1].digits = match[1]
|
||||
sections[1].problems = match[2]
|
||||
}
|
||||
} else if (line.includes('÷:')) {
|
||||
const match = line.match(/(\d+) digits.*?\((\d+)/)
|
||||
if (match) {
|
||||
sections[2].digits = match[1]
|
||||
sections[2].problems = match[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
interface LevelSliderDisplayProps {
|
||||
initialIndex?: number
|
||||
autoAdvanceEnabled?: boolean
|
||||
autoAdvanceInterval?: number
|
||||
showLegend?: boolean
|
||||
}
|
||||
|
||||
export function LevelSliderDisplay({
|
||||
initialIndex = 0,
|
||||
autoAdvanceEnabled = true,
|
||||
autoAdvanceInterval = 3000,
|
||||
showLegend = true,
|
||||
}: LevelSliderDisplayProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
const [isPaneHovered, setIsPaneHovered] = useState(false)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// State for animated abacus digits
|
||||
const [animatedDigits, setAnimatedDigits] = useState<string>('')
|
||||
|
||||
// Initialize animated digits when level changes
|
||||
useEffect(() => {
|
||||
const generateRandomDigits = (numDigits: number) => {
|
||||
return Array.from({ length: numDigits }, () => Math.floor(Math.random() * 10)).join('')
|
||||
}
|
||||
setAnimatedDigits(generateRandomDigits(currentLevel.digits))
|
||||
}, [currentLevel.digits])
|
||||
|
||||
// Animate abacus calculations - speed increases with Dan level
|
||||
useEffect(() => {
|
||||
// Calculate animation speed based on level
|
||||
// Kyu levels: 500ms
|
||||
// Pre-1st Dan: 500ms
|
||||
// 1st-10th Dan: interpolate from 500ms to 10ms
|
||||
const getAnimationInterval = () => {
|
||||
if (currentIndex < 11) {
|
||||
// Kyu levels and Pre-1st Dan: constant 500ms
|
||||
return 500
|
||||
}
|
||||
// 1st Dan through 10th Dan: speed up from 500ms to 10ms
|
||||
// Index 11 (1st Dan) → 500ms
|
||||
// Index 20 (10th Dan) → 10ms
|
||||
const danProgress = (currentIndex - 11) / 9 // 0.0 to 1.0
|
||||
return 500 - danProgress * 490 // 500ms down to 10ms
|
||||
}
|
||||
|
||||
const intervalMs = getAnimationInterval()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setAnimatedDigits((prev) => {
|
||||
const digits = prev.split('').map(Number)
|
||||
const numColumns = digits.length
|
||||
|
||||
// Pick 1-3 adjacent columns to change (grouping effect)
|
||||
const groupSize = Math.floor(Math.random() * 3) + 1
|
||||
const startCol = Math.floor(Math.random() * (numColumns - groupSize + 1))
|
||||
|
||||
// Change the selected columns
|
||||
for (let i = startCol; i < startCol + groupSize && i < numColumns; i++) {
|
||||
digits[i] = Math.floor(Math.random() * 10)
|
||||
}
|
||||
|
||||
return digits.join('')
|
||||
})
|
||||
}, intervalMs)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [currentIndex])
|
||||
|
||||
// Auto-advance slider position every 3 seconds (unless pane is hovered)
|
||||
useEffect(() => {
|
||||
if (!autoAdvanceEnabled || isPaneHovered) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => {
|
||||
// Cycle back to 0 when reaching the end
|
||||
return prev >= allLevels.length - 1 ? 0 : prev + 1
|
||||
})
|
||||
}, autoAdvanceInterval)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [autoAdvanceEnabled, isPaneHovered, autoAdvanceInterval])
|
||||
|
||||
// Handle hover on slider track
|
||||
const handleSliderHover = (e: React.MouseEvent<HTMLSpanElement>) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const percentage = x / rect.width
|
||||
const index = Math.round(percentage * (allLevels.length - 1))
|
||||
setCurrentIndex(Math.max(0, Math.min(allLevels.length - 1, index)))
|
||||
}
|
||||
|
||||
// Calculate scale factor based on number of columns to fit the page
|
||||
// Use constrained range to prevent huge size differences between levels
|
||||
// Min 1.2 (for 30-column Dan levels) to Max 2.0 (for 2-column Kyu levels)
|
||||
const scaleFactor = Math.max(1.2, Math.min(2.0, 20 / currentLevel.digits))
|
||||
|
||||
// Animate scale factor with React Spring for smooth transitions
|
||||
const animatedProps = useSpring({
|
||||
scaleFactor,
|
||||
config: { tension: 350, friction: 45 },
|
||||
})
|
||||
|
||||
// Animate emoji with proper cross-fade (old fades out, new fades in)
|
||||
const emojiTransitions = useTransition(currentLevel.emoji, {
|
||||
keys: currentIndex,
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 },
|
||||
config: { duration: 120 },
|
||||
})
|
||||
|
||||
// Convert animated digits to a number/BigInt for the abacus display
|
||||
// Use BigInt for large numbers to get full 30-digit precision
|
||||
const displayValue =
|
||||
animatedDigits.length > 15
|
||||
? BigInt(animatedDigits || '0')
|
||||
: Number.parseInt(animatedDigits || '0', 10)
|
||||
|
||||
return (
|
||||
<div className={stack({ gap: '8' })}>
|
||||
{/* Current Level Display */}
|
||||
<div
|
||||
onMouseEnter={() => setIsPaneHovered(true)}
|
||||
onMouseLeave={() => setIsPaneHovered(false)}
|
||||
className={css({
|
||||
bg: 'transparent',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.500'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.500'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
rounded: 'xl',
|
||||
p: { base: '6', md: '8' },
|
||||
height: { base: 'auto', md: '700px' },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
{/* Abacus-themed Radix Slider */}
|
||||
<div className={css({ mb: '6', px: { base: '2', md: '8' } })}>
|
||||
<div className={css({ position: 'relative', py: '12' })}>
|
||||
{/* Emoji tick marks */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
left: '0',
|
||||
right: '0',
|
||||
h: 'full',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'none',
|
||||
px: '0', // Use full width for tick spacing
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
w: 'full',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
{allLevels.map((level, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
className={css({
|
||||
fontSize: '4xl',
|
||||
opacity: index === currentIndex ? '1' : '0.3',
|
||||
transition: 'all 0.2s',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'auto',
|
||||
_hover: { opacity: index === currentIndex ? '1' : '0.6' },
|
||||
})}
|
||||
>
|
||||
{level.emoji}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Slider.Root
|
||||
value={[currentIndex]}
|
||||
onValueChange={([value]) => setCurrentIndex(value)}
|
||||
min={0}
|
||||
max={allLevels.length - 1}
|
||||
step={1}
|
||||
onMouseMove={handleSliderHover}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
w: 'full',
|
||||
h: '32',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
>
|
||||
<Slider.Track
|
||||
className={css({
|
||||
bg: 'rgba(255, 255, 255, 0.2)',
|
||||
position: 'relative',
|
||||
flexGrow: 1,
|
||||
rounded: 'full',
|
||||
h: '3px',
|
||||
})}
|
||||
>
|
||||
<Slider.Range className={css({ display: 'none' })} />
|
||||
</Slider.Track>
|
||||
|
||||
<Slider.Thumb
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
w: '180px',
|
||||
h: '128px',
|
||||
bg: 'transparent',
|
||||
cursor: 'grab',
|
||||
transition: 'transform 0.15s ease-out, left 0.3s ease-out',
|
||||
zIndex: 10,
|
||||
_hover: { transform: 'scale(1.15)' },
|
||||
_focus: {
|
||||
outline: 'none',
|
||||
transform: 'scale(1.15)',
|
||||
},
|
||||
_active: { cursor: 'grabbing' },
|
||||
})}
|
||||
>
|
||||
<div className={css({ opacity: 0.75 })}>
|
||||
<StandaloneBead
|
||||
size={128}
|
||||
color={currentLevel.color === 'violet' ? '#8b5cf6' : '#22c55e'}
|
||||
animated={false}
|
||||
/>
|
||||
</div>
|
||||
{emojiTransitions((style, emoji) => (
|
||||
<animated.div
|
||||
style={style}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
fontSize: '9xl',
|
||||
pointerEvents: 'none',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
})}
|
||||
>
|
||||
{emoji}
|
||||
</animated.div>
|
||||
))}
|
||||
|
||||
{/* Level text as part of the bead display */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
bottom: '-80px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
textAlign: 'center',
|
||||
pointerEvents: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
})}
|
||||
>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.400'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.400'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.400'
|
||||
: 'amber.400',
|
||||
mb: '0.5',
|
||||
})}
|
||||
>
|
||||
{currentLevel.level}
|
||||
</h2>
|
||||
{'name' in currentLevel && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'md',
|
||||
color: 'gray.300',
|
||||
mb: '0.5',
|
||||
})}
|
||||
>
|
||||
{currentLevel.name}
|
||||
</div>
|
||||
)}
|
||||
{'minScore' in currentLevel && (
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
Min: {currentLevel.minScore}pts
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Slider.Thumb>
|
||||
</Slider.Root>
|
||||
</div>
|
||||
|
||||
{/* Level Markers */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mt: '1',
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
>
|
||||
<span>10th Kyu</span>
|
||||
<span>1st Kyu</span>
|
||||
<span>10th Dan</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Abacus Display with Level Details */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '4',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
{/* Level Details (only for Kyu levels) */}
|
||||
{currentLevel.type === 'kyu' &&
|
||||
(() => {
|
||||
const detailsKey = getLevelDetailsKey(currentLevel.level)
|
||||
const rawText = detailsKey
|
||||
? kyuLevelDetails[detailsKey as keyof typeof kyuLevelDetails]
|
||||
: null
|
||||
const sections = rawText ? parseKyuDetails(rawText) : []
|
||||
|
||||
// Use consistent sizing across all levels
|
||||
const sizing = { fontSize: 'md', gap: '3', iconSize: '4xl' }
|
||||
|
||||
return sections.length > 0 ? (
|
||||
<div
|
||||
className={css({
|
||||
flex: '0 0 auto',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '2',
|
||||
p: '2',
|
||||
maxW: '400px',
|
||||
alignContent: 'center',
|
||||
})}
|
||||
>
|
||||
{sections.map((section, idx) => {
|
||||
const hasData = section.digits !== null
|
||||
const levelColor =
|
||||
currentLevel.color === 'green'
|
||||
? 'green.300'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.300'
|
||||
: 'violet.300'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={css({
|
||||
bg: hasData ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.2)',
|
||||
border: '1px solid',
|
||||
borderColor: hasData ? 'gray.700' : 'gray.800',
|
||||
rounded: 'md',
|
||||
p: '3',
|
||||
transition: 'all 0.2s',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '1.5',
|
||||
opacity: hasData ? 1 : 0.3,
|
||||
width: '170px',
|
||||
height: '150px',
|
||||
_hover: hasData
|
||||
? {
|
||||
borderColor: 'gray.500',
|
||||
transform: 'scale(1.05)',
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: sizing.iconSize, lineHeight: '1' })}>
|
||||
{section.icon}
|
||||
</span>
|
||||
{hasData && section.digits && (
|
||||
<>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: levelColor,
|
||||
})}
|
||||
>
|
||||
{section.digits} digits
|
||||
</div>
|
||||
{(section.rows || section.chars) && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{section.rows && `${section.rows} rows`}
|
||||
{section.rows && section.chars && ' • '}
|
||||
{section.chars && `${section.chars} chars`}
|
||||
</div>
|
||||
)}
|
||||
{section.problems && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{section.problems} problems
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: hasData ? 'gray.500' : 'gray.700',
|
||||
textAlign: 'center',
|
||||
fontWeight: hasData ? 'normal' : 'bold',
|
||||
})}
|
||||
>
|
||||
{section.label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null
|
||||
})()}
|
||||
|
||||
{/* Abacus (right-aligned for Kyu, centered for Dan) */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: currentLevel.type === 'kyu' ? 'flex-end' : 'center',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
})}
|
||||
>
|
||||
<animated.div
|
||||
style={{
|
||||
transform: animatedProps.scaleFactor.to((s) => `scale(${s / scaleFactor})`),
|
||||
}}
|
||||
>
|
||||
<AbacusReact
|
||||
value={displayValue}
|
||||
columns={currentLevel.digits}
|
||||
scaleFactor={scaleFactor}
|
||||
showNumbers={true}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
</animated.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Digit Count */}
|
||||
<div
|
||||
className={css({
|
||||
textAlign: 'center',
|
||||
color: 'gray.400',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
Requires mastery of <strong>{currentLevel.digits}-digit</strong> calculations
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
{showLegend && (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6',
|
||||
justifyContent: 'center',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
bg: 'green.500',
|
||||
rounded: 'sm',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>Beginner (10-7 Kyu)</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
bg: 'blue.500',
|
||||
rounded: 'sm',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Intermediate (6-4 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
bg: 'violet.500',
|
||||
rounded: 'sm',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>Advanced (3-1 Kyu)</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
bg: 'amber.500',
|
||||
rounded: 'sm',
|
||||
})}
|
||||
/>
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>Master (Dan ranks)</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
apps/web/src/contexts/HomeHeroContext.tsx
Normal file
127
apps/web/src/contexts/HomeHeroContext.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Subtitle } from '../data/abaciOneSubtitles'
|
||||
import { subtitles } from '../data/abaciOneSubtitles'
|
||||
|
||||
interface HomeHeroContextValue {
|
||||
subtitle: Subtitle
|
||||
abacusValue: number
|
||||
setAbacusValue: (value: number) => void
|
||||
isHeroVisible: boolean
|
||||
setIsHeroVisible: (visible: boolean) => void
|
||||
isAbacusLoaded: boolean
|
||||
isSubtitleLoaded: boolean
|
||||
}
|
||||
|
||||
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
|
||||
|
||||
export { HomeHeroContext }
|
||||
|
||||
export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
|
||||
// Use first subtitle for SSR, then select random one on client mount
|
||||
const [subtitle, setSubtitle] = useState<Subtitle>(subtitles[0])
|
||||
const [isSubtitleLoaded, setIsSubtitleLoaded] = useState(false)
|
||||
|
||||
// Select random subtitle only on client side, persist per-session
|
||||
useEffect(() => {
|
||||
// Check if we have a stored subtitle index for this session
|
||||
const storedIndex = sessionStorage.getItem('heroSubtitleIndex')
|
||||
|
||||
if (storedIndex !== null) {
|
||||
// Use the stored subtitle index
|
||||
const index = parseInt(storedIndex, 10)
|
||||
if (!Number.isNaN(index) && index >= 0 && index < subtitles.length) {
|
||||
setSubtitle(subtitles[index])
|
||||
setIsSubtitleLoaded(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new random index and store it
|
||||
const randomIndex = Math.floor(Math.random() * subtitles.length)
|
||||
sessionStorage.setItem('heroSubtitleIndex', randomIndex.toString())
|
||||
setSubtitle(subtitles[randomIndex])
|
||||
setIsSubtitleLoaded(true)
|
||||
}, [])
|
||||
|
||||
// Shared abacus value - always start at 0 for SSR/hydration consistency
|
||||
const [abacusValue, setAbacusValue] = useState(0)
|
||||
const [isAbacusLoaded, setIsAbacusLoaded] = useState(false)
|
||||
const isLoadingFromStorage = useRef(false)
|
||||
|
||||
// Load from sessionStorage after mount (client-only, no hydration mismatch)
|
||||
useEffect(() => {
|
||||
console.log('[HeroAbacus] Loading from sessionStorage...')
|
||||
isLoadingFromStorage.current = true // Block saves during load
|
||||
|
||||
const saved = sessionStorage.getItem('heroAbacusValue')
|
||||
console.log('[HeroAbacus] Saved value from storage:', saved)
|
||||
|
||||
if (saved) {
|
||||
const parsedValue = parseInt(saved, 10)
|
||||
console.log('[HeroAbacus] Parsed value:', parsedValue)
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
console.log('[HeroAbacus] Setting abacus value to:', parsedValue)
|
||||
setAbacusValue(parsedValue)
|
||||
}
|
||||
} else {
|
||||
console.log('[HeroAbacus] No saved value found, staying at 0')
|
||||
}
|
||||
|
||||
// Use setTimeout to ensure the value has been set before we allow saves
|
||||
setTimeout(() => {
|
||||
isLoadingFromStorage.current = false
|
||||
setIsAbacusLoaded(true)
|
||||
console.log('[HeroAbacus] Load complete, allowing saves now and fading in')
|
||||
}, 0)
|
||||
}, [])
|
||||
|
||||
// Persist value to sessionStorage when it changes (but skip during load)
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'[HeroAbacus] Save effect triggered. Value:',
|
||||
abacusValue,
|
||||
'isLoadingFromStorage:',
|
||||
isLoadingFromStorage.current
|
||||
)
|
||||
|
||||
if (!isLoadingFromStorage.current) {
|
||||
console.log('[HeroAbacus] Saving to sessionStorage:', abacusValue)
|
||||
sessionStorage.setItem('heroAbacusValue', abacusValue.toString())
|
||||
console.log(
|
||||
'[HeroAbacus] Saved successfully. Storage now contains:',
|
||||
sessionStorage.getItem('heroAbacusValue')
|
||||
)
|
||||
} else {
|
||||
console.log('[HeroAbacus] Skipping save (currently loading from storage)')
|
||||
}
|
||||
}, [abacusValue])
|
||||
|
||||
// Track hero visibility for nav branding
|
||||
const [isHeroVisible, setIsHeroVisible] = useState(true)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
subtitle,
|
||||
abacusValue,
|
||||
setAbacusValue,
|
||||
isHeroVisible,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
}),
|
||||
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
|
||||
)
|
||||
|
||||
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>
|
||||
}
|
||||
|
||||
export function useHomeHero() {
|
||||
const context = useContext(HomeHeroContext)
|
||||
if (!context) {
|
||||
throw new Error('useHomeHero must be used within HomeHeroProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
97
apps/web/src/data/abaciOneSubtitles.ts
Normal file
97
apps/web/src/data/abaciOneSubtitles.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Abaci One subtitle options with descriptions
|
||||
* Three-word rhyming subtitles for the main app navigation
|
||||
*/
|
||||
|
||||
export interface Subtitle {
|
||||
text: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const subtitles: Subtitle[] = [
|
||||
{ text: 'Speed Bead Lead', description: 'blaze through bead races' },
|
||||
{ text: 'Rod Mod Nod', description: 'tweak rod technique, approval earned' },
|
||||
{ text: 'Grid Kid Lid', description: 'lock in neat grid habits' },
|
||||
{ text: 'Count Mount Amount', description: 'stack up that number sense' },
|
||||
{ text: 'Stack Track Tack', description: 'line up beads, lock in sums' },
|
||||
{ text: 'Quick Flick Trick', description: 'rapid-fire bead tactics' },
|
||||
{ text: 'Flash Dash Math', description: 'fly through numeric challenges' },
|
||||
{ text: 'Slide Glide Pride', description: 'smooth soroban strokes' },
|
||||
{ text: 'Shift Sift Gift', description: 'sort beads, reveal talent' },
|
||||
{ text: 'Beat Seat Meet', description: 'compete head-to-head' },
|
||||
{ text: 'Brain Train Gain', description: 'mental math muscle building' },
|
||||
{ text: 'Flow Show Pro', description: 'demonstrate soroban mastery' },
|
||||
{ text: 'Fast Blast Past', description: 'surpass speed limits' },
|
||||
{ text: 'Snap Tap Map', description: 'chart your calculation path' },
|
||||
{ text: 'Row Grow Know', description: 'advance through structured drills' },
|
||||
{ text: 'Drill Skill Thrill', description: 'practice that excites' },
|
||||
{ text: 'Think Link Sync', description: 'connect mind and beads' },
|
||||
{ text: 'Boost Joust Roost', description: 'power up, compete, settle in' },
|
||||
{ text: 'Add Grad Rad', description: 'level up addition awesomely' },
|
||||
{ text: 'Sum Fun Run', description: 'enjoy the arithmetic sprint' },
|
||||
{ text: 'Track Stack Pack', description: 'organize solutions systematically' },
|
||||
{ text: 'Beat Neat Feat', description: 'clean victories, impressive wins' },
|
||||
{ text: 'Math Path Wrath', description: 'dominate numeric challenges' },
|
||||
{ text: 'Spark Mark Arc', description: 'ignite progress, track growth' },
|
||||
{ text: 'Race Pace Ace', description: 'speed up, master it' },
|
||||
{ text: 'Flex Hex Reflex', description: 'adapt calculations instantly' },
|
||||
{ text: 'Glide Pride Stride', description: 'smooth confident progress' },
|
||||
{ text: 'Flash Dash Smash', description: 'speed through, crush totals' },
|
||||
{ text: 'Stack Attack Jack', description: 'aggressive bead strategies' },
|
||||
{ text: 'Quick Pick Click', description: 'rapid bead selection' },
|
||||
{ text: 'Snap Map Tap', description: 'visualize and execute' },
|
||||
{ text: 'Mind Find Grind', description: 'discover mental endurance' },
|
||||
{ text: 'Flip Skip Rip', description: 'fast transitions, tear through' },
|
||||
{ text: 'Blend Trend Send', description: 'mix methods, share progress' },
|
||||
{ text: 'Power Tower Hour', description: 'build skills intensively' },
|
||||
{ text: 'Launch Staunch Haunch', description: 'start strong, stay firm' },
|
||||
{ text: 'Rush Crush Hush', description: 'speed quietly dominates' },
|
||||
{ text: 'Swipe Stripe Hype', description: 'sleek moves, excitement' },
|
||||
{ text: 'Train Gain Sustain', description: 'build lasting ability' },
|
||||
{ text: 'Frame Claim Flame', description: 'structure your fire' },
|
||||
{ text: 'Streak Peak Tweak', description: 'hot runs, optimize performance' },
|
||||
{ text: 'Edge Pledge Wedge', description: 'commit to precision' },
|
||||
{ text: 'Pace Grace Space', description: 'rhythm, elegance, room to grow' },
|
||||
{ text: 'Link Think Brink', description: 'connect at breakthrough edge' },
|
||||
{ text: 'Quest Test Best', description: 'challenge yourself to excel' },
|
||||
{ text: 'Drive Thrive Arrive', description: 'push hard, succeed, reach goals' },
|
||||
{ text: 'Smart Start Chart', description: 'begin wisely, track progress' },
|
||||
{ text: 'Boost Coast Toast', description: 'accelerate, cruise, celebrate' },
|
||||
{ text: 'Spark Dark Embark', description: 'ignite before dawn journeys' },
|
||||
{ text: 'Blaze Graze Amaze', description: 'burn through, touch lightly, wow' },
|
||||
{ text: 'Shift Drift Gift', description: 'adapt smoothly, reveal talent' },
|
||||
{ text: 'Zone Hone Own', description: 'focus, refine, claim mastery' },
|
||||
{ text: 'Vault Halt Exalt', description: 'leap high, pause, celebrate' },
|
||||
{ text: 'Peak Seek Streak', description: 'find heights, maintain momentum' },
|
||||
{ text: 'Glow Show Grow', description: 'shine, display, expand' },
|
||||
{ text: 'Scope Hope Rope', description: 'survey possibilities, climb up' },
|
||||
{ text: 'Core Score More', description: 'fundamentals yield better results' },
|
||||
{ text: 'Rank Bank Thank', description: 'earn status, save wins, appreciate' },
|
||||
{ text: 'Merge Surge Verge', description: 'combine forces, power up, edge closer' },
|
||||
{ text: 'Bold Gold Hold', description: 'brave attempts, prize rewards, maintain' },
|
||||
{ text: 'Rise Prize Wise', description: 'ascend, win, learn' },
|
||||
{ text: 'Move Groove Prove', description: 'act, find rhythm, demonstrate' },
|
||||
{ text: 'Trust Thrust Adjust', description: 'believe, push, refine' },
|
||||
{ text: 'Beam Dream Team', description: 'radiate, aspire, collaborate' },
|
||||
{ text: 'Spin Win Grin', description: 'rotate beads, succeed, smile' },
|
||||
{ text: 'String Ring Bring', description: 'connect, cycle, deliver' },
|
||||
{ text: 'Clear Gear Steer', description: 'focus, equip, direct' },
|
||||
{ text: 'Path Math Aftermath', description: 'route, calculate, results' },
|
||||
{ text: 'Play Slay Day', description: 'engage, dominate, own it' },
|
||||
{ text: 'Code Mode Road', description: 'pattern, style, journey' },
|
||||
{ text: 'Craft Draft Shaft', description: 'build, sketch, core structure' },
|
||||
{ text: 'Light Might Fight', description: 'illuminate, empower, compete' },
|
||||
{ text: 'Stream Dream Extreme', description: 'flow, envision, push limits' },
|
||||
{ text: 'Claim Frame Aim', description: 'assert, structure, target' },
|
||||
{ text: 'Chart Smart Start', description: 'map, intelligent, begin' },
|
||||
{ text: 'Bright Flight Height', description: 'brilliant, soar, elevation' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Get a random subtitle from the list
|
||||
* Uses current timestamp as seed for variety across sessions
|
||||
*/
|
||||
export function getRandomSubtitle(): Subtitle {
|
||||
const index = Math.floor(Math.random() * subtitles.length)
|
||||
return subtitles[index]
|
||||
}
|
||||
123
apps/web/src/data/kyuLevelDetails.ts
Normal file
123
apps/web/src/data/kyuLevelDetails.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Detailed requirements for each Kyu level in the Soroban certification system
|
||||
* Source: shuzan.jp
|
||||
*
|
||||
* Note: Stored verbatim from source. Display formatting/translation happens in the UI layer.
|
||||
*/
|
||||
|
||||
export const kyuLevelDetails = {
|
||||
'10-kyu': `Add/Sub: 2-digit, 5口, 10字
|
||||
|
||||
×: 実+法 = 3 digits (20 problems)
|
||||
|
||||
Time: 20 min; Pass ≥ 60/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'9-kyu': `Add/Sub: 2-digit, 5口, 10字
|
||||
|
||||
×: 実+法 = 3 digits (20)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200. (If only one part clears, it's treated as 10-kyu per federation notes.)
|
||||
shuzan.jp`,
|
||||
|
||||
'8-kyu': `Add/Sub: 2-digit, 8口, 16字
|
||||
|
||||
×: 実+法 = 4 digits (10)
|
||||
|
||||
÷: 法+商 = 3 digits (10)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'7-kyu': `Add/Sub: 2-digit, 10口, 20字
|
||||
|
||||
×: 実+法 = 4 digits (10)
|
||||
|
||||
÷: 法+商 = 4 digits (10)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'6-kyu': `Add/Sub: 10口, 30字
|
||||
|
||||
×: 実+法 = 5 digits (20)
|
||||
|
||||
÷: 法+商 = 4 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'5-kyu': `Add/Sub: 10口, 40字
|
||||
|
||||
×: 実+法 = 6 digits (20)
|
||||
|
||||
÷: 法+商 = 5 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'4-kyu': `Add/Sub: 10口, 50字
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-3-kyu': `Add/Sub: 10口, 50字 ×5題 and 10口, 60字 ×5題 (total 10)
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'3-kyu': `Add/Sub: 10口, 60字
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-2-kyu': `Add/Sub: 10口, 70字
|
||||
|
||||
×: 実+法 = 8 digits (20)
|
||||
|
||||
÷: 法+商 = 7 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'2-kyu': `Add/Sub: 10口, 80字
|
||||
|
||||
×: 実+法 = 9 digits (20)
|
||||
|
||||
÷: 法+商 = 8 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-1-kyu': `Add/Sub: 10口, 90字
|
||||
|
||||
×: 実+法 = 10 digits (20)
|
||||
|
||||
÷: 法+商 = 9 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'1-kyu': `Add/Sub: 10口, 100字
|
||||
|
||||
×: 実+法 = 11 digits (20)
|
||||
|
||||
÷: 法+商 = 10 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
} as const
|
||||
|
||||
export type KyuLevel = keyof typeof kyuLevelDetails
|
||||
@@ -14,59 +14,59 @@ export interface GameTheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard theme presets
|
||||
* These use Panda CSS's color system and provide consistent styling
|
||||
* Standard theme presets with vibrant gradients
|
||||
* Updated for eye-catching game cards on the homepage
|
||||
*/
|
||||
export const GAME_THEMES = {
|
||||
blue: {
|
||||
color: 'blue',
|
||||
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
|
||||
borderColor: '#bfdbfe', // blue.200
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Vibrant cyan
|
||||
borderColor: '#00f2fe',
|
||||
},
|
||||
purple: {
|
||||
color: 'purple',
|
||||
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
|
||||
borderColor: '#ddd6fe', // purple.200
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Vibrant purple
|
||||
borderColor: '#764ba2',
|
||||
},
|
||||
green: {
|
||||
color: 'green',
|
||||
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
|
||||
borderColor: '#a7f3d0', // green.200
|
||||
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // Vibrant green/teal
|
||||
borderColor: '#38f9d7',
|
||||
},
|
||||
teal: {
|
||||
color: 'teal',
|
||||
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
|
||||
borderColor: '#99f6e4', // teal.200
|
||||
gradient: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', // Vibrant teal
|
||||
borderColor: '#38ef7d',
|
||||
},
|
||||
indigo: {
|
||||
color: 'indigo',
|
||||
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
|
||||
borderColor: '#c7d2fe', // indigo.200
|
||||
gradient: 'linear-gradient(135deg, #5f72bd 0%, #9b23ea 100%)', // Vibrant indigo
|
||||
borderColor: '#9b23ea',
|
||||
},
|
||||
pink: {
|
||||
color: 'pink',
|
||||
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
|
||||
borderColor: '#fbcfe8', // pink.200
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', // Vibrant pink
|
||||
borderColor: '#f5576c',
|
||||
},
|
||||
orange: {
|
||||
color: 'orange',
|
||||
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
|
||||
borderColor: '#fed7aa', // orange.200
|
||||
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // Vibrant orange/coral
|
||||
borderColor: '#fee140',
|
||||
},
|
||||
yellow: {
|
||||
color: 'yellow',
|
||||
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
|
||||
borderColor: '#fde68a', // yellow.200
|
||||
gradient: 'linear-gradient(135deg, #ffd89b 0%, #19547b 100%)', // Vibrant yellow/blue
|
||||
borderColor: '#ffd89b',
|
||||
},
|
||||
red: {
|
||||
color: 'red',
|
||||
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
|
||||
borderColor: '#fecaca', // red.200
|
||||
gradient: 'linear-gradient(135deg, #f85032 0%, #e73827 100%)', // Vibrant red
|
||||
borderColor: '#e73827',
|
||||
},
|
||||
gray: {
|
||||
color: 'gray',
|
||||
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
|
||||
borderColor: '#e5e7eb', // gray.200
|
||||
gradient: 'linear-gradient(135deg, #868f96 0%, #596164 100%)', // Vibrant gray
|
||||
borderColor: '#596164',
|
||||
},
|
||||
} as const satisfies Record<string, GameTheme>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.30.0",
|
||||
"version": "4.63.4",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,3 +1,305 @@
|
||||
# [2.1.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v2.0.0...abacus-react-v2.1.0) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add fixed height to entire level display pane ([200b26c](https://github.com/antialias/soroban-abacus-flashcards/commit/200b26c2cd35d1d637ede9dcfc3dbbc7f3f19320))
|
||||
* **levels:** increase container height to prevent abacus clipping ([cd5c15a](https://github.com/antialias/soroban-abacus-flashcards/commit/cd5c15aeb260c568fe7ad9b6a4f51c4d6498b2b8))
|
||||
* **levels:** only animate abacus, not container with background/border ([c80477d](https://github.com/antialias/soroban-abacus-flashcards/commit/c80477d24877ddada5f3f4405abbf05e1d753b5d))
|
||||
* **levels:** reduce Dan scale and container height to prevent clipping ([563136f](https://github.com/antialias/soroban-abacus-flashcards/commit/563136fb79fa10b2af3a119bf0f861e3b0812b2e))
|
||||
* **levels:** reduce max scale factor to allow more compact container ([ead9ee9](https://github.com/antialias/soroban-abacus-flashcards/commit/ead9ee9589aa4d7376e9385da5da53a6b444858a))
|
||||
* **levels:** reduce scale factor variation to minimize margin differences ([abb647c](https://github.com/antialias/soroban-abacus-flashcards/commit/abb647ce40b8f9d0c8268ab18c139324ae3195c5))
|
||||
* **levels:** revert delayed column change, keep overflow hidden ([22f00f5](https://github.com/antialias/soroban-abacus-flashcards/commit/22f00f59f5facc36a846408dcd196ec54ea676b1))
|
||||
* **levels:** stabilize slider position and prevent abacus clipping ([09004dc](https://github.com/antialias/soroban-abacus-flashcards/commit/09004dc2c055031ee2f71c964ceee6f7b1d42ecd))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** export StandaloneBead component wired to AbacusDisplayContext ([0146ce1](https://github.com/antialias/soroban-abacus-flashcards/commit/0146ce1e67da27a24cbaa8338ba6a1a6befd6bd3))
|
||||
* **levels:** add hover interaction and smooth React Spring transitions ([fd2b633](https://github.com/antialias/soroban-abacus-flashcards/commit/fd2b6338a84c3bbc683eff216a8da3b155749f0f))
|
||||
* **levels:** redesign slider with abacus-themed beads ([f3dce84](https://github.com/antialias/soroban-abacus-flashcards/commit/f3dce84532fa706e4ec9551facde2055a060ee13))
|
||||
* **levels:** replace slider thumb with diamond-shaped abacus beads ([0fbde53](https://github.com/antialias/soroban-abacus-flashcards/commit/0fbde53039d3ea000c6a3be492b733479e7bf47c))
|
||||
|
||||
# [2.0.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.8.0...abacus-react-v2.0.0) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add dark color for abacus numerals ([73ff32c](https://github.com/antialias/soroban-abacus-flashcards/commit/73ff32c2432beb62710e57aa8b3b4793eca43fda)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
|
||||
* add POST handler for join requests API endpoint ([d3e5cdf](https://github.com/antialias/soroban-abacus-flashcards/commit/d3e5cdfc54f2749f27c6f8b8db854a8d0b6029f8))
|
||||
* add Typst to Docker image for flashcard generation ([d9a7694](https://github.com/antialias/soroban-abacus-flashcards/commit/d9a769403187bf70fb069be7ffe77417a62271a5))
|
||||
* allow join with pending invitation for restricted rooms ([85b2cf9](https://github.com/antialias/soroban-abacus-flashcards/commit/85b2cf98167ccf632ab634a94eb436e1eb584614))
|
||||
* allow password retry when joining via share link ([e469363](https://github.com/antialias/soroban-abacus-flashcards/commit/e469363699071610a35e0b5c507d0e15e29daa44))
|
||||
* **api:** add 'math-sprint' to settings endpoint validation ([d790e5e](https://github.com/antialias/soroban-abacus-flashcards/commit/d790e5e278f81686077dbe3ef4adca49574ae434)), closes [#1](https://github.com/antialias/soroban-abacus-flashcards/issues/1)
|
||||
* **api:** include members and memberPlayers in room creation response ([8320d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/8320d9e730e2b9964e509847dfa504a78b721b5a))
|
||||
* **arcade-rooms:** navigate to invite link after room creation ([1922b21](https://github.com/antialias/soroban-abacus-flashcards/commit/1922b2122bb1bc4aeada7526d8c46aa89024bb00))
|
||||
* **arcade:** add defensive checks and update test fixtures ([a93d981](https://github.com/antialias/soroban-abacus-flashcards/commit/a93d981d1ab3abed019b28cebe87525191313cc7))
|
||||
* **arcade:** add host-only game selection with clear messaging ([22df1b0](https://github.com/antialias/soroban-abacus-flashcards/commit/22df1b0b661efe69fac1a6bd716531c904757412))
|
||||
* **arcade:** add host-only game selection with clear messaging ([c0680ca](https://github.com/antialias/soroban-abacus-flashcards/commit/c0680cad0fa26af0933e93a06c50317bf443cc7d))
|
||||
* **arcade:** add Number Guesser to game config helpers ([7d1a351](https://github.com/antialias/soroban-abacus-flashcards/commit/7d1a351ed6a1442ae34f6b75d46039bfa77a921b))
|
||||
* **arcade:** allow room creator to rejoin restricted/approval rooms ([654ba19](https://github.com/antialias/soroban-abacus-flashcards/commit/654ba19ccca595d34ad205c036c18afb99a494c7))
|
||||
* **arcade:** delete old session when room game changes ([98a3a25](https://github.com/antialias/soroban-abacus-flashcards/commit/98a3a2573db51899c41ba02796895d676c4e16ef))
|
||||
* **arcade:** implement settings persistence for matching game ([08fe432](https://github.com/antialias/soroban-abacus-flashcards/commit/08fe4326a6a7c484b9058a241f4ff79b3fb5125f))
|
||||
* **arcade:** only notify room creator of join requests ([bc571e3](https://github.com/antialias/soroban-abacus-flashcards/commit/bc571e3d0d11fe4142680132d551e25ca626d950))
|
||||
* **arcade:** preserve game settings when returning to game selection ([0ee7739](https://github.com/antialias/soroban-abacus-flashcards/commit/0ee7739091d60580d2f98cfe288b8586b03348f3))
|
||||
* **arcade:** preserve gameConfig when switching games ([2273c71](https://github.com/antialias/soroban-abacus-flashcards/commit/2273c71a872a5122d0b2023835fe30640106048e))
|
||||
* **arcade:** prevent empty update in settings API when only gameConfig changes ([ffb626f](https://github.com/antialias/soroban-abacus-flashcards/commit/ffb626f4038fd32d0f40dba8d83ae4d881d698d0))
|
||||
* **arcade:** prevent gameConfig from being overwritten when switching games ([a89d3a9](https://github.com/antialias/soroban-abacus-flashcards/commit/a89d3a970137471e2652de992c45370dbb97416d))
|
||||
* **arcade:** prevent server-side loading of React components ([784793b](https://github.com/antialias/soroban-abacus-flashcards/commit/784793ba244731edf45391da44588a978b137abe))
|
||||
* **arcade:** read nested gameConfig correctly when creating sessions ([94ef392](https://github.com/antialias/soroban-abacus-flashcards/commit/94ef39234d362b82e032cb69d3561b9fcb436eaf))
|
||||
* **arcade:** remove broken query param from game URLs ([87631af](https://github.com/antialias/soroban-abacus-flashcards/commit/87631af6788bd7b42e671374e55ec0ad8435900c))
|
||||
* **arcade:** remove legacy master-organizer placeholder ([76d207e](https://github.com/antialias/soroban-abacus-flashcards/commit/76d207e2e5244f84bc0d76fe3d753034f1991228))
|
||||
* **arcade:** resolve TypeScript errors in game config helpers ([04c9944](https://github.com/antialias/soroban-abacus-flashcards/commit/04c9944f2ed1025f5a4ece61761889edd08cc60d))
|
||||
* **build:** resolve Docker build failures preventing deployment ([7801dbb](https://github.com/antialias/soroban-abacus-flashcards/commit/7801dbb25fb0a33429c70f11294264f7238ce7a4))
|
||||
* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](https://github.com/antialias/soroban-abacus-flashcards/commit/26edec1bbf038264405ec9d161edcd18f67a6fc6))
|
||||
* **card-sorting:** faithfully port UI/UX from Python original ([c92076f](https://github.com/antialias/soroban-abacus-flashcards/commit/c92076f232930aa12d9a0230fa745b73b5cc04d9)), closes [#2c5f76](https://github.com/antialias/soroban-abacus-flashcards/issues/2c5f76) [#1976d2](https://github.com/antialias/soroban-abacus-flashcards/issues/1976d2)
|
||||
* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](https://github.com/antialias/soroban-abacus-flashcards/commit/d2a3b7ae2e3f6819b8d9ace32be22f04f748d1bc))
|
||||
* **card-sorting:** increase SVG size to fill card containers ([cf9d893](https://github.com/antialias/soroban-abacus-flashcards/commit/cf9d893f3fdbef6e91cd0ba283d602b9215569f1))
|
||||
* **card-sorting:** match game selector background to other games ([db62519](https://github.com/antialias/soroban-abacus-flashcards/commit/db62519f9beb0b4bc6120e1fd5ec251cfde5c3c1)), closes [#ccfbf1](https://github.com/antialias/soroban-abacus-flashcards/issues/ccfbf1) [#99f6e4](https://github.com/antialias/soroban-abacus-flashcards/issues/99f6e4)
|
||||
* **card-sorting:** match Python card layout with flex wrap ([9679d68](https://github.com/antialias/soroban-abacus-flashcards/commit/9679d68154ac8b6a2f905ec7d17a34b39bc00237))
|
||||
* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](https://github.com/antialias/soroban-abacus-flashcards/commit/e14ffe44d66d0c97bc0cc4e0c255698e88ce723a))
|
||||
* **card-sorting:** use blue gradient matching other game cards ([bdb84f5](https://github.com/antialias/soroban-abacus-flashcards/commit/bdb84f5d909542060fa886a83a5af62c4a785a98))
|
||||
* clear hover state in CLEAR_MISMATCH for clean turn transitions ([43f7c92](https://github.com/antialias/soroban-abacus-flashcards/commit/43f7c92f6d61616e18439c995dc4a4848e233520))
|
||||
* clear hover state on turn changes and game transitions ([6fd425c](https://github.com/antialias/soroban-abacus-flashcards/commit/6fd425ce85ddbf5e0125f757dc9886915fb6f749))
|
||||
* **complement-race:** add missing AI commentary cooldown updates ([357aa30](https://github.com/antialias/soroban-abacus-flashcards/commit/357aa30618f80d659ae515f94b7b9254bb458910))
|
||||
* **complement-race:** add missing useEffect import ([3054130](https://github.com/antialias/soroban-abacus-flashcards/commit/30541304dd0f0801860dd62967f7f7cae717bcdd))
|
||||
* **complement-race:** add missing useRef import ([d43829a](https://github.com/antialias/soroban-abacus-flashcards/commit/d43829ad48f7ee879a46879f5e6ac1256db1f564))
|
||||
* **complement-race:** add pressure decay system and improve logging ([66992e8](https://github.com/antialias/soroban-abacus-flashcards/commit/66992e877065a42d00379ef8fae0a6e252b0ffcb))
|
||||
* **complement-race:** balance AI speeds to match original implementation ([054f0c0](https://github.com/antialias/soroban-abacus-flashcards/commit/054f0c0d235dc2b0042a0f6af48840d23a4c5ff8))
|
||||
* **complement-race:** clear input state on question transitions ([5872030](https://github.com/antialias/soroban-abacus-flashcards/commit/587203056a1e1692348805eb0de909d81d16e158))
|
||||
* **complement-race:** correct passenger boarding to use multiplayer fields ([7ed1b94](https://github.com/antialias/soroban-abacus-flashcards/commit/7ed1b94b8fa620cb4f64ba43e160ef511704f3ce))
|
||||
* **complement-race:** counter-flip AI speech bubbles to make text readable ([07d5607](https://github.com/antialias/soroban-abacus-flashcards/commit/07d5607218aee03e813eceff5d161a7838d66bcb))
|
||||
* **complement-race:** flip AI racers to face right in practice mode ([ebfff1a](https://github.com/antialias/soroban-abacus-flashcards/commit/ebfff1a62fd104d531a8158345c8c012ec8a55d3))
|
||||
* **complement-race:** flip player avatar to face right in practice mode ([fa6b3b6](https://github.com/antialias/soroban-abacus-flashcards/commit/fa6b3b69d5a4a7eb70f8c18fc8c122c54c4d504a))
|
||||
* **complement-race:** implement client-side momentum with continuous decay for smooth train movement ([ea19ff9](https://github.com/antialias/soroban-abacus-flashcards/commit/ea19ff918bc70ad3eb0339e18dbd32195f34816e))
|
||||
* **complement-race:** improve AI speech bubble positioning ([6e436db](https://github.com/antialias/soroban-abacus-flashcards/commit/6e436db5e709d944ebffed6936ea1f8e4bd2e19e))
|
||||
* **complement-race:** reduce initial momentum from 50 to 10 to prevent train sailing past first station ([5f146b0](https://github.com/antialias/soroban-abacus-flashcards/commit/5f146b0daf74d54e1c7b9a57d3a2f37e73849ff2))
|
||||
* **complement-race:** remove dual game loop conflict preventing route progression ([84d42e2](https://github.com/antialias/soroban-abacus-flashcards/commit/84d42e22ac0cdd25e87e45dc698029ad7ed78559))
|
||||
* **complement-race:** resolve TypeScript errors in state adapter ([59abcca](https://github.com/antialias/soroban-abacus-flashcards/commit/59abcca4c4192ca28944fa1fa366791d557c1c27))
|
||||
* **complement-race:** restore smooth train movement with client-side game loop ([46a80cb](https://github.com/antialias/soroban-abacus-flashcards/commit/46a80cbcc8ec39224d4edaf540da25611d48fbdd))
|
||||
* **complement-race:** show new passengers when route changes ([ec1c8ed](https://github.com/antialias/soroban-abacus-flashcards/commit/ec1c8ed263844f56477c1f709041339b42b48f4e))
|
||||
* **complement-race:** track physical car indices to prevent boarding issues ([53bbae8](https://github.com/antialias/soroban-abacus-flashcards/commit/53bbae84af7317d5e12109db2054cc70ca5bea27))
|
||||
* **complement-race:** track previous position to detect route threshold crossing ([a6c20aa](https://github.com/antialias/soroban-abacus-flashcards/commit/a6c20aab3b245d9893808d188d16a35ab80cfca9))
|
||||
* **complement-race:** train now moves in sprint mode ([54b46e7](https://github.com/antialias/soroban-abacus-flashcards/commit/54b46e771e654721e7fabb1f45ecd45daf8e447f))
|
||||
* **complement-race:** update passenger display when state changes ([5116364](https://github.com/antialias/soroban-abacus-flashcards/commit/511636400c19776b58c6bddf8f7c9cc398a05236))
|
||||
* **complement-race:** use active local players pattern from navbar ([71cdc34](https://github.com/antialias/soroban-abacus-flashcards/commit/71cdc342c97ca53b5e7e4202d4d344199e8ddd98))
|
||||
* **complement-race:** use local player emoji instead of first active player ([76eb051](https://github.com/antialias/soroban-abacus-flashcards/commit/76eb0517c202d1b9160b49dec0b99ff4972daff2))
|
||||
* correct AbacusReact API usage and add structural styling ([247377f](https://github.com/antialias/soroban-abacus-flashcards/commit/247377fca35ee3433e02ad594ecc1c4f391f0143)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78)
|
||||
* create arcade sessions on room join to enable config changes ([c29501f](https://github.com/antialias/soroban-abacus-flashcards/commit/c29501f6663cb6063f2ddef8b3fdb14c31927639))
|
||||
* **db:** add 'math-sprint' to database schema enums ([7b112a9](https://github.com/antialias/soroban-abacus-flashcards/commit/7b112a98babe782d4c254ef18a0295e7cbf8fefa)), closes [#1](https://github.com/antialias/soroban-abacus-flashcards/issues/1)
|
||||
* **deployment:** pass git info to Docker build for deployment info modal ([4b04e43](https://github.com/antialias/soroban-abacus-flashcards/commit/4b04e43ff8c9e9f239d7f5e306aab338b535296f))
|
||||
* **docker:** add packages/templates for Typst flashcard generation ([1417722](https://github.com/antialias/soroban-abacus-flashcards/commit/14177224380b8c37413123bee344c9b762055a15))
|
||||
* **docker:** add qpdf for PDF linearization and validation ([c92ff39](https://github.com/antialias/soroban-abacus-flashcards/commit/c92ff3971c853e4e55ccd632ff3ee292fcce8315))
|
||||
* **docker:** bypass PEP 668 externally-managed-environment error ([bb59c61](https://github.com/antialias/soroban-abacus-flashcards/commit/bb59c61638e60b0678043e954e044d9390f88e7f))
|
||||
* **docker:** copy core package with Python scripts to production image ([33e9ad2](https://github.com/antialias/soroban-abacus-flashcards/commit/33e9ad2f79b591f1c5ee57a6691e1bcf48420859))
|
||||
* **docker:** include Panda CSS styled-system in production image ([57fabff](https://github.com/antialias/soroban-abacus-flashcards/commit/57fabffe605d953b4a4d7e05032401cbf1ab2d14))
|
||||
* **docker:** install py3-pip for Python dependency installation ([0f55909](https://github.com/antialias/soroban-abacus-flashcards/commit/0f55909533414bdc07f113b93bb8bfa21367959b))
|
||||
* **docker:** install Python dependencies for flashcard generation ([c9b7e92](https://github.com/antialias/soroban-abacus-flashcards/commit/c9b7e92f39ee7aa7f13606c2836763144df102e7))
|
||||
* **docker:** remove reference to deleted @soroban/client package ([2953ef8](https://github.com/antialias/soroban-abacus-flashcards/commit/2953ef8917f7b13f6eb562eb7d58d14179a718da))
|
||||
* exclude dist from TypeScript compilation and add missing type import ([b7f1d5a](https://github.com/antialias/soroban-abacus-flashcards/commit/b7f1d5a5696888bb4fbf6d5da14ca333de0f0167))
|
||||
* hide hover avatar for current user's own player ([dba42b5](https://github.com/antialias/soroban-abacus-flashcards/commit/dba42b59257f2422ec8f31a46c222393fcc157d4))
|
||||
* **homepage:** adjust mini abacus container height ([c4066d6](https://github.com/antialias/soroban-abacus-flashcards/commit/c4066d687925bbe7737ebfeefdada7365ff97c6c))
|
||||
* **homepage:** correct positioning of progression arrows in Your Journey section ([3fff9ef](https://github.com/antialias/soroban-abacus-flashcards/commit/3fff9ef140bf1f462042f8319ed6c5e2a376e4ba))
|
||||
* **homepage:** fix MiniAbacus runtime error and improve sizing ([1fa0df8](https://github.com/antialias/soroban-abacus-flashcards/commit/1fa0df85f7d3988cbc61701d89476419ccf0a13c))
|
||||
* **homepage:** improve text contrast in Your Journey section ([24d1200](https://github.com/antialias/soroban-abacus-flashcards/commit/24d120004dccecc1ce2f08c1b73eec902868fb23))
|
||||
* **homepage:** use correct AbacusReact API and fix clipping/styling issues ([1432afd](https://github.com/antialias/soroban-abacus-flashcards/commit/1432afd6e6bd547bd0da76dbeea1c2b71244826f))
|
||||
* **homepage:** use direct conditionals for mini abacus padding ([38ef16a](https://github.com/antialias/soroban-abacus-flashcards/commit/38ef16a8f91f8ab4ad0d717b0321e2002636fafb))
|
||||
* **homepage:** use explicit RGBA colors for Your Journey text ([9c51cc9](https://github.com/antialias/soroban-abacus-flashcards/commit/9c51cc94eec4efcab9c0b9d1190f5b79c0c7d365))
|
||||
* **homepage:** use inline styles for journey level colors ([5d85e89](https://github.com/antialias/soroban-abacus-flashcards/commit/5d85e898d65d44d8d09bee952fad44b5a9c0cd20)), closes [#4ade80](https://github.com/antialias/soroban-abacus-flashcards/issues/4ade80) [#60a5](https://github.com/antialias/soroban-abacus-flashcards/issues/60a5) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
|
||||
* **homepage:** use inline styles for Your Journey text contrast ([8e51390](https://github.com/antialias/soroban-abacus-flashcards/commit/8e5139001818d7013e1b2654ac707f7429316d58)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3) [#d1d5](https://github.com/antialias/soroban-abacus-flashcards/issues/d1d5)
|
||||
* **home:** use Panda CSS token() for dynamic colors and center arrows properly ([d52ba63](https://github.com/antialias/soroban-abacus-flashcards/commit/d52ba6373a4577655dc1e5f5ff4926af7f7d96c3))
|
||||
* improve authorization error handling and add missing decline invitation endpoint ([97669ad](https://github.com/antialias/soroban-abacus-flashcards/commit/97669ad084b077cbf6f33b570710016ba666cdb6))
|
||||
* improve join request approval error handling with actionable messages ([57bf846](https://github.com/antialias/soroban-abacus-flashcards/commit/57bf8460c8ecff374355bfb93f4b06dfbb148273))
|
||||
* improve kicked modal message for retired room ejections ([f865ce1](https://github.com/antialias/soroban-abacus-flashcards/commit/f865ce16ecf7648e41549795c8137f4fc33e34ac))
|
||||
* join user socket channel to receive approval notifications ([7d08fdd](https://github.com/antialias/soroban-abacus-flashcards/commit/7d08fdd90643920857eda09998ac01afbae74154))
|
||||
* **levels:** use correct AbacusReact API with direct props ([892b377](https://github.com/antialias/soroban-abacus-flashcards/commit/892b377eb3bbd555dd2566bf58e946e9faa7b9f6))
|
||||
* **levels:** use correct dark mode styling from homepage + docs update ([c38767f](https://github.com/antialias/soroban-abacus-flashcards/commit/c38767f4d399fa2caa5cd4e0185689d0207fbdaf))
|
||||
* **matching:** add settings persistence to matching game ([00dcb87](https://github.com/antialias/soroban-abacus-flashcards/commit/00dcb872b7e70bdb7de301b56fe42195e6ee923f))
|
||||
* **matching:** apply turn indicators to arcade version too ([e6f96a8](https://github.com/antialias/soroban-abacus-flashcards/commit/e6f96a8b992c15f868ac5b1c1ac36b32caf433ed))
|
||||
* **matching:** make MemoryGrid generic to support different card types ([dcda826](https://github.com/antialias/soroban-abacus-flashcards/commit/dcda826b9a7cab6614638f8661f288e9fa010324))
|
||||
* **matching:** only apply turn indicator when game is active ([cb4c061](https://github.com/antialias/soroban-abacus-flashcards/commit/cb4c061d11433799a0091f4a958371ff7cef7a00))
|
||||
* **matching:** replace mismatch banner with card shake animation ([804096f](https://github.com/antialias/soroban-abacus-flashcards/commit/804096fd8a0709750114ab01a1015f9b5fc28b63))
|
||||
* **matching:** use UUID instead of numeric index for scores ([5036cb0](https://github.com/antialias/soroban-abacus-flashcards/commit/5036cb00b6eff91cfa52b5babb7e5a91ff7e18b3))
|
||||
* **math-sprint:** remove unused import and autoFocus attribute ([51593eb](https://github.com/antialias/soroban-abacus-flashcards/commit/51593eb44f93e369d6a773ee80e5f5cf50f3be67))
|
||||
* **memory-quiz:** fix playMode persistence by updating validator ([de0efd5](https://github.com/antialias/soroban-abacus-flashcards/commit/de0efd59321ec779cddb900724035884290419b7))
|
||||
* **memory-quiz:** persist playMode setting across game switches ([487ca7f](https://github.com/antialias/soroban-abacus-flashcards/commit/487ca7fba62e370c85bc3779ca8a96eb2c2cc3e3))
|
||||
* **memory-quiz:** prevent duplicate card processing from optimistic updates ([51676fc](https://github.com/antialias/soroban-abacus-flashcards/commit/51676fc15f5bc15cdb43393d3e66f7c5a0667868))
|
||||
* **memory-quiz:** prevent input lag during rapid typing in room mode ([b45139b](https://github.com/antialias/soroban-abacus-flashcards/commit/b45139b588d0ab6df4d6c1003c1b65b634e2b041))
|
||||
* **memory-quiz:** scope game settings by game name for proper persistence ([3dfe54f](https://github.com/antialias/soroban-abacus-flashcards/commit/3dfe54f1cb89bd636e763e1c5acb03776f97c011))
|
||||
* **memory-quiz:** synchronize card display across all players in multiplayer ([472f201](https://github.com/antialias/soroban-abacus-flashcards/commit/472f201088d82f92030273fadaf8a8e488820d6c))
|
||||
* **migrations:** add migration 0009 for display_password column ([040d749](https://github.com/antialias/soroban-abacus-flashcards/commit/040d7495a0801076b252d2574023f5323540db1a))
|
||||
* **moderation:** don't show pending invitation for users already in room ([fae5920](https://github.com/antialias/soroban-abacus-flashcards/commit/fae5920e2fda910f8db724103a837537b1063ac7))
|
||||
* **moderation:** improve access mode settings UX ([dd9e657](https://github.com/antialias/soroban-abacus-flashcards/commit/dd9e657db85752b32ff91ae1b33a0bf7a7628e07))
|
||||
* move invitations into nav and filter out current/banned rooms ([cfaf82b](https://github.com/antialias/soroban-abacus-flashcards/commit/cfaf82b2cc93c6da1dcceb5aea8d0bd2c7b14cea))
|
||||
* **nav:** add delay to hamburger menu hover to prevent premature closing ([95cd72e](https://github.com/antialias/soroban-abacus-flashcards/commit/95cd72e9bf410c7772999a17fae88719dd6e404e))
|
||||
* **nav:** add z-index to turn labels to prevent avatar overlap ([7c294da](https://github.com/antialias/soroban-abacus-flashcards/commit/7c294dafff4e4d70831e12897aec06092cd3ff3f))
|
||||
* **nav:** close hamburger menu when nested dropdown closes and mouse not hovering ([7d65212](https://github.com/antialias/soroban-abacus-flashcards/commit/7d652126d04239d1971b8aa302137295a3dde90b))
|
||||
* **nav:** enable tooltips for local players during gameplay ([5499700](https://github.com/antialias/soroban-abacus-flashcards/commit/54997007b8485c4d7c605d5b6179cedef3fdc9c7))
|
||||
* **nav:** improve readability of turn label text ([bbd1da0](https://github.com/antialias/soroban-abacus-flashcards/commit/bbd1da02b5e94d625844d9301c617da98d01868a))
|
||||
* **nav:** improve text contrast in room info pane ([3e691cb](https://github.com/antialias/soroban-abacus-flashcards/commit/3e691cb06d64e32f65146ffd690fa1c25e9b487d))
|
||||
* **nav:** navigate to /arcade/room (not /arcade/rooms/{id}) ([1c55f36](https://github.com/antialias/soroban-abacus-flashcards/commit/1c55f3630cb5f07b685555e41baa5a49314f15a3))
|
||||
* **nav:** navigate to room after creation from (+) menu ([21e6e33](https://github.com/antialias/soroban-abacus-flashcards/commit/21e6e33173e7939102a7e6d6a7bd5168a97a49d6))
|
||||
* **nav:** prevent hamburger menu from closing when toggling Style dropdown ([a898fbc](https://github.com/antialias/soroban-abacus-flashcards/commit/a898fbc187bf5286f63719420e0e98654ef25bb3))
|
||||
* **nav:** prevent style dropdown from closing hamburger menu ([560a052](https://github.com/antialias/soroban-abacus-flashcards/commit/560a05266e15b51089cfd127b4ebe0990f04e64d))
|
||||
* **nav:** prevent turn label text from being obscured ([c4b00dd](https://github.com/antialias/soroban-abacus-flashcards/commit/c4b00dd679aa2f87ae6b84a37e5b6d0d38113606))
|
||||
* **nav:** properly prevent nested style dropdown from closing hamburger menu ([c5b6a82](https://github.com/antialias/soroban-abacus-flashcards/commit/c5b6a82ca42e0f9b381f2086e825e3ce36d738a9))
|
||||
* **nav:** remove animation/enlargement from network player turn indicator ([53079ed](https://github.com/antialias/soroban-abacus-flashcards/commit/53079ede13ce9734cd3e702f27e6cb8f7fff626e))
|
||||
* **nav:** remove blue gradient background from network players ([2881aff](https://github.com/antialias/soroban-abacus-flashcards/commit/2881affecca16afefdeb05aecdbb3648cba05691))
|
||||
* **nav:** remove opacity reduction from local players ([5215af8](https://github.com/antialias/soroban-abacus-flashcards/commit/5215af801fc7e7412dc1c84e6abf1231f7670bfb))
|
||||
* **nav:** remove play arrow badge from turn indicators ([80cfc10](https://github.com/antialias/soroban-abacus-flashcards/commit/80cfc10f7887533b20d663f320696917e1856899))
|
||||
* **nav:** update types for registry games with nullable gameName ([a51e539](https://github.com/antialias/soroban-abacus-flashcards/commit/a51e539d023681daf639ec104e79079c8ceec98e))
|
||||
* **number-guesser:** add turn indicators, error feedback, and fix player ordering ([9f62623](https://github.com/antialias/soroban-abacus-flashcards/commit/9f626236845493ef68e1b3626e80efa35637b449))
|
||||
* pixel-perfect alignment across all nav elements ([fa78a2c](https://github.com/antialias/soroban-abacus-flashcards/commit/fa78a2c001f3530b0a0929411e6e0addbc0abda0))
|
||||
* **player-config:** correct label positioning in player settings dialog ([554cc40](https://github.com/antialias/soroban-abacus-flashcards/commit/554cc4063bc756c9c9cd1adf0c1964d3f2f6151b))
|
||||
* populate session activePlayers from room members on join ([2d00939](https://github.com/antialias/soroban-abacus-flashcards/commit/2d00939f1b59a10d271f82098c1b88acb2245ce1))
|
||||
* prevent duplicate arcade sessions per room ([4cc3de5](https://github.com/antialias/soroban-abacus-flashcards/commit/4cc3de5f43711bb2ffe9b10052108b27bba6889c))
|
||||
* remove duplicate ModerationNotifications causing double toasts ([c6886a0](https://github.com/antialias/soroban-abacus-flashcards/commit/c6886a0e59b3cbf051a828e0157495101cd8c823))
|
||||
* replace isLocked with accessMode and add bcryptjs ([a74b96b](https://github.com/antialias/soroban-abacus-flashcards/commit/a74b96bb6fe331d27f3d27b8f77a3ce32b254bce))
|
||||
* replace last remaining isLoading with isPending in CreateRoomModal ([85d13cc](https://github.com/antialias/soroban-abacus-flashcards/commit/85d13cc552cfe2e825f8ea20c7db00d666599134))
|
||||
* replace native alerts with inline confirmations in ModerationPanel ([ebe123e](https://github.com/antialias/soroban-abacus-flashcards/commit/ebe123ed7edf24fbc7b8765ed709455a8513d6d5))
|
||||
* reset join request toast state when moderation event cleared ([6beb58a](https://github.com/antialias/soroban-abacus-flashcards/commit/6beb58a7b8f8e1841c71729a3517ab459e924aa9))
|
||||
* resolve Memory Quiz room-based multiplayer validation issues ([2ffeade](https://github.com/antialias/soroban-abacus-flashcards/commit/2ffeade43710b5f3fff9991cc84763bbdbf97010))
|
||||
* resolve TypeScript errors in MemoryGrid and StandardGameLayout ([cabbc82](https://github.com/antialias/soroban-abacus-flashcards/commit/cabbc821955d70f118630dc21a9fcbb6d340f278))
|
||||
* **room-data:** update query cache when gameConfig changes ([7cea297](https://github.com/antialias/soroban-abacus-flashcards/commit/7cea297095b78d74f5b77ca83489ec1be684a486))
|
||||
* **rooms:** add real-time ownership transfer updates via WebSocket ([c00cfa3](https://github.com/antialias/soroban-abacus-flashcards/commit/c00cfa3de011720f3399fa340182b347f7e0d456))
|
||||
* **room:** update GAME_TYPE_TO_NAME mapping for memory-quiz ([4afa171](https://github.com/antialias/soroban-abacus-flashcards/commit/4afa171af212902120599b3d68f58cfbdf7820b0))
|
||||
* set color on abacus container div for numeral visibility ([cd47960](https://github.com/antialias/soroban-abacus-flashcards/commit/cd4796024e41f731ae5d83c82f6582e19d6eaf99)), closes [#1f2937](https://github.com/antialias/soroban-abacus-flashcards/issues/1f2937)
|
||||
* show initial value and improve numeral contrast ([1b57f6d](https://github.com/antialias/soroban-abacus-flashcards/commit/1b57f6ddecf3a118f2e4fadd1a91be1256f5a034)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
|
||||
* simplify abacus pane with light background ([30f48ab](https://github.com/antialias/soroban-abacus-flashcards/commit/30f48ab8976976688e089b07ece7fdae6d7ada79))
|
||||
* **socket-io:** update import path for socket-server module ([1a64dec](https://github.com/antialias/soroban-abacus-flashcards/commit/1a64decf5afe67c16e1aec283262ffa6132dcd83))
|
||||
* stack game title dropdown ABOVE room pane, not inside it ([7bc815f](https://github.com/antialias/soroban-abacus-flashcards/commit/7bc815fd7dbbb1489a17782b2df0c3fe508dd574))
|
||||
* **toast:** scope animations to prevent affecting other UI elements ([245ed8a](https://github.com/antialias/soroban-abacus-flashcards/commit/245ed8a625ba848f8cd79d51bfd88600cd77f0b9))
|
||||
* **tutorial:** correct column index calculation for variable column counts ([bf1ced4](https://github.com/antialias/soroban-abacus-flashcards/commit/bf1ced43f801938b05f01548eea5fe771de1b58f))
|
||||
* **tutorial:** filter bead highlights when using fewer columns ([4d906ec](https://github.com/antialias/soroban-abacus-flashcards/commit/4d906ec20e90a9b0b3838f9b8428e0c68992f381))
|
||||
* **tutorial:** reduce tooltip z-index to scroll under nav bar ([47640f3](https://github.com/antialias/soroban-abacus-flashcards/commit/47640f3486c6d4a7107d59bdcce043f76fabbb1d))
|
||||
* **tutorial:** resolve React hydration error in TutorialPlayer ([c883d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/c883d9e4c1b3a2f52c9d41e3ddce7418399f2649))
|
||||
* **tutorial:** resolve TypeScript errors in TutorialPlayer ([88f57ce](https://github.com/antialias/soroban-abacus-flashcards/commit/88f57ce6df125142d6ea7feec60c475926bd4929))
|
||||
* **tutorial:** use correct customStyles API for dark mode frame styling ([fdc882c](https://github.com/antialias/soroban-abacus-flashcards/commit/fdc882cb046e3d8835fbca59841e9af5329bcc52))
|
||||
* update locked room terminology and allow existing members ([1ddf985](https://github.com/antialias/soroban-abacus-flashcards/commit/1ddf985938d9542fe26e44da58234f3d4e3c9543))
|
||||
* use app-wide abacus config and remove instruction text ([0a50c73](https://github.com/antialias/soroban-abacus-flashcards/commit/0a50c733b089c7c341f0fdef47da78d1c61a3cb5))
|
||||
* use color instead of fill for numeral styling ([ea10c16](https://github.com/antialias/soroban-abacus-flashcards/commit/ea10c16811eb969b9963417079c330ae9ff295ba))
|
||||
* use defaultValue for interactive abacus control ([06aca98](https://github.com/antialias/soroban-abacus-flashcards/commit/06aca986ace4d76b70f2fd2f5e57f66758185b38))
|
||||
* use useCreateRoom hook instead of nonexistent createRoom from useRoomData ([f7d63b3](https://github.com/antialias/soroban-abacus-flashcards/commit/f7d63b30ac498b63797ae8683a0beb435a1c97b3))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **db:** remove database schema coupling for game names ([e135d92](https://github.com/antialias/soroban-abacus-flashcards/commit/e135d92abb4d27f646c1fbeff6524a729d107426)), closes [#1](https://github.com/antialias/soroban-abacus-flashcards/issues/1)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** add BigInt support for 30-digit Dan level abacuses ([0ab4cc2](https://github.com/antialias/soroban-abacus-flashcards/commit/0ab4cc288066b75a6ea4371f65098db5c0fc8847))
|
||||
* add API routes for moderation and invitations ([79a8518](https://github.com/antialias/soroban-abacus-flashcards/commit/79a85185575571cf628d655b0558f8246d2b02c7))
|
||||
* add backend library functions for room moderation ([84f3c4b](https://github.com/antialias/soroban-abacus-flashcards/commit/84f3c4bcfd258cb2e54b89b55d5162af57b74fe5))
|
||||
* add common UI components ([cd3115a](https://github.com/antialias/soroban-abacus-flashcards/commit/cd3115aa6d6bb8cf227b3d15d055f27dc5377a00))
|
||||
* add database schema for room moderation and invitations ([97d1604](https://github.com/antialias/soroban-abacus-flashcards/commit/97d16041dfe33bd817df2472323962dc4e94f8ee))
|
||||
* add drizzle migration for room_game_configs table ([3bae00b](https://github.com/antialias/soroban-abacus-flashcards/commit/3bae00b9a9dc925039a02fe07d036a2fc5e0fb79))
|
||||
* add fun automatic player naming system ([249257c](https://github.com/antialias/soroban-abacus-flashcards/commit/249257c6c77d503b48479065664c96c5de36a234))
|
||||
* add invitation system UI components ([fd3a2d1](https://github.com/antialias/soroban-abacus-flashcards/commit/fd3a2d1f76eb12473bb2b5a33453362d6889d7b0))
|
||||
* add moderation panel with unban & invite feature ([a2d0169](https://github.com/antialias/soroban-abacus-flashcards/commit/a2d0169f8063f00726ce2769bd5db270cfa82f4d))
|
||||
* add name generator button and abacus emoji ([07212e4](https://github.com/antialias/soroban-abacus-flashcards/commit/07212e4df0c7fd4b8cccf935c48b14164df6961d))
|
||||
* add player count to stacked room info ([540f6b7](https://github.com/antialias/soroban-abacus-flashcards/commit/540f6b76d05f561baa581d67070ab43134d8b5f6))
|
||||
* add prominent join request approval notifications for room moderators ([036da6d](https://github.com/antialias/soroban-abacus-flashcards/commit/036da6de66ca7d3f459c55df657b04a9e88d9cd3))
|
||||
* add real-time socket updates for moderation events ([86ceba3](https://github.com/antialias/soroban-abacus-flashcards/commit/86ceba3df3d39812d63fbf5d03fc37d8c3a75027))
|
||||
* add room access modes and ownership transfer ([6ff21c4](https://github.com/antialias/soroban-abacus-flashcards/commit/6ff21c4f1dd0dd1db14257612809b4d40512689a))
|
||||
* add room creation and join flow UI ([7f95032](https://github.com/antialias/soroban-abacus-flashcards/commit/7f950322530e8deb2e330d0d2147d1a20fa1e642))
|
||||
* add socket listener and polling for approval notifications ([35b4a72](https://github.com/antialias/soroban-abacus-flashcards/commit/35b4a72c8b2f80a74b5d2fe02b048d4ec4d1d6f2))
|
||||
* add waiting state for approval requests in JoinRoomModal ([f9b0429](https://github.com/antialias/soroban-abacus-flashcards/commit/f9b0429a2e2d22944acba66009dd87a9d9eb28c2))
|
||||
* adjust tier probabilities for more abacus flavor ([49219e3](https://github.com/antialias/soroban-abacus-flashcards/commit/49219e34cde32736155a11929d10581e783cba69))
|
||||
* **arcade:** add Card Sorting Challenge game scaffolding ([df37260](https://github.com/antialias/soroban-abacus-flashcards/commit/df37260e26bbb146493e0834e093afd98fa3f2a4))
|
||||
* **arcade:** add Change Game functionality for room hosts ([ee39241](https://github.com/antialias/soroban-abacus-flashcards/commit/ee39241e3c9e04202592497d9987eafcb89c00c9))
|
||||
* **arcade:** add game selection screen with navigation to room page ([4124f1c](https://github.com/antialias/soroban-abacus-flashcards/commit/4124f1cc081f5cb9d6f450f3c2e0cca8a247deba))
|
||||
* **arcade:** add Math Sprint game implementation ([e5be09e](https://github.com/antialias/soroban-abacus-flashcards/commit/e5be09ef5f170c7544557f75b9eca17bb2069246))
|
||||
* **arcade:** add modular game SDK and registry system ([de30bec](https://github.com/antialias/soroban-abacus-flashcards/commit/de30bec47923565fe5d1d5a6f719f3fc4e9d1509))
|
||||
* **arcade:** add Number Guesser demo game with plugin architecture ([0e3c058](https://github.com/antialias/soroban-abacus-flashcards/commit/0e3c0587073a69574a50f05c467f2499296012bf))
|
||||
* **arcade:** broadcast game selection changes to all room members ([b99e754](https://github.com/antialias/soroban-abacus-flashcards/commit/b99e7543952bb0d47f42e79dc4226b3c1280a0ee))
|
||||
* **arcade:** migrate matching pairs - phases 1-4 and 7 complete ([2a3af97](https://github.com/antialias/soroban-abacus-flashcards/commit/2a3af973f70ff07de30b38bbe1cdc549a971846f))
|
||||
* **arcade:** migrate memory-quiz to modular game system ([f48c37a](https://github.com/antialias/soroban-abacus-flashcards/commit/f48c37accccb88e790c7a1b438fd0566e7120e11))
|
||||
* **arcade:** register Math Sprint in game system ([0c05a7c](https://github.com/antialias/soroban-abacus-flashcards/commit/0c05a7c6bbc8d6f6e1f92e15e691d7e1aba0d8f7)), closes [#2](https://github.com/antialias/soroban-abacus-flashcards/issues/2) [#3](https://github.com/antialias/soroban-abacus-flashcards/issues/3)
|
||||
* **card-sorting:** add spectator mode UX enhancements ([4ab093a](https://github.com/antialias/soroban-abacus-flashcards/commit/4ab093a9d8ba5b290da44aaa6aa71ad7d7149b32))
|
||||
* **card-sorting:** add UI components and fix AbacusReact props ([d249ec0](https://github.com/antialias/soroban-abacus-flashcards/commit/d249ec0e5ff4610f55f35f762d726e0c98ac366c))
|
||||
* **card-sorting:** implement Provider with arcade session integration ([7f6fea9](https://github.com/antialias/soroban-abacus-flashcards/commit/7f6fea91f6dcc69a173eea86bcefc9921f1c1664))
|
||||
* **complement-race:** add infinite win condition for Steam Sprint mode ([d8fdfee](https://github.com/antialias/soroban-abacus-flashcards/commit/d8fdfeef74a5d3bb9684254af1c9d64d264b46ad))
|
||||
* **complement-race:** add mini app navigation bar ([ed0ef2d](https://github.com/antialias/soroban-abacus-flashcards/commit/ed0ef2d3b87324470d06b3246652967544caec26))
|
||||
* **complement-race:** enable adaptive AI difficulty in arcade ([55010d2](https://github.com/antialias/soroban-abacus-flashcards/commit/55010d2bcd953718d8fea428b1f7f613a193779c))
|
||||
* **complement-race:** implement state adapter for multiplayer support ([13882bd](https://github.com/antialias/soroban-abacus-flashcards/commit/13882bda3258d68a817473d7d830381f02553043))
|
||||
* **complement-race:** restore AI opponents in practice and survival modes ([325e07d](https://github.com/antialias/soroban-abacus-flashcards/commit/325e07de5929169aa333ef16f7bca5b41eeb1622))
|
||||
* **homepage:** add animated mini abacus to "Read and set numbers" card ([e028e34](https://github.com/antialias/soroban-abacus-flashcards/commit/e028e342ad4bc01491e05a4ba074628155926fd8))
|
||||
* **homepage:** add more visual embellishments to learning cards ([4ec1b95](https://github.com/antialias/soroban-abacus-flashcards/commit/4ec1b952f202d50f6db287c41732ec65ca17c142))
|
||||
* **homepage:** enhance "What You'll Learn" with visual cards ([d142342](https://github.com/antialias/soroban-abacus-flashcards/commit/d1423420e653b26b2f89d9d17ae5d597807d6979))
|
||||
* **home:** redesign home page to showcase complete platform ([ee6c4f2](https://github.com/antialias/soroban-abacus-flashcards/commit/ee6c4f2f4f39e3b30f59c54866c3857c218fb80f))
|
||||
* implement approval request flow for share links ([4a6b3ca](https://github.com/antialias/soroban-abacus-flashcards/commit/4a6b3cabe5c6aa42f4fa00ed09f9b3713f097539))
|
||||
* implement avatar-themed name generation with probabilistic mixing ([76a8472](https://github.com/antialias/soroban-abacus-flashcards/commit/76a8472f12d251071b97f2288f62f0b358576232))
|
||||
* implement proper retired room behavior with member expulsion ([a2d5368](https://github.com/antialias/soroban-abacus-flashcards/commit/a2d53680f27db04b2cd09973e62a76c5a7d4ce06))
|
||||
* improve arcade nav player grouping and add room join code display ([8e9980d](https://github.com/antialias/soroban-abacus-flashcards/commit/8e9980dc82ba2ab4ce973fc2c7ed259a20af9b19))
|
||||
* improve room creation UX and add password support for share links ([dcbb507](https://github.com/antialias/soroban-abacus-flashcards/commit/dcbb5072d8e0a12838fe70e3faa85f94cd63b0c1))
|
||||
* integrate moderation system into arcade pages ([087652f](https://github.com/antialias/soroban-abacus-flashcards/commit/087652f9e7091a93c02906162275ef88ec5e44c6))
|
||||
* **levels:** add Dan levels ladder visualization ([c18012c](https://github.com/antialias/soroban-abacus-flashcards/commit/c18012cb505a1f2a86ebed7579b379a4d7d97f2c))
|
||||
* **levels:** add dark mode styling and responsive scaling to abacus ([92e1e62](https://github.com/antialias/soroban-abacus-flashcards/commit/92e1e621321039206f65b3605f5797bbdc6beafc))
|
||||
* **levels:** add informational footer section ([0b1bff7](https://github.com/antialias/soroban-abacus-flashcards/commit/0b1bff7eab8f5da84ae309dbda336e168c2fe3fd))
|
||||
* **levels:** add Kyu & Dan levels page with homepage link ([39b1e7d](https://github.com/antialias/soroban-abacus-flashcards/commit/39b1e7de16f15412c91cf648c714e31e2de7a6bc))
|
||||
* **levels:** add kyu level data and cards ([6463a3b](https://github.com/antialias/soroban-abacus-flashcards/commit/6463a3b2f6371ebebac1048197fb44178997d2ef))
|
||||
* **levels:** create true horizontal slider with abacus visualizations ([6d734f1](https://github.com/antialias/soroban-abacus-flashcards/commit/6d734f1d51f5ba1367f55923e58bd977413d754e))
|
||||
* **levels:** implement interactive slider for exploring kyu & dan ranks ([eb3b100](https://github.com/antialias/soroban-abacus-flashcards/commit/eb3b1000563536d4143ba1f4ec04e59e8dd2e608))
|
||||
* **levels:** replace kyu grid with interactive slider and abacus visualizations ([10978e8](https://github.com/antialias/soroban-abacus-flashcards/commit/10978e890beee65dea78ddcce52cfe5315d58063))
|
||||
* make home page abacus interactive with audio ([9a53d7e](https://github.com/antialias/soroban-abacus-flashcards/commit/9a53d7e5db18853aca4e2e0c7abc799217feaecf))
|
||||
* **matching:** use nav avatars as turn indicators ([7263828](https://github.com/antialias/soroban-abacus-flashcards/commit/7263828ed494a6487999c8436af53618715b3864))
|
||||
* **math-sprint:** add game manifest ([1eefcc8](https://github.com/antialias/soroban-abacus-flashcards/commit/1eefcc89a58b79f928932a7425d6b88fb45a5526))
|
||||
* **memory-quiz:** add multiplayer support with redesigned scoreboards ([1cf4469](https://github.com/antialias/soroban-abacus-flashcards/commit/1cf44696c26473ce4ab2fc2039ff42f08c20edb6))
|
||||
* **memory-quiz:** persist game settings per-game across sessions ([05a8e0a](https://github.com/antialias/soroban-abacus-flashcards/commit/05a8e0a84272c6c45a4014413ee00726eb88b76a))
|
||||
* **memory-quiz:** show player emojis on cards to indicate who found them ([05bd11a](https://github.com/antialias/soroban-abacus-flashcards/commit/05bd11a133706c9ed8c09c744da7ca8955fa979a))
|
||||
* **moderation:** add inline feedback and persistent password display ([86e3d41](https://github.com/antialias/soroban-abacus-flashcards/commit/86e3d4199628f95048b9265c9de0adfdc2934f93))
|
||||
* **moderation:** improve password input with copy button ([2580e47](https://github.com/antialias/soroban-abacus-flashcards/commit/2580e474d08bf91477339e998b2c70962a633f41))
|
||||
* **nav:** add prominent turn indicator arrow badge ([f574558](https://github.com/antialias/soroban-abacus-flashcards/commit/f574558dffe22a1ecf06ee44d37b5eb1a20011b9))
|
||||
* **nav:** add pulsing indicator for offline network players ([64fb30e](https://github.com/antialias/soroban-abacus-flashcards/commit/64fb30e7eca7b485687329b9dd3d9e90ac507e2d))
|
||||
* **nav:** add turn indicators to network players ([623314b](https://github.com/antialias/soroban-abacus-flashcards/commit/623314bd383a54c57ca93ad9f2d8620cf89412e9))
|
||||
* **nav:** add turn label text under current player avatars ([52a66d5](https://github.com/antialias/soroban-abacus-flashcards/commit/52a66d5f6869e760f2a8914a6d39d21d47cfb7f4))
|
||||
* **nav:** center game context with hamburger menu for utilities ([a35a7d5](https://github.com/antialias/soroban-abacus-flashcards/commit/a35a7d56df945a5b15c1ddfa8b489c0d292e71c4))
|
||||
* **nav:** combine room info and network players in single pane ([d5473ab](https://github.com/antialias/soroban-abacus-flashcards/commit/d5473ab66a7c8c78f10cef9bb7084fc311520b2c))
|
||||
* **nav:** unify room dropdown with join code and game menu ([f7b83f8](https://github.com/antialias/soroban-abacus-flashcards/commit/f7b83f8c149b532321251abed98a94874196b2f5))
|
||||
* prevent invitations to retired rooms ([a7c3c1f](https://github.com/antialias/soroban-abacus-flashcards/commit/a7c3c1f4cd802985c8f040bc1cdf3ea4482a2fce))
|
||||
* redesign home page with component showcase ([29af265](https://github.com/antialias/soroban-abacus-flashcards/commit/29af265958f9fdab0253b92e153c01575840454d))
|
||||
* redesign homepage with educational vision and interactive demo ([2f09cb5](https://github.com/antialias/soroban-abacus-flashcards/commit/2f09cb5539f2bb0b8c77359c6f774c3742313e1e))
|
||||
* redesign room info as compact inline badge with click-to-copy ([6b3a440](https://github.com/antialias/soroban-abacus-flashcards/commit/6b3a4403695cc2f32df684005784a11f054827ff))
|
||||
* replace access mode dropdown with visual button grid ([e5d0672](https://github.com/antialias/soroban-abacus-flashcards/commit/e5d067205989d7c3105998dcd7d67fd0408f332c))
|
||||
* **tutorial:** add dark mode styling for coaching bar and abacus frame ([7e2f580](https://github.com/antialias/soroban-abacus-flashcards/commit/7e2f580877af9d21409f427778fa3569c950fcf5))
|
||||
* **tutorial:** add dark theme and column control props ([d42f9b2](https://github.com/antialias/soroban-abacus-flashcards/commit/d42f9b2d9ad630826c55b753dc581c469e8f9083))
|
||||
* **tutorial:** add fill color support for dark mode column posts and reckoning bar ([2eb3ff3](https://github.com/antialias/soroban-abacus-flashcards/commit/2eb3ff340613301df20bf14f5b461371a27d7f05))
|
||||
* **tutorial:** add hideNavigation prop to TutorialPlayer ([79ea52a](https://github.com/antialias/soroban-abacus-flashcards/commit/79ea52af80c8cbb482bbdd87f77caf32ada737ee))
|
||||
* **tutorial:** add hideTooltip prop and improve dark mode coaching bar ([1ee25b3](https://github.com/antialias/soroban-abacus-flashcards/commit/1ee25b3dd2f0ee9dd7ed571ba818b7ca5a247f85))
|
||||
* **tutorial:** add silentErrors prop to suppress error messages ([8835e1c](https://github.com/antialias/soroban-abacus-flashcards/commit/8835e1c57ab8adcecefe0db082360dd98fbfaac7))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* **nav:** restore original room creation/join behavior ([710e93c](https://github.com/antialias/soroban-abacus-flashcards/commit/710e93c9972339885b8f4ca02ecd2c1cdf65c040))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **db:** Database schemas now accept any string for game names
|
||||
* Added DELETE /api/arcade/rooms/:roomId/invite endpoint for declining invitations
|
||||
|
||||
Authorization Error Handling:
|
||||
- ModerationPanel: Parse and display API error messages (kick, ban, unban, invite, data loading)
|
||||
- PendingInvitations: Parse and display API error messages (decline, fetch)
|
||||
- All moderation actions now show specific auth errors like "Only the host can kick users"
|
||||
|
||||
New Endpoint:
|
||||
- DELETE /api/arcade/rooms/:roomId/invite: Allow users to decline their pending invitations
|
||||
* Validates invitation exists and is pending
|
||||
* Only invited user can decline their own invitation
|
||||
* Returns proper error messages for auth failures
|
||||
|
||||
Bug Fix:
|
||||
- Fixed invitations/pending/route.ts ban check query (removed reference to non-existent unbannedAt field)
|
||||
- Ban records are deleted when unbanned, so any existing ban is active
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
|
||||
# [1.8.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.7.0...abacus-react-v1.8.0) (2025-10-10)
|
||||
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ export interface AbacusCallbacks {
|
||||
value: number,
|
||||
event: React.MouseEvent,
|
||||
) => void;
|
||||
onValueChange?: (newValue: number) => void;
|
||||
onValueChange?: (newValue: number | bigint) => void;
|
||||
onBeadRef?: (bead: BeadConfig, element: SVGElement | null) => void;
|
||||
// Legacy callback for backward compatibility
|
||||
onClick?: (bead: BeadConfig) => void;
|
||||
@@ -240,7 +240,7 @@ export interface AbacusOverlay {
|
||||
|
||||
export interface AbacusConfig {
|
||||
// Basic configuration
|
||||
value?: number;
|
||||
value?: number | bigint;
|
||||
columns?: number | "auto";
|
||||
showEmptyColumns?: boolean;
|
||||
hideInactiveBeads?: boolean;
|
||||
@@ -271,7 +271,7 @@ export interface AbacusConfig {
|
||||
|
||||
// Legacy callbacks for backward compatibility
|
||||
onClick?: (bead: BeadConfig) => void;
|
||||
onValueChange?: (newValue: number) => void;
|
||||
onValueChange?: (newValue: number | bigint) => void;
|
||||
}
|
||||
|
||||
export interface AbacusDimensions {
|
||||
@@ -474,18 +474,23 @@ export function useAbacusState(
|
||||
|
||||
// NEW: Native place-value state management hook (eliminates the column index nightmare!)
|
||||
export function useAbacusPlaceStates(
|
||||
controlledValue: number = 0,
|
||||
controlledValue: number | bigint = 0,
|
||||
maxPlaceValue: ValidPlaceValues = 4,
|
||||
) {
|
||||
// Initialize state from value using place values as keys - NO MORE ARRAY INDICES!
|
||||
const initializeFromValue = useCallback(
|
||||
(value: number): PlaceStatesMap => {
|
||||
(value: number | bigint): PlaceStatesMap => {
|
||||
const states = new Map<ValidPlaceValues, PlaceState>();
|
||||
|
||||
// Convert to string to handle both number and bigint
|
||||
const valueStr = value.toString();
|
||||
const digits = valueStr.split('').map(Number);
|
||||
|
||||
// Always create ALL place values from 0 to maxPlaceValue (to match columns)
|
||||
for (let place = 0; place <= maxPlaceValue; place++) {
|
||||
const placeValueNum = Math.pow(10, place);
|
||||
const digit = Math.floor(value / placeValueNum) % 10;
|
||||
// Get digit from right: place 0 = rightmost, place 1 = second from right, etc.
|
||||
const digitIndex = digits.length - 1 - place;
|
||||
const digit = digitIndex >= 0 ? digits[digitIndex] : 0;
|
||||
|
||||
states.set(place as ValidPlaceValues, {
|
||||
placeValue: place as ValidPlaceValues,
|
||||
@@ -504,18 +509,32 @@ export function useAbacusPlaceStates(
|
||||
);
|
||||
|
||||
// Calculate current value from place states - NO MORE INDEX MATH!
|
||||
// Use BigInt for numbers that exceed safe integer range (>15 digits)
|
||||
const value = useMemo(() => {
|
||||
let total = 0;
|
||||
placeStates.forEach((state) => {
|
||||
const placeValueNum = Math.pow(10, state.placeValue);
|
||||
const digitValue = (state.heavenActive ? 5 : 0) + state.earthActive;
|
||||
total += digitValue * placeValueNum;
|
||||
});
|
||||
return total;
|
||||
}, [placeStates]);
|
||||
// Check if we need BigInt (maxPlaceValue > 14 means >15 digits)
|
||||
const useBigInt = maxPlaceValue > 14;
|
||||
|
||||
if (useBigInt) {
|
||||
let total = 0n;
|
||||
placeStates.forEach((state) => {
|
||||
const placeValueNum = 10n ** BigInt(state.placeValue);
|
||||
const digitValue = BigInt((state.heavenActive ? 5 : 0) + state.earthActive);
|
||||
total += digitValue * placeValueNum;
|
||||
});
|
||||
return total;
|
||||
} else {
|
||||
let total = 0;
|
||||
placeStates.forEach((state) => {
|
||||
const placeValueNum = Math.pow(10, state.placeValue);
|
||||
const digitValue = (state.heavenActive ? 5 : 0) + state.earthActive;
|
||||
total += digitValue * placeValueNum;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
}, [placeStates, maxPlaceValue]);
|
||||
|
||||
const setValue = useCallback(
|
||||
(newValue: number) => {
|
||||
(newValue: number | bigint) => {
|
||||
setPlaceStates(initializeFromValue(newValue));
|
||||
},
|
||||
[initializeFromValue],
|
||||
@@ -531,6 +550,37 @@ export function useAbacusPlaceStates(
|
||||
}
|
||||
}, [controlledValue, initializeFromValue, value]);
|
||||
|
||||
// Clean up place states when maxPlaceValue decreases (columns decrease)
|
||||
// This prevents stale place values from causing out-of-bounds access
|
||||
React.useEffect(() => {
|
||||
setPlaceStates((prev) => {
|
||||
const newStates = new Map(prev);
|
||||
let hasChanges = false;
|
||||
|
||||
// Remove any place values greater than maxPlaceValue
|
||||
for (const placeValue of newStates.keys()) {
|
||||
if (placeValue > maxPlaceValue) {
|
||||
newStates.delete(placeValue);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing place values up to maxPlaceValue
|
||||
for (let place = 0; place <= maxPlaceValue; place++) {
|
||||
if (!newStates.has(place as ValidPlaceValues)) {
|
||||
newStates.set(place as ValidPlaceValues, {
|
||||
placeValue: place as ValidPlaceValues,
|
||||
heavenActive: false,
|
||||
earthActive: 0,
|
||||
});
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? newStates : prev;
|
||||
});
|
||||
}, [maxPlaceValue]);
|
||||
|
||||
const getPlaceState = useCallback(
|
||||
(placeValue: ValidPlaceValues): PlaceState => {
|
||||
return (
|
||||
@@ -1058,13 +1108,15 @@ function calculateBeadStates(
|
||||
// NEW: Native place-value bead state calculation (eliminates array index math!)
|
||||
function calculateBeadStatesFromPlaces(
|
||||
placeStates: PlaceStatesMap,
|
||||
maxPlaceValue: ValidPlaceValues,
|
||||
): BeadConfig[][] {
|
||||
const columnsList: BeadConfig[][] = [];
|
||||
|
||||
// Convert Map to sorted array by place value (ascending order for correct visual layout)
|
||||
const sortedPlaces = Array.from(placeStates.entries()).sort(
|
||||
([a], [b]) => a - b,
|
||||
);
|
||||
// Filter to only include place values that are within the current column count
|
||||
const sortedPlaces = Array.from(placeStates.entries())
|
||||
.filter(([placeValue]) => placeValue <= maxPlaceValue)
|
||||
.sort(([a], [b]) => a - b);
|
||||
|
||||
for (const [placeValue, placeState] of sortedPlaces) {
|
||||
const beads: BeadConfig[] = [];
|
||||
@@ -1114,17 +1166,26 @@ function calculateValueFromColumnStates(
|
||||
}
|
||||
|
||||
// NEW: Native place-value calculation (eliminates the array index nightmare!)
|
||||
function calculateValueFromPlaceStates(placeStates: PlaceStatesMap): number {
|
||||
let value = 0;
|
||||
function calculateValueFromPlaceStates(placeStates: PlaceStatesMap): number | bigint {
|
||||
// Determine if we need BigInt based on the largest place value
|
||||
const maxPlace = Math.max(...Array.from(placeStates.keys()));
|
||||
const useBigInt = maxPlace > 14; // >15 digits
|
||||
|
||||
// Direct place value iteration - NO MORE ARRAY INDEX MATH!
|
||||
for (const [placeValue, placeState] of placeStates) {
|
||||
const digitValue =
|
||||
(placeState.heavenActive ? 5 : 0) + placeState.earthActive;
|
||||
value += digitValue * Math.pow(10, placeValue); // Direct place value - no conversion!
|
||||
if (useBigInt) {
|
||||
let value = 0n;
|
||||
for (const [placeValue, placeState] of placeStates) {
|
||||
const digitValue = BigInt((placeState.heavenActive ? 5 : 0) + placeState.earthActive);
|
||||
value += digitValue * (10n ** BigInt(placeValue));
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
let value = 0;
|
||||
for (const [placeValue, placeState] of placeStates) {
|
||||
const digitValue = (placeState.heavenActive ? 5 : 0) + placeState.earthActive;
|
||||
value += digitValue * Math.pow(10, placeValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Components
|
||||
@@ -1588,8 +1649,8 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
// Use new place-value bead calculation!
|
||||
const beadStates = useMemo(
|
||||
() => calculateBeadStatesFromPlaces(placeStates),
|
||||
[placeStates],
|
||||
() => calculateBeadStatesFromPlaces(placeStates, maxPlaceValue),
|
||||
[placeStates, maxPlaceValue],
|
||||
);
|
||||
|
||||
// Layout calculations using exact Typst positioning
|
||||
@@ -1939,6 +2000,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
{/* Background glow effects - rendered behind everything */}
|
||||
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex;
|
||||
const columnStyles = customStyles?.columns?.[colIndex];
|
||||
const backgroundGlow = columnStyles?.backgroundGlow;
|
||||
|
||||
@@ -1952,7 +2014,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`background-glow-${colIndex}`}
|
||||
key={`background-glow-pv${placeValue}`}
|
||||
x={x - glowWidth / 2}
|
||||
y={-(backgroundGlow.spread || 0) / 2}
|
||||
width={glowWidth}
|
||||
@@ -1970,6 +2032,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
{/* Rods - positioned as rectangles like in Typst */}
|
||||
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex;
|
||||
const x =
|
||||
colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
|
||||
@@ -2001,7 +2064,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`rod-${colIndex}`}
|
||||
key={`rod-pv${placeValue}`}
|
||||
x={x - dimensions.rodWidth / 2}
|
||||
y={rodStartY}
|
||||
width={dimensions.rodWidth}
|
||||
@@ -2056,6 +2119,15 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
} else {
|
||||
// Earth bead positioning - exact Typst formulas (lines 249-261)
|
||||
const columnState = columnStates[colIndex];
|
||||
if (!columnState) {
|
||||
throw new Error(
|
||||
`Invalid abacus state: columnState is undefined for column index ${colIndex}. ` +
|
||||
`effectiveColumns=${effectiveColumns}, columnStates.length=${columnStates.length}, ` +
|
||||
`beadStates.length=${beadStates.length}, placeValue=${bead.placeValue}. ` +
|
||||
`This indicates a mismatch between the number of columns and the bead states. ` +
|
||||
`Please report this issue with the abacus configuration that triggered it.`
|
||||
);
|
||||
}
|
||||
const earthActive = columnState.earthActive;
|
||||
|
||||
if (bead.active) {
|
||||
@@ -2134,7 +2206,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<Bead
|
||||
key={`bead-${colIndex}-${bead.type}-${beadIndex}`}
|
||||
key={`bead-pv${bead.placeValue}-${bead.type}-${bead.type === "earth" ? bead.position : 0}`}
|
||||
bead={bead}
|
||||
x={x}
|
||||
y={y}
|
||||
@@ -2208,6 +2280,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
{/* Background rectangles for place values - in SVG */}
|
||||
{finalConfig.showNumbers &&
|
||||
placeValues.map((value, columnIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - columnIndex;
|
||||
const x =
|
||||
columnIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
// Position background rectangles to match the text positioning
|
||||
@@ -2220,7 +2293,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`place-bg-${columnIndex}`}
|
||||
key={`place-bg-pv${placeValue}`}
|
||||
x={x - 12 * finalConfig.scaleFactor}
|
||||
y={y - 12 * finalConfig.scaleFactor}
|
||||
width={24 * finalConfig.scaleFactor}
|
||||
@@ -2248,6 +2321,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
{/* NumberFlow place value displays - inside SVG using foreignObject */}
|
||||
{finalConfig.showNumbers &&
|
||||
placeValues.map((value, columnIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - columnIndex;
|
||||
const x =
|
||||
columnIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
// Position numbers within the allocated numbers space (below the baseHeight)
|
||||
@@ -2259,7 +2333,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<foreignObject
|
||||
key={`place-number-${columnIndex}`}
|
||||
key={`place-number-pv${placeValue}`}
|
||||
x={x - 12 * finalConfig.scaleFactor}
|
||||
y={y - 8 * finalConfig.scaleFactor}
|
||||
width={24 * finalConfig.scaleFactor}
|
||||
@@ -2320,6 +2394,12 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
if (targetBeadType === "heaven") {
|
||||
const columnState = columnStates[targetColumn];
|
||||
if (!columnState) {
|
||||
console.error(
|
||||
`Invalid abacus overlay: columnState is undefined for overlay targeting column ${targetColumn}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
y = columnState.heavenActive
|
||||
? dimensions.heavenEarthGap -
|
||||
dimensions.beadSize / 2 -
|
||||
@@ -2332,6 +2412,12 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
targetBeadPosition !== undefined
|
||||
) {
|
||||
const columnState = columnStates[targetColumn];
|
||||
if (!columnState) {
|
||||
console.error(
|
||||
`Invalid abacus overlay: columnState is undefined for overlay targeting column ${targetColumn}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const earthActive = columnState.earthActive;
|
||||
const isActive = targetBeadPosition < earthActive;
|
||||
|
||||
@@ -2403,6 +2489,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
{/* Column interaction areas - rendered last to be on top of all other elements */}
|
||||
{Array.from({ length: effectiveColumns }, (_, colIndex) => {
|
||||
const placeValue = effectiveColumns - 1 - colIndex;
|
||||
const x =
|
||||
colIndex * dimensions.rodSpacing + dimensions.rodSpacing / 2;
|
||||
const columnStyles = customStyles?.columns?.[colIndex];
|
||||
@@ -2413,7 +2500,7 @@ export const AbacusReact: React.FC<AbacusConfig> = ({
|
||||
|
||||
return (
|
||||
<rect
|
||||
key={`column-interaction-${colIndex}`}
|
||||
key={`column-interaction-pv${placeValue}`}
|
||||
x={x - backgroundWidth / 2}
|
||||
y={0}
|
||||
width={backgroundWidth}
|
||||
|
||||
147
packages/abacus-react/src/StandaloneBead.tsx
Normal file
147
packages/abacus-react/src/StandaloneBead.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useSpring, animated } from "@react-spring/web";
|
||||
import {
|
||||
useAbacusConfig,
|
||||
getDefaultAbacusConfig,
|
||||
type BeadShape,
|
||||
} from "./AbacusContext";
|
||||
|
||||
export interface StandaloneBeadProps {
|
||||
/** Size of the bead in pixels */
|
||||
size?: number;
|
||||
/** Override the shape from context (diamond, circle, square) */
|
||||
shape?: BeadShape;
|
||||
/** Override the color from context */
|
||||
color?: string;
|
||||
/** Enable animation */
|
||||
animated?: boolean;
|
||||
/** Custom className */
|
||||
className?: string;
|
||||
/** Custom style */
|
||||
style?: React.CSSProperties;
|
||||
/** Active state for the bead */
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standalone Bead component that respects the AbacusDisplayContext.
|
||||
* This component renders a single abacus bead with styling from the context.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* import { StandaloneBead, AbacusDisplayProvider } from '@soroban/abacus-react';
|
||||
*
|
||||
* <AbacusDisplayProvider>
|
||||
* <StandaloneBead size={28} color="#8b5cf6" />
|
||||
* </AbacusDisplayProvider>
|
||||
* ```
|
||||
*/
|
||||
export const StandaloneBead: React.FC<StandaloneBeadProps> = ({
|
||||
size = 28,
|
||||
shape: shapeProp,
|
||||
color: colorProp,
|
||||
animated: animatedProp,
|
||||
className,
|
||||
style,
|
||||
active = true,
|
||||
}) => {
|
||||
// Try to use context config, fallback to defaults if no context
|
||||
let contextConfig;
|
||||
try {
|
||||
contextConfig = useAbacusConfig();
|
||||
} catch {
|
||||
// No context provider, use defaults
|
||||
contextConfig = getDefaultAbacusConfig();
|
||||
}
|
||||
|
||||
// Use props if provided, otherwise fall back to context config
|
||||
const shape = shapeProp ?? contextConfig.beadShape;
|
||||
const enableAnimation = animatedProp ?? contextConfig.animated;
|
||||
const color = colorProp ?? "#000000";
|
||||
|
||||
const [springs, api] = useSpring(() => ({
|
||||
scale: 1,
|
||||
config: { tension: 300, friction: 20 },
|
||||
}));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enableAnimation) {
|
||||
api.start({ scale: 1 });
|
||||
}
|
||||
}, [enableAnimation, api]);
|
||||
|
||||
const renderShape = () => {
|
||||
const halfSize = size / 2;
|
||||
const actualColor = active ? color : "rgb(211, 211, 211)";
|
||||
|
||||
switch (shape) {
|
||||
case "diamond":
|
||||
return (
|
||||
<polygon
|
||||
points={`${size * 0.7},0 ${size * 1.4},${halfSize} ${size * 0.7},${size} 0,${halfSize}`}
|
||||
fill={actualColor}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
);
|
||||
case "square":
|
||||
return (
|
||||
<rect
|
||||
width={size}
|
||||
height={size}
|
||||
fill={actualColor}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
rx="1"
|
||||
/>
|
||||
);
|
||||
case "circle":
|
||||
default:
|
||||
return (
|
||||
<circle
|
||||
cx={halfSize}
|
||||
cy={halfSize}
|
||||
r={halfSize}
|
||||
fill={actualColor}
|
||||
stroke="#000"
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getXOffset = () => {
|
||||
return shape === "diamond" ? size * 0.7 : size / 2;
|
||||
};
|
||||
|
||||
const getYOffset = () => {
|
||||
return size / 2;
|
||||
};
|
||||
|
||||
const AnimatedG = animated.g;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size * (shape === "diamond" ? 1.4 : 1)}
|
||||
height={size}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
<AnimatedG
|
||||
transform={`translate(0, 0)`}
|
||||
style={
|
||||
enableAnimation
|
||||
? {
|
||||
transform: springs.scale.to((s) => `scale(${s})`),
|
||||
transformOrigin: "center",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{renderShape()}
|
||||
</AnimatedG>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -14,3 +14,6 @@ export type {
|
||||
AbacusDisplayConfig,
|
||||
AbacusDisplayContextType,
|
||||
} from "./AbacusContext";
|
||||
|
||||
export { StandaloneBead } from "./StandaloneBead";
|
||||
export type { StandaloneBeadProps } from "./StandaloneBead";
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -134,6 +134,9 @@ importers:
|
||||
'@types/jsdom':
|
||||
specifier: ^21.1.7
|
||||
version: 21.1.7
|
||||
'@use-gesture/react':
|
||||
specifier: ^10.3.1
|
||||
version: 10.3.1(react@18.3.1)
|
||||
bcryptjs:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
|
||||
Reference in New Issue
Block a user