Compare commits

..

97 Commits

Author SHA1 Message Date
semantic-release-bot
11aa44d882 chore(release): 4.61.1 [skip ci]
## [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](30e16c8e5a))
2025-10-21 15:37:33 +00:00
Thomas Hallock
30e16c8e5a fix(flashcards): fix position snap-back by using api.set before decay
Fixed the issue where cards were snapping back to pickup position:
- Use api.set() to immediately snap spring position to dropped location
- Then apply decay animation with momentum from that position
- Removed problematic 'from' property which doesn't work with decay

The bug was that react-spring's 'from' property is ignored when using
decay: true, causing the spring to animate from its current value rather
than the specified position. Using api.set() first ensures the spring
starts from the correct dropped position.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:36:07 -05:00
semantic-release-bot
86357b3d7a chore(release): 4.61.0 [skip ci]
## [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](ad1ad690f0))
2025-10-21 15:32:18 +00:00
Thomas Hallock
ad1ad690f0 feat(flashcards): enable unbounded drag and position persistence
Made interactive flashcards a fun easter egg by:
- Changed overflow from 'hidden' to 'visible' to allow cards to be
  thrown anywhere on the page, not just within container bounds
- Fixed position persistence: cards now stay exactly where dropped
  instead of snapping back to pickup location
- Updated currentPositionRef immediately on drop before applying
  momentum physics

Cards can now be dragged and tossed freely around the entire page
and will maintain their position after being dropped.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:31:08 -05:00
semantic-release-bot
53475cf40e chore(release): 4.60.0 [skip ci]
## [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](424f41d4bf))
2025-10-21 15:29:16 +00:00
Thomas Hallock
424f41d4bf feat(homepage): significantly increase mobile hero abacus size
Increased mobile scale from 2.5 to 3.5 to better utilize available screen
space on mobile devices. Screenshot review showed ample room above and
below the abacus without risk of overlapping title or scroll hint.

Final scales:
- Mobile (base): scale(3.5) - 75% larger than original
- Medium (md): scale(3.5) - 16% larger than original
- Desktop (lg): scale(4.25) - 6% larger than original

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:27:53 -05:00
semantic-release-bot
4c6939807e chore(release): 4.59.1 [skip ci]
## [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](86dee31c9a))
* **homepage:** reduce mobile abacus scale to prevent scroll hint overlap ([b8235be](b8235be612))
2025-10-21 15:27:10 +00:00
Thomas Hallock
b8235be612 fix(homepage): reduce mobile abacus scale to prevent scroll hint overlap
Reduced mobile scale from 2.8 to 2.5 to ensure the "Scroll to explore"
hint at the bottom is not covered by the abacus.

Final scales:
- Mobile (base): scale(2.5) - 25% larger than original, no overlap
- Medium (md): scale(3.5) - 16% larger than original
- Desktop (lg): scale(4.25) - 6% larger than original

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:25:50 -05:00
Thomas Hallock
86dee31c9a fix(homepage): adjust hero abacus scale for optimal sizing across devices
Fine-tuned scale values based on visual feedback:

- Mobile (base): scale(2.8) - increased for better mobile visibility (40% larger than original 2)
- Medium screens (md): scale(3.5) - unchanged (16% larger than original 3)
- Desktop (lg): scale(4.25) - reduced 15% for safety (6% larger than original 4)

This balances maximum visual impact on mobile while preventing any
layout issues on larger screens.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:25:00 -05:00
semantic-release-bot
b401bb5fa4 chore(release): 4.59.0 [skip ci]
## [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](7666b0aea9))
2025-10-21 15:21:46 +00:00
Thomas Hallock
7666b0aea9 feat(homepage): increase hero abacus size for better visibility
Increase scale values across all breakpoints, especially on mobile:
- Mobile (base): scale(2) → scale(3) (50% increase)
- Medium screens (md): scale(3) → scale(4.5) (50% increase)
- Large screens (lg): scale(4) → scale(6) (50% increase)

The abacus now fills more of the available hero space without
clipping, improving visual impact and usability on all devices.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:19:55 -05:00
semantic-release-bot
39afa455de chore(release): 4.58.2 [skip ci]
## [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](a58f7b78b0))
2025-10-21 15:02:58 +00:00
Thomas Hallock
a58f7b78b0 refactor(navbar): prevent subtitle wrap and remove abacus emoji
Two small refinements to navbar branding:
- Added whiteSpace: 'nowrap' to subtitle to prevent text wrapping
- Removed abacus emoji from "Abaci One" branding for cleaner look

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 10:01:40 -05:00
semantic-release-bot
1c001e07b7 chore(release): 4.58.1 [skip ci]
## [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](8893675b36))
2025-10-21 14:58:54 +00:00
Thomas Hallock
8893675b36 fix(navbar): apply glassmorphism to transparent mode, not scrolled mode
Fixed logic - the glassmorphism effect (backdrop blur, subtle backgrounds,
borders, shadows) now applies when the navbar is transparent at the top of
the homepage (isTransparent=true), not when scrolled down.

When scrolled or on other pages, the links are simpler without the extra
glassmorphism styling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:57:41 -05:00
semantic-release-bot
4254459238 chore(release): 4.58.0 [skip ci]
## [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](89b90723b7))
2025-10-21 14:56:51 +00:00
Thomas Hallock
89b90723b7 feat(navbar): add glassmorphism effect to nav links when scrolled
Enhanced nav links (Create, Guide, Games) with floating glass pill effect
when navbar is in scrolled mode:

- Added backdrop-filter blur(8px) to blur content underneath each link
- Subtle semi-transparent backgrounds (white for inactive, purple for active)
- Added borders with purple/white tints for enhanced definition
- Soft box shadows that glow purple on hover
- Enhanced hover states with stronger purple effects

The links now have better contrast against the dark navbar while maintaining
the ethereal floating illusion. Effects are only applied when scrolled (not
when navbar is transparent on homepage hero).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:55:31 -05:00
Thomas Hallock
6e5aec858f revert(navbar): remove bottom fade gradient
Removed the 10px gradient fade at the bottom of the navbar per user request.
Kept the border fix that removes the black line artifact when transparent.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:53:33 -05:00
semantic-release-bot
5611d148aa chore(release): 4.57.10 [skip ci]
## [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](d5f60ce9d2))

### Styles

* **navbar:** improve theming to match homepage dark aesthetic ([284fc90](284fc90a53))
2025-10-21 14:53:05 +00:00
Thomas Hallock
d5f60ce9d2 fix(navbar): remove border artifact and add 10px bottom fade
Fixed two visual issues with the navbar:

1. Removed black border line when transparent: Changed borderBottom to
   conditionally be 'none' instead of '1px solid transparent', which was
   showing up as a visible line when the navbar is transparent on homepage

2. Added 10px gradient fade at bottom: Applied linear-gradient that fades
   from the dark background to transparent over the last 10px, creating a
   softer transition instead of a sharp cut-off

The navbar now seamlessly blends into the page content with no visual artifacts.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:51:48 -05:00
Thomas Hallock
284fc90a53 style(navbar): improve theming to match homepage dark aesthetic
Updated AppNavBar styling to blend with homepage's dark glassmorphism theme:

- Changed navbar background from white to semi-transparent dark (rgba(0,0,0,0.5))
- Added backdrop-filter blur(12px) for glassmorphism effect
- Updated border color to purple accent (rgba(139,92,246,0.2))
- Changed logo/branding text to white/light purple tones
- Updated nav link colors from gray to light gray/purple palette
- Enhanced hover states with purple highlights (rgba(196,181,253))

The navbar now seamlessly integrates with the homepage's dark theme while
maintaining transparency when the hero section is visible.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:45:43 -05:00
semantic-release-bot
e54ea20dbe chore(release): 4.57.9 [skip ci]
## [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](415a1fb1fa))
2025-10-21 14:44:30 +00:00
Thomas Hallock
415a1fb1fa fix(homepage): add overflow hidden to Your Journey section
Prevents the level slider from causing horizontal overflow on mobile
devices, which was making the page width wider than intended.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:43:13 -05:00
semantic-release-bot
60b3a788b3 chore(release): 4.57.8 [skip ci]
## [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](62ff067bb9))
2025-10-21 14:33:47 +00:00
Thomas Hallock
62ff067bb9 fix(homepage): adjust responsive breakpoints to prevent skill card clipping
Changed layout breakpoints from md (768px) to xl (1280px) to ensure:
- Skills section doesn't get clipped at medium viewport sizes
- Layout only switches to side-by-side when there's sufficient space
- Container min-width is responsive (100% below xl, 1400px at xl+)

This prevents the issue where content was overflowing at intermediate
viewport widths.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-21 09:32:33 -05:00
semantic-release-bot
3d774c8d82 chore(release): 4.57.7 [skip ci]
## [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](61403f2f50))
* **homepage:** set min-width 1400px on container and remove max-width ([aa297d4](aa297d4ef7))
2025-10-21 03:07:11 +00:00
Thomas Hallock
61403f2f50 fix(homepage): restructure layout to center 1400px wide demo section
Moved "Learn by Doing" section outside the maxW:7xl container to allow
the demo div to be 1400px wide and properly centered with mx:auto.

Remaining content stays within the 7xl constrained container.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 22:06:02 -05:00
Thomas Hallock
aa297d4ef7 fix(homepage): set min-width 1400px on container and remove max-width
Added minW: '1400px' and removed maxW: '1200px' constraint to ensure
the "What You'll Learn" section is properly sized and centered.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 22:03:31 -05:00
semantic-release-bot
712d318e7c chore(release): 4.57.6 [skip ci]
## [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](cd3eb61cb5))
2025-10-21 02:36:21 +00:00
Thomas Hallock
cd3eb61cb5 fix(homepage): increase skill cards container width to prevent title wrapping
Increased container from 650px to 800px to ensure skill card titles
like "Friends techniques" don't wrap awkwardly across multiple lines.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 21:35:05 -05:00
semantic-release-bot
8871050990 chore(release): 4.57.5 [skip ci]
## [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](a6ac55b7b1))
2025-10-21 02:23:27 +00:00
Thomas Hallock
a6ac55b7b1 fix(homepage): prevent text overflow in skill cards
Fixed text being cut off on the right side of skill cards by:
- Adding minWidth: '0' to text container to enable proper flex shrinking
- Adding flexWrap: 'wrap' to title/badge row for better wrapping behavior

This fixes the flexbox issue where text was overflowing the card boundaries.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 21:22:17 -05:00
semantic-release-bot
64e2464ec1 chore(release): 4.57.4 [skip ci]
## [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](422bf3d968))
* **homepage:** make MiniAbacus fill container properly ([3b5d147](3b5d14765d))
* **homepage:** widen skill cards container to 650px ([bc1ad3a](bc1ad3a43a))
2025-10-21 00:30:13 +00:00
Thomas Hallock
422bf3d968 fix(homepage): align container width breakpoint with grid columns
Changed width breakpoint from md to lg to match when the grid switches
to 2 columns. This prevents overflow on medium-width screens where the
container was 650px wide but still showing 1-column layout.

Now:
- Below lg: 100% width, 1 column
- At lg+: 650px width, 2 columns

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:29:05 -05:00
Thomas Hallock
bc1ad3a43a fix(homepage): widen skill cards container to 650px
Increased the "What You'll Learn" container width from 420px to 650px
to give the 2-column grid of skill cards proper breathing room and
prevent cramped layouts.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:27:58 -05:00
Thomas Hallock
3b5d14765d fix(homepage): make MiniAbacus fill container properly
Fixed skill card abacus sizing by:
- Changing MiniAbacus to use width/height 100% instead of fixed 75px/80px
- Increased scale from 0.6 to 0.75 for better visibility
- Now properly fills the 120px/150px container set on the wrapper

This fixes the clipping issue by making the component hierarchy work correctly:
outer container (120px/150px) -> MiniAbacus (100%) -> scaled AbacusReact

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:26:45 -05:00
semantic-release-bot
9847f8f461 chore(release): 4.57.3 [skip ci]
## [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](57c212f4f5))
2025-10-21 00:25:47 +00:00
Thomas Hallock
57c212f4f5 fix(homepage): increase abacus container width to 120px/150px
Increase abacus container width from 95px/110px to 120px (mobile) and
150px (desktop) to properly accommodate 3-column abacus visualizations
without clipping.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:24:37 -05:00
semantic-release-bot
b2f5c19ce3 chore(release): 4.57.2 [skip ci]
## [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](e65e96952f))
2025-10-21 00:23:44 +00:00
Thomas Hallock
e65e96952f fix(homepage): increase skill card abacus container width
Remove overflow:hidden and increase the abacus container width from
75px to responsive widths (95px mobile, 110px desktop) to properly
accommodate the abacus visualizations without clipping.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:22:25 -05:00
semantic-release-bot
556a0eb194 chore(release): 4.57.1 [skip ci]
## [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](5070d8d64f))
2025-10-21 00:22:09 +00:00
Thomas Hallock
5070d8d64f fix(homepage): add overflow hidden to skill cards
Add overflow: hidden to skill card containers to properly contain
content within card bounds and prevent visual overflow issues.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:20:59 -05:00
semantic-release-bot
54cedbe03a chore(release): 4.57.0 [skip ci]
## [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](9ec0a71546))

### Bug Fixes

* **homepage:** prevent skill card overflow ([fa26acf](fa26acfbae))
2025-10-21 00:19:34 +00:00
Thomas Hallock
fa26acfbae fix(homepage): prevent skill card overflow
Change abacus container height to minHeight to prevent content
overflow in skill cards. This allows the container to grow as
needed while maintaining minimum dimensions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:18:23 -05:00
Thomas Hallock
9ec0a71546 feat(homepage): make skills section responsive with emojis
Update "What You'll Learn" section to display in a responsive grid:
- One column on mobile/tablet
- Two columns on larger screens (lg breakpoint)
- Increased padding and height on two-column layout
- Added emojis to skill titles for better visual appeal

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 19:17:26 -05:00
semantic-release-bot
6448249512 chore(release): 4.56.0 [skip ci]
## [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](a537bc18c3))
2025-10-20 23:52:50 +00:00
Thomas Hallock
a537bc18c3 feat(homepage): emphasize single-player and observer modes
Update arcade section subtitle to highlight both single-player
challenges and multiplayer battles. Also mention the ability to
invite friends to observe games live over the network.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:51:34 -05:00
semantic-release-bot
33ab7aaaf0 chore(release): 4.55.0 [skip ci]
## [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](f47b172f66))
2025-10-20 23:46:47 +00:00
Thomas Hallock
f47b172f66 feat(homepage): update section title to "The Arcade"
Replace "Available Now" section with "The Arcade" to better describe
the multiplayer room system. Updated subtitle to explain that users
can create or join rooms to play real-time games with friends over
the network.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:45:31 -05:00
semantic-release-bot
9d25e1dd35 chore(release): 4.54.0 [skip ci]
## [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](8a2d5ae319))

### Code Refactoring

* **levels:** extract levels slider into shared component ([015e30b](015e30b085))
2025-10-20 23:31:37 +00:00
Thomas Hallock
015e30b085 refactor(levels): extract levels slider into shared component
Consolidate the complete levels slider UI from /levels page into a reusable
LevelSliderDisplay component. This eliminates code duplication and provides
a single source of truth for the levels progression display.

Changes:
- Created LevelSliderDisplay component (862 lines) with all levels data,
  state management, animations, and UI
- Updated /levels page to use shared component (959 → 117 lines, 87.8% reduction)
- Updated homepage to use shared component (removed duplicate state/data)
- Deleted incomplete LevelsSlider component from previous commit

Component includes:
- Complete allLevels array (21 levels: 10th Kyu through 10th Dan)
- Interactive Radix slider with emoji tick marks
- StandaloneBead thumb with animated emoji transitions
- Level details for Kyu levels (Add/Sub, Multiply, Divide stats)
- Animated abacus display with speed scaling for Dan levels
- Auto-advance functionality (3s interval, pauses on hover)
- Dark theme styling

Both homepage and /levels page now use the same component with identical
behavior and appearance, eliminating maintenance overhead.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:30:22 -05:00
Thomas Hallock
8a2d5ae319 feat(homepage): add interactive levels slider to replace static progression
Replace the static emoji progression display in "Your Journey" section with
an interactive slider component adapted from /levels page. Features:

- Auto-advancing slider (3s intervals, pauses on hover)
- Interactive bead thumb with animated emoji overlay
- Animated abacus display that changes with level
- Shows 8 key milestone levels (10th Kyu through 10th Dan)
- Emoji tick marks above slider for visual navigation
- Dynamic border colors based on level category

Components:
- Created LevelsSlider component to encapsulate the UI
- Reuses existing React Spring animations from /levels page
- Uses Radix Slider for accessible interaction
- Dark theme abacus styling for consistency

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:30:22 -05:00
semantic-release-bot
d2ff2c6a29 chore(release): 4.53.0 [skip ci]
## [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](074488349a))

### Code Refactoring

* **homepage:** merge flashcard display with create button section ([3cf4f92](3cf4f92643))
2025-10-20 23:12:50 +00:00
Thomas Hallock
3cf4f92643 refactor(homepage): merge flashcard display with create button section
Combine the interactive flashcards demonstration and the "Create
Flashcards" call-to-action into a single unified section. The card
throwing area, feature highlights, and CTA button are now part of
one cohesive pane instead of two separate sections.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:11:32 -05:00
Thomas Hallock
074488349a feat(homepage): add interactive draggable flashcards with physics
Add physics-based flashcard component with drag, throw, and momentum:
- Random generation of 8-15 flashcards with abacus visualizations
- Smooth drag interactions with velocity-based rotation
- Decay physics for realistic throwing and sliding
- Transform-origin pivot point based on click position
- Position persistence so cards stay where thrown

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 18:11:32 -05:00
semantic-release-bot
30a5587bca chore(release): 4.52.2 [skip ci]
## [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](4082a246a3))
2025-10-20 22:44:22 +00:00
Thomas Hallock
4082a246a3 fix(homepage): use actual container dimensions for flashcard positioning
Fix flashcards being positioned outside visible area by using the
container's actual dimensions instead of hardcoded pixel values.

The previous implementation used CONTAINER_WIDTH=800px and
CONTAINER_HEIGHT=500px to position cards, but the actual container
width is 100% which varies by screen size. This caused cards to be
positioned outside the visible area on smaller screens.

Changes:
- Add containerRef to get actual container dimensions
- Calculate card positions based on offsetWidth/offsetHeight
- Remove hardcoded dimension constants
- Ensure cards stay within visible bounds with proper margins

This makes the flashcard positioning responsive to any screen size.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:43:06 -05:00
semantic-release-bot
9703fed94c chore(release): 4.52.1 [skip ci]
## [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](5dc636a71c))
2025-10-20 22:40:40 +00:00
Thomas Hallock
5dc636a71c fix(homepage): correct flashcard transform rendering
Fix interactive flashcards not rendering by properly converting
react-spring animated values to CSS transforms. The x, y, rotation,
and scale spring values now use the `to` helper to create proper
CSS transform strings.

Changes:
- Import `to` helper from @react-spring/web
- Convert spring values to CSS transform using translate/rotate/scale
- Set position to absolute with left:0, top:0 as transform origin

This fixes the issue where flashcards were invisible because the
spring values weren't being properly converted to CSS.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:39:20 -05:00
semantic-release-bot
16d978db9a chore(release): 4.52.0 [skip ci]
## [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](e711c52757))
2025-10-20 22:39:00 +00:00
Thomas Hallock
e711c52757 feat(homepage): add interactive draggable flashcards with physics
Add a fun, interactive flashcard display to the homepage's flashcard
generator section. Users can drag and throw 8-15 randomly generated
flashcards around with realistic physics-based momentum.

Features:
- Drag and drop flashcards with mouse/touch
- Throw cards with velocity-based physics
- 8-15 randomly generated flashcards (100-999 range)
- Real AbacusReact components for each card
- Client-side rendering to avoid hydration errors

Technical implementation:
- Uses @use-gesture/react for drag gesture handling
- Uses @react-spring/web for smooth physics animations
- Cards generated client-side with useEffect to prevent SSR mismatch
- Each card maintains its own spring-based position and rotation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:37:48 -05:00
semantic-release-bot
009162e22c chore(release): 4.51.0 [skip ci]
## [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](cd30944c5e))
2025-10-20 22:33:29 +00:00
Thomas Hallock
cd30944c5e feat(homepage): create fancy flashcard display with spread-out cards
Transform the flashcard generator section into an eye-catching display
featuring five rotated flashcards spread out in a fan pattern, each
showing real AbacusReact components with different numbers.

Features:
- Five flashcards (123, 456, 789, 321, 654) rotated at different angles
- Spread out horizontally for a dynamic, fanned-out effect
- Interactive hover effects - cards lift and scale on hover
- Real AbacusReact components showing actual bead positions
- Feature boxes highlighting capabilities (formats, customization, sizes)
- Blue glow effect on hover for the entire section
- Matches the visual style of other homepage sections

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:32:15 -05:00
semantic-release-bot
3e58cb5f92 chore(release): 4.50.1 [skip ci]
## [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](dc19622bbb))
* **homepage:** set fixed width for tutorial panel to prevent layout shift ([aba9f8a](aba9f8a94d))
2025-10-20 22:29:57 +00:00
Thomas Hallock
aba9f8a94d fix(homepage): set fixed width for tutorial panel to prevent layout shift
Set the tutorial panel (left side) to a fixed width of 500px on desktop
to prevent the layout from shifting when switching between tutorials
with different text lengths.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:28:48 -05:00
Thomas Hallock
dc19622bbb fix(homepage): set fixed width for learning panel to prevent layout shift
Changed the "What You'll Learn" panel from maxW to a fixed width (420px)
on desktop screens to prevent it from shifting when switching between
tutorials with different text lengths.

Also added minW: '0' to the tutorial panel to allow proper flex shrinking.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:27:58 -05:00
semantic-release-bot
1babfde328 chore(release): 4.50.0 [skip ci]
## [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](76d6f19d51))
2025-10-20 22:23:24 +00:00
Thomas Hallock
76d6f19d51 feat(homepage): add interactive learning panels with animated mini-tutorials
Transform "What You'll Learn" section into an interactive experience where
users can click skill panels to see corresponding tutorials with animated
abacus demonstrations.

Changes:
- Make skill panels clickable to switch between different tutorials
- Replace static emojis with animated MiniAbacus components for each skill
- Create skill-specific tutorials: basic numbers, friends of 5,
  multiplication, and mental calculation
- Add visual indication for selected panel (yellow glow effect)
- Update MiniAbacus component to cycle through custom value sequences
- Default to "Friends techniques" panel (Friends of 5 tutorial)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:22:11 -05:00
semantic-release-bot
9ad35e65d3 chore(release): 4.49.1 [skip ci]
## [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](d362a770d6))
2025-10-20 22:19:50 +00:00
Thomas Hallock
d362a770d6 refactor(homepage): streamline homepage sections
Simplified homepage by removing redundant sections and elevating
the flashcard creator to a standalone section.

Changes:
- Remove 'For Kids & Families' section entirely
- Remove 'Interactive Abacus' pane from Additional Tools
- Promote 'Flashcard Creator' to standalone top-level section

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:18:22 -05:00
semantic-release-bot
095cdda4ca chore(release): 4.49.0 [skip ci]
## [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](a1a135a858)), 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](7f516526fb))
2025-10-20 22:14:22 +00:00
Thomas Hallock
a1a135a858 feat: add vibrant gradients and equal heights to game cards
Updated the game theme system and game cards to have vibrant, eye-catching
gradients and consistent heights:

Theme System Changes:
- Updated all game themes from pastel to vibrant gradients
- Blue: Vibrant cyan (#4facfe to #00f2fe)
- Purple: Vibrant purple (#667eea to #764ba2)
- Pink: Vibrant pink (#f093fb to #f5576c)
- Green: Vibrant green/teal (#43e97b to #38f9d7)
- Plus updated indigo, teal, orange, yellow, red, gray themes

Game Manifest Updates:
- Memory Lightning: now uses purple theme
- Matching Pairs: now uses pink theme
- Complement Race: continues using blue (cyan) theme
- Card Sorting: now uses green theme

Homepage Game Cards:
- Added height: '100%' and flexbox to make all cards equal height
- Cards now stretch uniformly regardless of content length
- Maintains responsive hover effects and text readability overlays

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:13:09 -05:00
Thomas Hallock
7f516526fb refactor: make homepage game cards dynamic from game registry
Previously game cards were hardcoded. Now they automatically populate
from getAvailableGames() in the game registry. This means:
- Adding a new game to the registry automatically shows it on homepage
- Game metadata (icon, title, description, gradient, chips) comes from
  game manifests
- No manual homepage updates needed when games are added

Changes:
- Import getAvailableGames from game registry
- Replace hardcoded GameCard components with .map() over available games
- Format maxPlayers into player count string (e.g., "1-4 players")

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:08:55 -05:00
semantic-release-bot
9f706e9dce chore(release): 4.48.5 [skip ci]
## [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](6410b21f82))
2025-10-20 22:03:34 +00:00
Thomas Hallock
6410b21f82 fix(homepage): add dark gradient overlay for better text contrast on game cards
Added layered approach for better text readability:
- Vibrant gradient background layer (bottom)
- Dark gradient overlay from transparent (top 10%) to semi-dark (bottom 50%)
- Content with relative z-index positioning

This keeps the colorful gradients visible while ensuring all text
(titles, descriptions, tags) is clearly readable on all four cards.

Gradient goes from barely visible at top to moderately dark at bottom,
creating a natural fade that doesn't overwhelm the vibrant colors.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 17:02:24 -05:00
semantic-release-bot
3dc9f48d12 chore(release): 4.48.4 [skip ci]
## [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](b6410c7c22))
2025-10-20 21:49:42 +00:00
Thomas Hallock
b6410c7c22 fix(homepage): improve text contrast on game cards with text shadows
Added text shadows to all game card text elements for better readability
on bright gradient backgrounds:
- Icon emoji: subtle shadow
- Title: strong shadow for maximum contrast
- Description: medium shadow (also increased opacity to 0.95)
- Player count: medium shadow (increased opacity to 0.85)
- Tags: medium shadow

This ensures all text is clearly readable on all four gradient backgrounds
(purple, pink, cyan, and green).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 16:48:35 -05:00
semantic-release-bot
b54aaf1a67 chore(release): 4.48.3 [skip ci]
## [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](c6dc210bf8))

### Documentation

* **z-index:** add comprehensive z-index and stacking context documentation ([c89aea7](c89aea7444))
2025-10-20 21:41:12 +00:00
Thomas Hallock
c6dc210bf8 fix(homepage): display gradient backgrounds on all game cards
Fixed issue where only the Memory Lightning card showed its gradient
background while other cards appeared dark. The problem was that Panda
CSS's css() function doesn't properly handle raw CSS gradient strings.

Solution: Moved gradient from Panda css() to inline style prop, allowing
all four game cards to display their vibrant gradients:
- Memory Lightning: purple/violet
- Matching Pairs: pink/red
- Complement Race: blue/cyan
- Card Sorting: green/cyan

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 16:39:56 -05:00
Thomas Hallock
c89aea7444 docs(z-index): add comprehensive z-index and stacking context documentation
Created .claude/Z_INDEX_MANAGEMENT.md with:
- Complete z-index layering hierarchy (0-20000+)
- Stacking context rules and examples
- Current z-index audit of all 100+ hardcoded values
- Guidelines for choosing z-index values
- Migration plan to use constants file consistently
- Debugging checklist for layering issues
- Documentation of recent nav bar z-index fix

Updated .claude/CLAUDE.md to reference z-index documentation.

This provides a single source of truth for reasoning about visual
layering and prevents future z-index conflicts like the tutorial
tooltip issue.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 16:24:18 -05:00
semantic-release-bot
3564bd51dc chore(release): 4.48.2 [skip ci]
## [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](cc315645de))

### Styles

* **hero:** unify background with rest of homepage ([035d831](035d8312c7))
2025-10-20 21:00:17 +00:00
Thomas Hallock
cc315645de fix(nav): ensure nav bar appears above tutorial tooltips
Increased navigation bar z-index from 30 to 1000 to ensure it remains
above tutorial tooltips (which use z-index 50 and 100) when scrolling.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 15:58:58 -05:00
Thomas Hallock
035d8312c7 style(hero): unify background with rest of homepage
Replace purple gradient with solid gray.900 background to eliminate
sharp transition between hero section and page content.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 15:52:33 -05:00
semantic-release-bot
5f9b2dfe2b chore(release): 4.48.1 [skip ci]
## [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](1bfde8fb25))
2025-10-20 19:31:27 +00:00
Thomas Hallock
1bfde8fb25 fix(hero): prevent SSR hydration mismatch for subtitle
Add isSubtitleLoaded flag to hide subtitle until client-side random
selection completes, preventing flash of wrong subtitle during hydration.

Changes:
- Add isSubtitleLoaded state to HomeHeroContext
- Set flag to true after subtitle is selected on client
- Add opacity transition to subtitle in HeroAbacus
- Matches loading pattern used for abacus value

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:30:21 -05:00
semantic-release-bot
48647e4fb5 chore(release): 4.48.0 [skip ci]
## [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](318f9469a0))
2025-10-20 19:20:03 +00:00
Thomas Hallock
318f9469a0 feat(hero): persist random subtitle per-session
Store the randomly selected subtitle index in sessionStorage so it
remains consistent throughout the user's session. This eliminates
subtitle flashing on page reloads and provides a more stable UX.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:18:31 -05:00
semantic-release-bot
4bace36561 chore(release): 4.47.2 [skip ci]
## [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](eff44b3ad1))
* **nav:** remove unnecessary borders from transparent nav ([8c2ddca](8c2ddca28d))
2025-10-20 19:16:22 +00:00
Thomas Hallock
8c2ddca28d fix(nav): remove unnecessary borders from transparent nav
Remove borders and extra padding around navigation elements when in
transparent mode. The borders were appearing as black and cluttering
the clean transparent overlay look. Now navigation elements show only
white text without any borders.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:14:59 -05:00
Thomas Hallock
eff44b3ad1 fix(nav): prevent thrashing by using fixed position always
The thrashing was caused by a layout shift feedback loop: switching
between position sticky (takes up space) and position fixed (overlays)
caused content to shift, triggering IntersectionObserver again.

Fix: Always use position fixed so nav state changes are purely visual
(transparency, borders, colors) without any layout shifts.

Also removed unnecessary hysteresis code since the root cause is fixed.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:13:54 -05:00
semantic-release-bot
f81b88ae30 chore(release): 4.47.1 [skip ci]
## [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](71b1b933b5))
2025-10-20 19:11:07 +00:00
Thomas Hallock
71b1b933b5 fix(hero): prevent nav thrashing with hysteresis
Add hysteresis to IntersectionObserver to prevent rapid toggling
between transparent and opaque nav bar states when scrolling near
the threshold. Now uses different thresholds for hiding (< 10%) vs
showing (> 30%), creating a 20% buffer zone.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:09:51 -05:00
semantic-release-bot
92d50673e5 chore(release): 4.47.0 [skip ci]
## [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](463841e191))

### Bug Fixes

* **hero:** use Number.isNaN instead of global isNaN ([c229faf](c229faffac))

### Styles

* **hero-abacus:** add purple bead colors for dark theme ([721dfe4](721dfe426d))
* **hero:** adjust spacing between title, subtitle, and abacus ([3a3706c](3a3706cc6f))
2025-10-20 19:04:17 +00:00
Thomas Hallock
c229faffac fix(hero): use Number.isNaN instead of global isNaN
Replace unsafe global isNaN with Number.isNaN to fix linting warning
and follow best practices for type coercion checking.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:02:59 -05:00
Thomas Hallock
463841e191 feat(nav): add transparent nav bar with borders when hero visible
When the hero section is visible on the homepage, the navigation bar
now becomes transparent with a fixed position overlay, featuring
contrasting white borders around nav elements for visibility. The nav
links change to white text for better contrast against the dark hero
background.

When scrolling past the hero, the nav returns to its normal white
background with sticky positioning.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:02:52 -05:00
Thomas Hallock
3a3706cc6f style(hero): adjust spacing between title, subtitle, and abacus
Reduce gap between title and subtitle from 4 to 2, and add margin
below subtitle to increase space before the abacus for better visual
hierarchy.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 14:02:43 -05:00
Thomas Hallock
721dfe426d style(hero-abacus): add purple bead colors for dark theme
Previously only styled column posts and reckoning bar, leaving
bright default bead colors that clashed with dark background.

Added:
- Heaven beads: light purple rgba(196, 181, 253, 0.8)
- Earth beads: medium purple rgba(167, 139, 250, 0.7)
- Both with violet strokes for definition

Beads now blend harmoniously with the dark purple gradient background.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 13:05:11 -05:00
18 changed files with 2618 additions and 1398 deletions

View File

@@ -1,3 +1,335 @@
## [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)

View File

@@ -223,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?

View 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

View File

@@ -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": []

View File

@@ -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",

View File

@@ -1,388 +1,11 @@
'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 { PageWithNav } from '@/components/PageWithNav'
import { LevelSliderDisplay } from '@/components/LevelSliderDisplay'
import { css } from '../../../styled-system/css'
import { container, 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
}
export default function LevelsPage() {
const [currentIndex, setCurrentIndex] = useState(0)
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 (isPaneHovered) return // Don't auto-advance when mouse is over the pane
const interval = setInterval(() => {
setCurrentIndex((prev) => {
// Cycle back to 0 when reaching the end
return prev >= allLevels.length - 1 ? 0 : prev + 1
})
}, 3000)
return () => clearInterval(interval)
}, [isPaneHovered])
// 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)
// 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,
},
}
return (
<PageWithNav navTitle="Kyu & Dan Levels" navEmoji="📊">
<div className={css({ bg: 'gray.900', minHeight: '100vh', pb: '12' })}>
@@ -449,471 +72,7 @@ export default function LevelsPage() {
{/* Main content */}
<div className={container({ maxW: '6xl', px: '4', py: '12' })}>
<section 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 */}
<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>
<LevelSliderDisplay />
{/* Info Section */}
<div

View File

@@ -8,24 +8,34 @@ import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
import { PageWithNav } from '@/components/PageWithNav'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'
import { getAvailableGames } from '@/lib/arcade/game-registry'
import { InteractiveFlashcards } from '@/components/InteractiveFlashcards'
import { LevelSliderDisplay } from '@/components/LevelSliderDisplay'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
import { token } from '../../styled-system/tokens'
// Mini abacus that cycles through random 3-digit numbers
function MiniAbacus() {
const [currentValue, setCurrentValue] = useState(123)
// Mini abacus that cycles through a sequence of values
function MiniAbacus({
values,
columns = 3,
interval = 2500,
}: {
values: number[]
columns?: number
interval?: number
}) {
const [currentIndex, setCurrentIndex] = useState(0)
const appConfig = useAbacusConfig()
useEffect(() => {
// Cycle through random 3-digit numbers every 2.5 seconds
const interval = setInterval(() => {
const randomNum = Math.floor(Math.random() * 1000) // 0-999
setCurrentValue(randomNum)
}, 2500)
if (values.length === 0) return
return () => clearInterval(interval)
}, [])
const timer = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % values.length)
}, interval)
return () => clearInterval(timer)
}, [values, interval])
// Dark theme styles for the abacus
const darkStyles = {
@@ -44,17 +54,17 @@ function MiniAbacus() {
return (
<div
className={css({
width: '75px',
height: '80px',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})}
>
<div className={css({ transform: 'scale(0.6)', transformOrigin: 'center center' })}>
<div className={css({ transform: 'scale(0.75)', transformOrigin: 'center center' })}>
<AbacusReact
value={currentValue}
columns={3}
value={values[currentIndex] || 0}
columns={columns}
beadShape={appConfig.beadShape}
customStyles={darkStyles}
/>
@@ -64,15 +74,46 @@ function MiniAbacus() {
}
export default function HomePage() {
// Extract just the "Friends of 5" step (2+3=5) for homepage demo
const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques"
const fullTutorial = getTutorialForEditor()
const friendsOf5Tutorial = {
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Learn the "Friends of 5" technique: adding 3 to make 5',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
}
// Create different tutorials for each skill level
const skillTutorials = [
// Skill 0: Read and set numbers (0-9999)
{
...fullTutorial,
id: 'read-numbers-demo',
title: 'Read and Set Numbers',
description: 'Master abacus number representation from zero to thousands',
steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
},
// Skill 1: Friends techniques (5 = 2+3)
{
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Add and subtract using complement pairs: 5 = 2+3',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
},
// Skill 2: Multiply & divide (12×34)
{
...fullTutorial,
id: 'multiply-demo',
title: 'Multiplication',
description: 'Fluent multi-digit calculations with advanced techniques',
steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
},
// Skill 3: Mental calculation (Speed math)
{
...fullTutorial,
id: 'mental-calc-demo',
title: 'Mental Calculation',
description: 'Visualize and compute without the physical tool (Anzan)',
steps: fullTutorial.steps.slice(-3),
},
]
const selectedTutorial = skillTutorials[selectedSkillIndex]
return (
<HomeHeroProvider>
@@ -81,125 +122,149 @@ export default function HomePage() {
{/* Hero Section with Large Interactive Abacus */}
<HeroAbacus />
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Learn by Doing Section - with inline tutorial demo */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
Learn by Doing
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Interactive tutorials teach you step-by-step. Try this example right now:
</p>
</div>
{/* Live demo and learning objectives */}
<div
{/* Learn by Doing Section - with inline tutorial demo */}
<section className={stack({ gap: '8', mb: '16', px: '4', py: '12' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '1200px',
mx: 'auto',
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
Learn by Doing
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Interactive tutorials teach you step-by-step. Try this example right now:
</p>
</div>
{/* Live demo and learning objectives */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
minW: { base: '100%', xl: '1400px' },
mx: 'auto',
})}
>
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', xl: 'row' },
gap: '8',
alignItems: { base: 'center', xl: 'flex-start' },
})}
>
{/* Tutorial on the left */}
<div
className={css({
display: 'flex',
flexDirection: { base: 'column', md: 'row' },
gap: '8',
alignItems: { base: 'center', md: 'flex-start' },
flex: '1',
minW: { base: '100%', xl: '500px' },
maxW: { base: '100%', xl: '500px' },
})}
>
{/* Tutorial on the left */}
<div className={css({ flex: '1' })}>
<TutorialPlayer
tutorial={friendsOf5Tutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
</div>
<TutorialPlayer
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
</div>
{/* What you'll learn on the right */}
<div
{/* What you'll learn on the right */}
<div
className={css({
flex: '0 0 auto',
w: { base: '100%', lg: '800px' },
})}
>
<h3
className={css({
flex: '0 0 auto',
minW: '340px',
maxW: { base: '100%', md: '420px' },
fontSize: '2xl',
fontWeight: 'bold',
color: 'white',
mb: '6',
})}
>
<h3
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: 'white',
mb: '6',
})}
>
What You'll Learn
</h3>
<div className={stack({ gap: '5' })}>
{[
{
icon: '🔢',
title: 'Read and set numbers',
desc: 'Master abacus number representation from zero to thousands',
example: '0-9999',
badge: 'Foundation',
},
{
icon: '🤝',
title: 'Friends techniques',
desc: 'Add and subtract using complement pairs and mental shortcuts',
example: '5 = 2+3',
badge: 'Core',
},
{
icon: '',
title: 'Multiply & divide',
desc: 'Fluent multi-digit calculations with advanced techniques',
example: '12×34',
badge: 'Advanced',
},
{
icon: '🧠',
title: 'Mental calculation',
desc: 'Visualize and compute without the physical tool (Anzan)',
example: 'Speed math',
badge: 'Expert',
},
].map((skill, i) => (
What You'll Learn
</h3>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
{[
{
title: '📖 Read and set numbers',
desc: 'Master abacus number representation from zero to thousands',
example: '0-9999',
badge: 'Foundation',
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3,
},
{
title: '🤝 Friends techniques',
desc: 'Add and subtract using complement pairs and mental shortcuts',
example: '5 = 2+3',
badge: 'Core',
values: [2, 5, 3],
columns: 1,
},
{
title: ' Multiply & divide',
desc: 'Fluent multi-digit calculations with advanced techniques',
example: '12×34',
badge: 'Advanced',
values: [12, 24, 36, 48],
columns: 2,
},
{
title: '🧠 Mental calculation',
desc: 'Visualize and compute without the physical tool (Anzan)',
example: 'Speed math',
badge: 'Expert',
values: [7, 14, 21, 28, 35],
columns: 2,
},
].map((skill, i) => {
const isSelected = i === selectedSkillIndex
return (
<div
key={i}
onClick={() => setSelectedSkillIndex(i)}
className={css({
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
borderRadius: 'xl',
p: '4',
p: { base: '4', lg: '5' },
border: '1px solid',
borderColor: 'rgba(255, 255, 255, 0.15)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.4)'
: 'rgba(255, 255, 255, 0.15)',
boxShadow: isSelected
? '0 6px 16px rgba(250, 204, 21, 0.2)'
: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s',
cursor: 'pointer',
_hover: {
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: 'rgba(255, 255, 255, 0.25)',
bg: isSelected
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
borderColor: isSelected
? 'rgba(250, 204, 21, 0.5)'
: 'rgba(255, 255, 255, 0.25)',
transform: 'translateY(-2px)',
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.4)',
boxShadow: isSelected
? '0 8px 20px rgba(250, 204, 21, 0.3)'
: '0 6px 16px rgba(0, 0, 0, 0.4)',
},
})}
>
@@ -207,20 +272,28 @@ export default function HomePage() {
<div
className={css({
fontSize: '3xl',
width: '75px',
height: '115px',
width: { base: '120px', lg: '150px' },
minHeight: { base: '115px', lg: '140px' },
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
bg: 'rgba(255, 255, 255, 0.1)',
bg: isSelected
? 'rgba(250, 204, 21, 0.15)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
{i === 0 ? <MiniAbacus /> : skill.icon}
<MiniAbacus values={skill.values} columns={skill.columns} />
</div>
<div className={stack({ gap: '2', flex: '1' })}>
<div className={hstack({ gap: '2', alignItems: 'center' })}>
<div className={stack({ gap: '2', flex: '1', minWidth: '0' })}>
<div
className={hstack({
gap: '2',
alignItems: 'center',
flexWrap: 'wrap',
})}
>
<div
className={css({
color: 'white',
@@ -272,13 +345,16 @@ export default function HomePage() {
</div>
</div>
</div>
))}
</div>
)
})}
</div>
</div>
</div>
</section>
</div>
</section>
{/* Main content container */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
{/* Current Offerings Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
@@ -290,98 +366,38 @@ export default function HomePage() {
mb: '2',
})}
>
Available Now
The Arcade
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>
Foundation tutorials and reinforcement games ready to use
Single-player challenges and multiplayer battles in networked rooms. Invite
friends to play or watch live.
</p>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
<GameCard
icon="🧠"
title="Memory Lightning"
description="Memorize soroban numbers"
players="1-8 players"
tags={['Memory', 'Pattern Recognition']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Match complement numbers"
players="1-4 players"
tags={['Friends of 5', 'Friends of 10']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<GameCard
icon="🏁"
title="Complement Race"
description="Race against time"
players="1-4 players"
tags={['Speed', 'Practice', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange numbers visually"
players="Solo challenge"
tags={['Visual Literacy', 'Ordering']}
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
href="/games"
/>
</div>
</section>
{/* For Kids & Families Section */}
<section className={stack({ gap: '6', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
For Kids & Families
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Simple enough for kids to start on their own, structured enough for parents to
trust
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🧒"
title="Self-Directed for Children"
features={[
'Big, obvious buttons and clear instructions',
'Progress at your own pace',
'Works with or without a physical abacus',
]}
accentColor="purple"
/>
<FeaturePanel
icon="👨‍👩‍👧"
title="Trusted by Parents"
features={[
'Structured curriculum following Japanese methods',
'Traditional kyu/dan progression levels',
'Track progress and celebrate achievements',
]}
accentColor="blue"
/>
{getAvailableGames().map((game) => {
const playersText =
game.manifest.maxPlayers === 1
? 'Solo challenge'
: `1-${game.manifest.maxPlayers} players`
return (
<GameCard
key={game.manifest.name}
icon={game.manifest.icon}
title={game.manifest.displayName}
description={game.manifest.description}
players={playersText}
tags={game.manifest.chips}
gradient={game.manifest.gradient}
href="/games"
/>
)
})}
</div>
</section>
{/* Progression Visualization */}
<section className={stack({ gap: '6', mb: '16' })}>
<section className={stack({ gap: '6', mb: '16', overflow: 'hidden' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
@@ -398,150 +414,11 @@ export default function HomePage() {
</p>
</div>
<Link
href="/levels"
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
border: '1px solid',
borderColor: 'gray.700',
rounded: 'xl',
p: '8',
display: 'block',
transition: 'all 0.2s',
cursor: 'pointer',
position: 'relative',
_hover: {
bg: 'rgba(0, 0, 0, 0.5)',
borderColor: 'violet.500',
transform: 'translateY(-2px)',
boxShadow: '0 8px 16px rgba(124, 58, 237, 0.2)',
},
})}
>
{/* Subtle arrow indicator */}
<div
className={css({
position: 'absolute',
top: '4',
right: '4',
fontSize: 'xl',
color: 'gray.500',
transition: 'all 0.2s',
_groupHover: {
color: 'violet.400',
transform: 'translateX(4px)',
},
})}
>
</div>
<div
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '4',
flexWrap: 'wrap',
})}
>
{(
[
{
level: '10 Kyu',
label: 'Beginner',
color: 'colors.green.400',
emoji: '🧒',
},
{
level: '5 Kyu',
label: 'Intermediate',
color: 'colors.blue.400',
emoji: '🧑',
},
{
level: '1 Kyu',
label: 'Advanced',
color: 'colors.violet.400',
emoji: '🧔',
},
{ level: 'Dan', label: 'Master', color: 'colors.amber.400', emoji: '🧙' },
] as const
).map((stage, i) => (
<div
key={i}
className={stack({
gap: '0',
textAlign: 'center',
flex: '1',
position: 'relative',
})}
>
<div
className={css({
fontSize: '5xl',
mb: '0',
})}
>
{stage.emoji}
</div>
<div
className={css({
fontSize: 'xl',
fontWeight: 'bold',
mt: '-2',
})}
style={{ color: token(stage.color) }}
>
{stage.level}
</div>
<div
className={css({
fontSize: 'sm',
color: 'gray.300',
})}
>
{stage.label}
</div>
{i < 3 && (
<div
style={{
position: 'absolute',
left: '100%',
marginLeft: '0.5rem',
top: '50%',
transform: 'translate(-50%, -50%)',
fontSize: '20px',
color: '#9ca3af',
}}
className={css({
display: { base: 'none', md: 'block' },
})}
>
</div>
)}
</div>
))}
</div>
<div
style={{
textAlign: 'center',
fontSize: '14px',
color: '#d1d5db',
fontStyle: 'italic',
}}
className={css({
mt: '6',
})}
>
Click to learn about the official Japanese ranking system →
</div>
</Link>
<LevelSliderDisplay />
</section>
{/* Additional Tools Section */}
<section className={stack({ gap: '6' })}>
{/* Flashcard Generator Section */}
<section className={stack({ gap: '8', mb: '16' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
@@ -551,35 +428,101 @@ export default function HomePage() {
mb: '2',
})}
>
Additional Tools
Create Custom Flashcards
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
Design beautiful flashcards for learning and practice
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
features={[
'Multiple formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
accentColor="blue"
ctaText="Create Flashcards →"
ctaHref="/create"
/>
<FeaturePanel
icon="🧮"
title="Interactive Abacus"
features={[
'Practice anytime in your browser',
'Multiple color schemes and bead styles',
'Sound effects and animations',
]}
accentColor="purple"
ctaText="Try the Abacus →"
ctaHref="/guide"
/>
{/* Combined interactive display and CTA */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: { base: '6', md: '8' },
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '1200px',
mx: 'auto',
})}
>
{/* Interactive Flashcards Display */}
<div className={css({ mb: '8' })}>
<InteractiveFlashcards />
</div>
{/* Features */}
<div className={grid({ columns: { base: 1, md: 3 }, gap: '4', mb: '6' })}>
{[
{
icon: '📄',
title: 'Multiple Formats',
desc: 'PDF, PNG, SVG, HTML',
},
{
icon: '🎨',
title: 'Customizable',
desc: 'Bead shapes, colors, layouts',
},
{
icon: '📐',
title: 'All Paper Sizes',
desc: 'A3, A4, A5, US Letter',
},
].map((feature, i) => (
<div
key={i}
className={css({
textAlign: 'center',
p: '4',
rounded: 'lg',
bg: 'rgba(255, 255, 255, 0.05)',
})}
>
<div className={css({ fontSize: '2xl', mb: '2' })}>{feature.icon}</div>
<div
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'white',
mb: '1',
})}
>
{feature.title}
</div>
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>
{feature.desc}
</div>
</div>
))}
</div>
{/* CTA Button */}
<div className={css({ textAlign: 'center' })}>
<Link
href="/create"
className={css({
display: 'inline-flex',
alignItems: 'center',
gap: '2',
bg: 'blue.600',
color: 'white',
px: '6',
py: '3',
rounded: 'lg',
fontWeight: 'semibold',
transition: 'all 0.2s',
_hover: {
bg: 'blue.500',
},
})}
>
<span>Create Flashcards</span>
<span>→</span>
</Link>
</div>
</div>
</section>
</div>
@@ -607,48 +550,112 @@ function GameCard({
href: string
}) {
return (
<Link href={href}>
<Link href={href} style={{ height: '100%', display: 'block' }}>
<div
className={css({
background: gradient,
rounded: 'xl',
p: '6',
shadow: 'lg',
transition: 'all 0.3s ease',
cursor: 'pointer',
position: 'relative',
overflow: 'hidden',
height: '100%',
display: 'flex',
flexDirection: 'column',
_hover: {
transform: 'translateY(-6px) scale(1.02)',
shadow: '0 25px 50px rgba(0, 0, 0, 0.3)',
},
})}
>
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
{title}
</h3>
<p className={css({ fontSize: 'sm', color: 'rgba(255, 255, 255, 0.9)', mb: '2' })}>
{description}
</p>
<p className={css({ fontSize: 'xs', color: 'rgba(255, 255, 255, 0.7)', mb: '3' })}>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
})}
>
{tag}
</span>
))}
{/* Vibrant gradient background */}
<div
style={{ background: gradient }}
className={css({
position: 'absolute',
inset: 0,
zIndex: 0,
})}
/>
{/* Dark gradient overlay for text readability */}
<div
className={css({
position: 'absolute',
inset: 0,
background: 'linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.5) 100%)',
zIndex: 1,
})}
/>
{/* Content */}
<div
className={css({
position: 'relative',
zIndex: 2,
flex: 1,
display: 'flex',
flexDirection: 'column',
})}
>
<div
className={css({
fontSize: '3xl',
mb: '3',
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
})}
>
{icon}
</div>
<h3
className={css({
fontSize: 'lg',
fontWeight: 'bold',
color: 'white',
mb: '2',
textShadow: '0 2px 8px rgba(0, 0, 0, 0.5)',
})}
>
{title}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'rgba(255, 255, 255, 0.95)',
mb: '2',
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
})}
>
{description}
</p>
<p
className={css({
fontSize: 'xs',
color: 'rgba(255, 255, 255, 0.85)',
mb: '3',
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
})}
>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)',
})}
>
{tag}
</span>
))}
</div>
</div>
</div>
</Link>

View File

@@ -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,
}

View File

@@ -23,7 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 4,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
...getGameTheme('purple'),
...getGameTheme('pink'),
available: true,
}

View File

@@ -23,7 +23,7 @@ const manifest: GameManifest = {
maxPlayers: 8,
difficulty: 'Intermediate',
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
...getGameTheme('blue'),
...getGameTheme('purple'),
available: true,
}

View File

@@ -569,17 +569,24 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
)
}
// Check if we should use transparent styling (when hero is visible)
const isTransparent = homeHero?.isHeroVisible
return (
<Tooltip.Provider delayDuration={200}>
<header
className={css({
bg: 'white',
shadow: 'sm',
borderBottom: '1px solid',
borderColor: 'gray.200',
position: 'sticky',
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,
zIndex: 30,
left: 0,
right: 0,
zIndex: 1000,
transition: 'all 0.3s ease',
})}
>
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
@@ -593,7 +600,7 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
flexDirection: 'column',
gap: '0',
textDecoration: 'none',
_hover: { '& > .brand-name': { color: 'brand.900' } },
_hover: { '& > .brand-name': { color: 'rgba(196, 181, 253, 1)' } },
opacity: 0,
animation: 'fadeIn 0.3s ease-out forwards',
})}
@@ -602,10 +609,10 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'brand.800',
color: 'rgba(255, 255, 255, 0.95)',
})}
>
🧮 Abaci One
Abaci One
</span>
<Tooltip.Root>
<Tooltip.Trigger asChild>
@@ -613,10 +620,11 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
className={css({
fontSize: 'xs',
fontWeight: 'medium',
color: 'brand.600',
color: 'rgba(196, 181, 253, 0.8)',
fontStyle: 'italic',
cursor: 'help',
_hover: { color: 'brand.700' },
whiteSpace: 'nowrap',
_hover: { color: 'rgba(196, 181, 253, 1)' },
})}
>
{subtitle.text}
@@ -656,13 +664,13 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
<div className={hstack({ gap: '6', alignItems: 'center' })}>
{/* Navigation Links */}
<nav className={hstack({ gap: '4' })}>
<NavLink href="/create" currentPath={pathname}>
<NavLink href="/create" currentPath={pathname} isTransparent={isTransparent}>
Create
</NavLink>
<NavLink href="/guide" currentPath={pathname}>
<NavLink href="/guide" currentPath={pathname} isTransparent={isTransparent}>
Guide
</NavLink>
<NavLink href="/games" currentPath={pathname}>
<NavLink href="/games" currentPath={pathname} isTransparent={isTransparent}>
Games
</NavLink>
</nav>
@@ -699,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' },
@@ -716,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',
},
})}
>

View File

@@ -6,20 +6,27 @@ import { css } from '../../styled-system/css'
import { useHomeHero } from '../contexts/HomeHeroContext'
export function HeroAbacus() {
const { subtitle, abacusValue, setAbacusValue, setIsHeroVisible } = useHomeHero()
const {
subtitle,
abacusValue,
setAbacusValue,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
} = useHomeHero()
const appConfig = useAbacusConfig()
const heroRef = useRef<HTMLDivElement>(null)
// Dark theme styles for the abacus (matching the mini abacus on homepage)
const darkStyles = {
// Styling for structural elements (solid, no translucency)
const structuralStyles = {
columnPosts: {
fill: 'rgba(255, 255, 255, 0.3)',
stroke: 'rgba(255, 255, 255, 0.2)',
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgba(255, 255, 255, 0.4)',
stroke: 'rgba(255, 255, 255, 0.25)',
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
}
@@ -43,30 +50,20 @@ export function HeroAbacus() {
return () => observer.disconnect()
}, [setIsHeroVisible])
// Auto-cycle through random numbers (optional - user can also interact)
useEffect(() => {
const interval = setInterval(() => {
const randomNum = Math.floor(Math.random() * 10000) // 0-9999
setAbacusValue(randomNum)
}, 4000)
return () => clearInterval(interval)
}, [setAbacusValue])
return (
<div
ref={heroRef}
className={css({
minHeight: '100vh',
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background:
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(88, 28, 135, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
justifyContent: 'space-between',
bg: 'gray.900',
position: 'relative',
overflow: 'hidden',
px: '4',
py: '12',
})}
>
{/* Background pattern */}
@@ -81,94 +78,93 @@ export function HeroAbacus() {
})}
/>
{/* Content */}
{/* Title and Subtitle Section - DIRECT CHILD */}
<div
className={css({
position: 'relative',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
height: '100%',
py: '12',
gap: '2',
zIndex: 10,
})}
>
{/* Title and Subtitle Section */}
<div
<h1
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4',
fontSize: { base: '4xl', md: '6xl', lg: '7xl' },
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
<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',
})}
>
{subtitle.text}
</p>
</div>
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 - centered in remaining space */}
{/* 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({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: '1',
width: '100%',
py: { base: '12', md: '16', lg: '20' },
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)',
})}
>
<div
className={css({
transform: { base: 'scale(2)', md: 'scale(3)', lg: 'scale(4)' },
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}
customStyles={darkStyles}
/>
</div>
<AbacusReact
value={abacusValue}
columns={4}
beadShape={appConfig.beadShape}
showNumbers={true}
interactive={true}
animated={true}
customStyles={structuralStyles}
onValueChange={setAbacusValue}
/>
</div>
</div>
{/* Subtle hint to scroll */}
<div
className={css({
fontSize: 'sm',
color: 'gray.400',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
animation: 'bounce 2s ease-in-out infinite',
})}
>
<span>Scroll to explore</span>
<span></span>
</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 */}

View File

@@ -0,0 +1,316 @@
'use client'
import { AbacusReact } from '@soroban/abacus-react'
import { useDrag } from '@use-gesture/react'
import { useEffect, useRef, useState } from 'react'
import { animated, config, to, useSpring } from '@react-spring/web'
import { css } from '../../styled-system/css'
interface Flashcard {
id: number
number: number
initialX: number
initialY: number
initialRotation: number
zIndex: number
}
/**
* InteractiveFlashcards - A fun, physics-based flashcard display
* Users can drag and throw flashcards around with realistic momentum
*/
export function InteractiveFlashcards() {
const containerRef = useRef<HTMLDivElement>(null)
// Generate 8-15 random flashcards (client-side only to avoid hydration errors)
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} />
))}
</div>
)
}
interface DraggableCardProps {
card: Flashcard
}
function DraggableCard({ card }: DraggableCardProps) {
// Track the card's current position in state (separate from the animation values)
const currentPositionRef = useRef({
x: card.initialX,
y: card.initialY,
rotation: card.initialRotation,
})
const [{ x, y, rotation, scale }, api] = useSpring(() => ({
x: card.initialX,
y: card.initialY,
rotation: card.initialRotation,
scale: 1,
config: config.wobbly,
}))
const [zIndex, setZIndex] = useState(card.zIndex)
const cardRef = useRef<HTMLDivElement>(null)
const dragOffsetRef = useRef({ x: 0, y: 0 })
const lastVelocityRef = useRef({ vx: 0, vy: 0 })
const velocityHistoryRef = useRef<Array<{ vx: number; vy: number }>>([])
const [transformOrigin, setTransformOrigin] = useState('center center')
const bind = useDrag(
({ down, movement: [mx, my], velocity: [vx, vy], direction: [dx, dy], first, xy }) => {
// Bring card to front when dragging
if (down) {
setZIndex(1000)
// Calculate drag offset from card center on first touch
if (first && cardRef.current) {
const cardRect = cardRef.current.getBoundingClientRect()
const cardWidth = cardRect.width
const cardHeight = cardRect.height
// xy is in viewport coordinates, convert to position relative to card
const clickRelativeToCard = {
x: xy[0] - cardRect.left,
y: xy[1] - cardRect.top,
}
// Calculate offset from card center
const cardCenterX = cardWidth / 2
const cardCenterY = cardHeight / 2
const offsetX = clickRelativeToCard.x - cardCenterX
const offsetY = clickRelativeToCard.y - cardCenterY
dragOffsetRef.current = { x: offsetX, y: offsetY }
// Convert offset to transform-origin (50% + offset as percentage of card size)
const originX = 50 + (offsetX / cardWidth) * 100
const originY = 50 + (offsetY / cardHeight) * 100
const transformOriginValue = `${originX}% ${originY}%`
console.log(
`Drag start: click at (${clickRelativeToCard.x.toFixed(0)}, ${clickRelativeToCard.y.toFixed(0)}) in card, offset from center: (${offsetX.toFixed(0)}, ${offsetY.toFixed(0)}), origin: ${transformOriginValue}`
)
setTransformOrigin(transformOriginValue)
velocityHistoryRef.current = []
}
// Smooth velocity by averaging last 3 frames
velocityHistoryRef.current.push({ vx, vy })
if (velocityHistoryRef.current.length > 3) {
velocityHistoryRef.current.shift()
}
const avgVx =
velocityHistoryRef.current.reduce((sum, v) => sum + v.vx, 0) /
velocityHistoryRef.current.length
const avgVy =
velocityHistoryRef.current.reduce((sum, v) => sum + v.vy, 0) /
velocityHistoryRef.current.length
// Calculate rotation based on smoothed velocity and drag offset
const velocityAngle = Math.atan2(avgVy, avgVx) * (180 / Math.PI)
const offsetAngle =
Math.atan2(dragOffsetRef.current.y, dragOffsetRef.current.x) * (180 / Math.PI)
// Card rotates to align with movement direction, offset by where we're grabbing
const targetRotation = velocityAngle - offsetAngle + 90
const speed = Math.sqrt(avgVx * avgVx + avgVy * avgVy)
// Store smoothed velocity for throw
lastVelocityRef.current = { vx: avgVx, vy: avgVy }
const finalRotation = speed > 0.01 ? targetRotation : currentPositionRef.current.rotation
api.start({
x: currentPositionRef.current.x + mx,
y: currentPositionRef.current.y + my,
scale: 1.1,
rotation: finalRotation,
immediate: (key) => key !== 'rotation', // Position immediate, rotation smooth
config: { tension: 200, friction: 30 }, // Smoother rotation spring
})
} else {
// On release, reset transform origin to center
setTransformOrigin('center center')
// Update current position to where the card was dropped
currentPositionRef.current.x = currentPositionRef.current.x + mx
currentPositionRef.current.y = currentPositionRef.current.y + my
// First, snap the spring to the dropped position immediately
api.set({
x: currentPositionRef.current.x,
y: currentPositionRef.current.y,
})
// On release, apply momentum with decay physics
const throwVelocityX = lastVelocityRef.current.vx * 1000
const throwVelocityY = lastVelocityRef.current.vy * 1000
// Calculate final rotation based on throw direction
const throwAngle = Math.atan2(throwVelocityY, throwVelocityX) * (180 / Math.PI)
api.start({
x: {
velocity: throwVelocityX,
config: { decay: true },
},
y: {
velocity: throwVelocityY,
config: { decay: true },
},
scale: 1,
rotation: throwAngle + 90, // Card aligns with throw direction
config: config.wobbly,
onChange: (result) => {
// Continue updating position as card settles with momentum
if (result.value.x !== undefined) {
currentPositionRef.current.x = result.value.x
}
if (result.value.y !== undefined) {
currentPositionRef.current.y = result.value.y
}
if (result.value.rotation !== undefined) {
currentPositionRef.current.rotation = result.value.rotation
}
},
})
}
},
{
// Prevent scrolling when dragging
preventDefault: true,
filterTaps: true,
}
)
return (
<animated.div
ref={cardRef}
{...bind()}
style={{
position: 'absolute',
left: 0,
top: 0,
transform: to(
[x, y, rotation, scale],
(x, y, r, s) => `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`
),
transformOrigin,
zIndex,
touchAction: 'none',
cursor: 'grab',
}}
className={css({
userSelect: 'none',
_active: {
cursor: 'grabbing',
},
})}
>
<div
className={css({
bg: 'white',
rounded: 'lg',
p: '4',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
minW: '120px',
border: '2px solid rgba(0, 0, 0, 0.1)',
transition: 'box-shadow 0.2s',
_hover: {
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.4)',
},
})}
>
{/* 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>
</animated.div>
)
}

View 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>
)
}

View File

@@ -1,9 +1,9 @@
'use client'
import type React from 'react'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
import type { Subtitle } from '../data/abaciOneSubtitles'
import { getRandomSubtitle, subtitles } from '../data/abaciOneSubtitles'
import { subtitles } from '../data/abaciOneSubtitles'
interface HomeHeroContextValue {
subtitle: Subtitle
@@ -11,6 +11,8 @@ interface HomeHeroContextValue {
setAbacusValue: (value: number) => void
isHeroVisible: boolean
setIsHeroVisible: (visible: boolean) => void
isAbacusLoaded: boolean
isSubtitleLoaded: boolean
}
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
@@ -20,14 +22,82 @@ 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 to avoid SSR mismatch
// Select random subtitle only on client side, persist per-session
useEffect(() => {
setSubtitle(getRandomSubtitle())
// 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 (so it stays consistent during morph)
const [abacusValue, setAbacusValue] = useState(1234)
// 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)
@@ -39,8 +109,10 @@ export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
setAbacusValue,
isHeroVisible,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
}),
[subtitle, abacusValue, isHeroVisible]
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
)
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>

View File

@@ -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>

View File

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

3
pnpm-lock.yaml generated
View File

@@ -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