Compare commits

...

35 Commits

Author SHA1 Message Date
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
semantic-release-bot
9dba75c9d9 chore(release): 4.46.2 [skip ci]
## [4.46.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.1...v4.46.2) (2025-10-20)

### Bug Fixes

* **types:** properly type HomeHeroContext in AppNavBar ([f9a7cb7](f9a7cb7f05))
2025-10-20 18:03:17 +00:00
Thomas Hallock
f9a7cb7f05 fix(types): properly type HomeHeroContext in AppNavBar
TypeScript error: Context type was incorrectly inferred when using
fallback React.createContext(null), causing type mismatch.

Solution: Add explicit HomeHeroContextValue type and cast both the
dynamically loaded context and the fallback to this type.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 13:01:59 -05:00
semantic-release-bot
ae7463d917 chore(release): 4.46.1 [skip ci]
## [4.46.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.0...v4.46.1) (2025-10-20)

### Bug Fixes

* **hero-abacus:** restructure layout to prevent visual overlap ([02b6c70](02b6c70b7a))
2025-10-20 17:59:13 +00:00
Thomas Hallock
02b6c70b7a fix(hero-abacus): restructure layout to prevent visual overlap
Problem: Title, subtitle, and scaled abacus were stacking too tightly,
creating visual overlap and a messy appearance.

Solution: Reorganize with space-between flexbox layout:
- Group title + subtitle together at top with compact spacing
- Give abacus its own centered flex container with generous padding
- Separate scroll hint at bottom
- Use vertical padding and flex: 1 to ensure proper spacing

This creates clear visual separation between all sections.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:58:05 -05:00
semantic-release-bot
4b72e0c561 chore(release): 4.46.0 [skip ci]
## [4.46.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.45.0...v4.46.0) (2025-10-20)

### Features

* **homepage:** add full-page hero abacus with scroll-based nav transition ([d8ec642](d8ec64280e))

### Bug Fixes

* **homepage:** improve hero abacus sizing and layout ([230f1dc](230f1dcd86))

### Styles

* **hero-abacus:** apply dark theme to match homepage styling ([e0b6a2e](e0b6a2e88b))
2025-10-20 17:57:21 +00:00
Thomas Hallock
e0b6a2e88b style(hero-abacus): apply dark theme to match homepage styling
Add dark mode custom styles for column posts and reckoning bar:
- Semi-transparent white fills (0.3-0.4 opacity)
- Subtle stroke borders (0.2-0.25 opacity)
- Matches styling used in MiniAbacus component

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:56:13 -05:00
Thomas Hallock
230f1dcd86 fix(homepage): improve hero abacus sizing and layout
Improvements:
- Increase hero abacus size: 2x→3x→4x scale for mobile/tablet/desktop
- Add better spacing between subtitle and abacus (mb: 16)
- Add z-index layering to prevent subtitle/abacus overlap
- Fix nav layout issue by adding spacer div when branding is hidden
- Remove emoji from hero title (redundant with actual abacus)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:55:01 -05:00
Thomas Hallock
d8ec64280e feat(homepage): add full-page hero abacus with scroll-based nav transition
Implement an immersive homepage experience with a large, interactive
4-column abacus that dominates the initial viewport, creating a
"play with this" first impression. The hero smoothly transitions
to reveal the Abaci One branding in the navigation when scrolled.

**New Components:**
- `HeroAbacus`: Full-viewport interactive abacus with title/subtitle
  - Auto-cycles through random 4-digit numbers
  - Responsive scaling (1.5x mobile, 2x tablet, 2.5x desktop)
  - Intersection Observer to track visibility

- `HomeHeroContext`: Shared state for subtitle and scroll visibility
  - SSR-safe random subtitle selection (client-side only)
  - Prevents hydration mismatch warnings
  - Shares abacus value across hero/nav

**Navigation Updates:**
- AppNavBar conditionally shows/hides branding based on hero visibility
- Smooth fadeIn animation when branding appears after scroll
- Uses same random subtitle from context (consistent across page)
- Optional context access without breaking other pages

**Mobile Support:**
- Responsive abacus scaling for all screen sizes
- Touch-friendly interactive abacus
- Smooth animations work on all devices

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:55:01 -05:00
semantic-release-bot
1d4419364a chore(release): 4.45.0 [skip ci]
## [4.45.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.3...v4.45.0) (2025-10-20)

### Features

* **branding:** rebrand navigation from 'Soroban Generator' to 'Abaci One' ([cce8980](cce8980e17))
2025-10-20 17:39:58 +00:00
Thomas Hallock
cce8980e17 feat(branding): rebrand navigation from 'Soroban Generator' to 'Abaci One'
Changes:
- Add new subtitle data file with 75 three-word rhyming options
- Update AppNavBar to display "🧮 Abaci One" with random subtitle
- Implement Radix UI tooltip showing subtitle description on hover
- Use useMemo for performance (subtitle won't change on re-renders)
- Clean, minimal design with italic subtitle and help cursor

Implementation:
- Created `/src/data/abaciOneSubtitles.ts` with subtitle data structure
- Updated AppNavBar imports to include Radix Tooltip and subtitle util
- Wrapped navigation in Tooltip.Provider with 200ms delay
- Logo displays vertically with brand name and subtitle
- Tooltip shows description like "blaze through bead races" on hover

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:38:38 -05:00
semantic-release-bot
4bcce2a8db chore(release): 4.44.3 [skip ci]
## [4.44.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.2...v4.44.3) (2025-10-20)

### Bug Fixes

* **levels:** reduce operator box sizes and remove divider line ([29d20a6](29d20a6c07))
* **levels:** use uniform padding on operator box grid ([2818fd1](2818fd15ca))
2025-10-20 16:40:18 +00:00
Thomas Hallock
2818fd15ca fix(levels): use uniform padding on operator box grid
- Replaced separate pl, pr, py with uniform p: '2'
- Ensures equal padding on all sides (left, right, top, bottom)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:39:09 -05:00
Thomas Hallock
29d20a6c07 fix(levels): reduce operator box sizes and remove divider line
- Reduced box dimensions from 200x180px to 170x150px for better fit
- Reduced grid gap from '3' to '2' for tighter layout
- Reduced box padding from '4' to '3' and gap from '2' to '1.5'
- Reduced maxW from 480px to 400px
- Changed my (margins) to py (padding) for more control
- Removed borderRight divider line between operator boxes and abacus

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:38:14 -05:00
10 changed files with 1658 additions and 670 deletions

View File

@@ -1,3 +1,127 @@
## [4.48.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.3...v4.48.4) (2025-10-20)
### Bug Fixes
* **homepage:** improve text contrast on game cards with text shadows ([b6410c7](https://github.com/antialias/soroban-abacus-flashcards/commit/b6410c7c225f01f42d095ca270b8da7903cbfbb0))
## [4.48.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.2...v4.48.3) (2025-10-20)
### Bug Fixes
* **homepage:** display gradient backgrounds on all game cards ([c6dc210](https://github.com/antialias/soroban-abacus-flashcards/commit/c6dc210bf8e3a5b4d7d6e53f2a7427d335c65322))
### Documentation
* **z-index:** add comprehensive z-index and stacking context documentation ([c89aea7](https://github.com/antialias/soroban-abacus-flashcards/commit/c89aea744478696b6f812fe53311a2dba210540f))
## [4.48.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.1...v4.48.2) (2025-10-20)
### Bug Fixes
* **nav:** ensure nav bar appears above tutorial tooltips ([cc31564](https://github.com/antialias/soroban-abacus-flashcards/commit/cc315645de30218d1b034da3e130458fe2961a69))
### Styles
* **hero:** unify background with rest of homepage ([035d831](https://github.com/antialias/soroban-abacus-flashcards/commit/035d8312c707cbf5b0e2a725d7b1d8ff406f842d))
## [4.48.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.0...v4.48.1) (2025-10-20)
### Bug Fixes
* **hero:** prevent SSR hydration mismatch for subtitle ([1bfde8f](https://github.com/antialias/soroban-abacus-flashcards/commit/1bfde8fb251b227ccd2528bfe1c47acffd79fa49))
## [4.48.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.2...v4.48.0) (2025-10-20)
### Features
* **hero:** persist random subtitle per-session ([318f946](https://github.com/antialias/soroban-abacus-flashcards/commit/318f9469a0805c200c55ce4024a95fd7b8dbe6a2))
## [4.47.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.1...v4.47.2) (2025-10-20)
### Bug Fixes
* **nav:** prevent thrashing by using fixed position always ([eff44b3](https://github.com/antialias/soroban-abacus-flashcards/commit/eff44b3ad1ea0535c6965ad58012f9275cb143ec))
* **nav:** remove unnecessary borders from transparent nav ([8c2ddca](https://github.com/antialias/soroban-abacus-flashcards/commit/8c2ddca28dbdd7743227eed4d19a9a8f662a72b5))
## [4.47.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.0...v4.47.1) (2025-10-20)
### Bug Fixes
* **hero:** prevent nav thrashing with hysteresis ([71b1b93](https://github.com/antialias/soroban-abacus-flashcards/commit/71b1b933b598c0a6a8aef1bc9f8c598c1871b2eb))
## [4.47.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.2...v4.47.0) (2025-10-20)
### Features
* **nav:** add transparent nav bar with borders when hero visible ([463841e](https://github.com/antialias/soroban-abacus-flashcards/commit/463841e1910f4ddb9af662f036e4efb867836a83))
### Bug Fixes
* **hero:** use Number.isNaN instead of global isNaN ([c229faf](https://github.com/antialias/soroban-abacus-flashcards/commit/c229faffac525f3eebeb12155cb5ca4dff744472))
### Styles
* **hero-abacus:** add purple bead colors for dark theme ([721dfe4](https://github.com/antialias/soroban-abacus-flashcards/commit/721dfe426db4fe259f6cdeac587d008339df769b))
* **hero:** adjust spacing between title, subtitle, and abacus ([3a3706c](https://github.com/antialias/soroban-abacus-flashcards/commit/3a3706cc6fb694c7762f065f4ab4996bb8608dc4))
## [4.46.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.1...v4.46.2) (2025-10-20)
### Bug Fixes
* **types:** properly type HomeHeroContext in AppNavBar ([f9a7cb7](https://github.com/antialias/soroban-abacus-flashcards/commit/f9a7cb7f05dfddf291d89212a77ba1c11c00c9c7))
## [4.46.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.0...v4.46.1) (2025-10-20)
### Bug Fixes
* **hero-abacus:** restructure layout to prevent visual overlap ([02b6c70](https://github.com/antialias/soroban-abacus-flashcards/commit/02b6c70b7a52f7de2954e5e0efddbed64d419d6c))
## [4.46.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.45.0...v4.46.0) (2025-10-20)
### Features
* **homepage:** add full-page hero abacus with scroll-based nav transition ([d8ec642](https://github.com/antialias/soroban-abacus-flashcards/commit/d8ec64280ec0c2f44f2fd9c72a93a882481f650b))
### Bug Fixes
* **homepage:** improve hero abacus sizing and layout ([230f1dc](https://github.com/antialias/soroban-abacus-flashcards/commit/230f1dcd866e5b3625e19f7400f5eae478fe7d0c))
### Styles
* **hero-abacus:** apply dark theme to match homepage styling ([e0b6a2e](https://github.com/antialias/soroban-abacus-flashcards/commit/e0b6a2e88b3ebbaae41ed54f23f9e514604d2262))
## [4.45.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.3...v4.45.0) (2025-10-20)
### Features
* **branding:** rebrand navigation from 'Soroban Generator' to 'Abaci One' ([cce8980](https://github.com/antialias/soroban-abacus-flashcards/commit/cce8980e177da1b3c344e46561d928ed98b86f6c))
## [4.44.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.2...v4.44.3) (2025-10-20)
### Bug Fixes
* **levels:** reduce operator box sizes and remove divider line ([29d20a6](https://github.com/antialias/soroban-abacus-flashcards/commit/29d20a6c0741e7427f2bb64bc9c3e950b1a3238a))
* **levels:** use uniform padding on operator box grid ([2818fd1](https://github.com/antialias/soroban-abacus-flashcards/commit/2818fd15cacac78de6d86ba769b9b2a02800ed1e))
## [4.44.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.1...v4.44.2) (2025-10-20)

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

@@ -688,14 +688,10 @@ export default function LevelsPage() {
flex: '0 0 auto',
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: sizing.gap,
pr: '4',
pl: '2',
borderRight: '1px solid',
borderColor: 'gray.600',
maxW: '480px',
gap: '2',
p: '2',
maxW: '400px',
alignContent: 'center',
my: '2',
})}
>
{sections.map((section, idx) => {
@@ -715,16 +711,16 @@ export default function LevelsPage() {
border: '1px solid',
borderColor: hasData ? 'gray.700' : 'gray.800',
rounded: 'md',
p: '4',
p: '3',
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '2',
gap: '1.5',
opacity: hasData ? 1 : 0.3,
width: '200px',
height: '180px',
width: '170px',
height: '150px',
_hover: hasData
? {
borderColor: 'gray.500',

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,41 @@
'use client'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import * as Tooltip from '@radix-ui/react-tooltip'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import React, { useState } from 'react'
import React, { useContext, useMemo, useState } from 'react'
import { css } from '../../styled-system/css'
import { container, hstack } from '../../styled-system/patterns'
import { Z_INDEX } from '../constants/zIndex'
import { useFullscreen } from '../contexts/FullscreenContext'
import { getRandomSubtitle } from '../data/abaciOneSubtitles'
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
// Import HomeHeroContext for optional usage
import type { Subtitle } from '../data/abaciOneSubtitles'
type HomeHeroContextValue = {
subtitle: Subtitle
isHeroVisible: boolean
} | null
// HomeHeroContext - imported dynamically to avoid circular deps
let HomeHeroContextModule: any = null
try {
HomeHeroContextModule = require('../contexts/HomeHeroContext')
} catch {
// Context not available
}
const HomeHeroContext: React.Context<HomeHeroContextValue> =
HomeHeroContextModule?.HomeHeroContext || React.createContext<HomeHeroContextValue>(null)
// Use HomeHeroContext without requiring it
function useOptionalHomeHero(): HomeHeroContextValue {
return useContext(HomeHeroContext)
}
interface AppNavBarProps {
variant?: 'full' | 'minimal'
navSlot?: React.ReactNode
@@ -514,6 +540,17 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
// Try to get home hero context (if on homepage)
const homeHero = useOptionalHomeHero()
// Select a random subtitle once on mount (performance: won't change on re-renders)
// Use homeHero subtitle if available, otherwise generate one
const fallbackSubtitle = useMemo(() => getRandomSubtitle(), [])
const subtitle = homeHero?.subtitle || fallbackSubtitle
// Show branding unless we're on homepage with visible hero
const showBranding = !homeHero || !homeHero.isHeroVisible
// Auto-detect variant based on context
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
@@ -532,54 +569,135 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
)
}
// Check if we should use transparent styling (when hero is visible)
const isTransparent = homeHero?.isHeroVisible
return (
<header
className={css({
bg: 'white',
shadow: 'sm',
borderBottom: '1px solid',
borderColor: 'gray.200',
position: 'sticky',
top: 0,
zIndex: 30,
})}
>
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
{/* Logo */}
<Link
href="/"
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'brand.800',
textDecoration: 'none',
_hover: { color: 'brand.900' },
})}
>
🧮 Soroban Generator
</Link>
<Tooltip.Provider delayDuration={200}>
<header
className={css({
bg: isTransparent ? 'transparent' : 'white',
shadow: isTransparent ? 'none' : 'sm',
borderBottom: isTransparent ? 'none' : '1px solid',
borderColor: isTransparent ? 'transparent' : 'gray.200',
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 1000,
transition: 'all 0.3s ease',
})}
>
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
{/* Logo - conditionally shown based on hero visibility */}
{showBranding ? (
<Link
href="/"
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0',
textDecoration: 'none',
_hover: { '& > .brand-name': { color: 'brand.900' } },
opacity: 0,
animation: 'fadeIn 0.3s ease-out forwards',
})}
>
<span
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'brand.800',
})}
>
🧮 Abaci One
</span>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span
className={css({
fontSize: 'xs',
fontWeight: 'medium',
color: 'brand.600',
fontStyle: 'italic',
cursor: 'help',
_hover: { color: 'brand.700' },
})}
>
{subtitle.text}
</span>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
side="bottom"
align="start"
sideOffset={4}
className={css({
bg: 'gray.900',
color: 'white',
px: '3',
py: '2',
rounded: 'md',
fontSize: 'sm',
maxW: '250px',
shadow: 'lg',
zIndex: 50,
})}
>
{subtitle.description}
<Tooltip.Arrow
className={css({
fill: 'gray.900',
})}
/>
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Link>
) : (
<div />
)}
<div className={hstack({ gap: '6', alignItems: 'center' })}>
{/* Navigation Links */}
<nav className={hstack({ gap: '4' })}>
<NavLink href="/create" currentPath={pathname}>
Create
</NavLink>
<NavLink href="/guide" currentPath={pathname}>
Guide
</NavLink>
<NavLink href="/games" currentPath={pathname}>
Games
</NavLink>
</nav>
<div className={hstack({ gap: '6', alignItems: 'center' })}>
{/* Navigation Links */}
<nav className={hstack({ gap: '4' })}>
<NavLink href="/create" currentPath={pathname} isTransparent={isTransparent}>
Create
</NavLink>
<NavLink href="/guide" currentPath={pathname} isTransparent={isTransparent}>
Guide
</NavLink>
<NavLink href="/games" currentPath={pathname} isTransparent={isTransparent}>
Games
</NavLink>
</nav>
{/* Abacus Style Dropdown */}
<AbacusDisplayDropdown isFullscreen={false} />
{/* Abacus Style Dropdown */}
<AbacusDisplayDropdown isFullscreen={false} />
</div>
</div>
</div>
</div>
</header>
</header>
{/* Keyframes for fade-in animation */}
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`,
}}
/>
</Tooltip.Provider>
)
}
@@ -587,10 +705,12 @@ function NavLink({
href,
currentPath,
children,
isTransparent,
}: {
href: string
currentPath: string | null
children: React.ReactNode
isTransparent?: boolean
}) {
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
@@ -604,8 +724,20 @@ 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
? 'brand.700'
: 'gray.600',
bg: isTransparent
? isActive
? 'rgba(255, 255, 255, 0.15)'
: 'transparent'
: isActive
? 'brand.50'
: 'transparent',
rounded: 'lg',
transition: 'all',
textDecoration: 'none',
@@ -613,8 +745,8 @@ function NavLink({
alignItems: 'center',
justifyContent: 'center',
_hover: {
color: isActive ? 'brand.800' : 'gray.900',
bg: isActive ? 'brand.100' : 'gray.50',
color: isTransparent ? 'white' : isActive ? 'brand.800' : 'gray.900',
bg: isTransparent ? 'rgba(255, 255, 255, 0.2)' : isActive ? 'brand.100' : 'gray.50',
},
})}
>

View File

@@ -0,0 +1,189 @@
'use client'
import { useEffect, useRef } from 'react'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { useHomeHero } from '../contexts/HomeHeroContext'
export function HeroAbacus() {
const {
subtitle,
abacusValue,
setAbacusValue,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
} = useHomeHero()
const appConfig = useAbacusConfig()
const heroRef = useRef<HTMLDivElement>(null)
// Styling for structural elements (solid, no translucency)
const structuralStyles = {
columnPosts: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 2,
},
reckoningBar: {
fill: 'rgb(255, 255, 255)',
stroke: 'rgb(200, 200, 200)',
strokeWidth: 3,
},
}
// Detect when hero scrolls out of view
useEffect(() => {
if (!heroRef.current) return
const observer = new IntersectionObserver(
([entry]) => {
// Hero is visible if more than 20% is in viewport
setIsHeroVisible(entry.intersectionRatio > 0.2)
},
{
threshold: [0, 0.2, 0.5, 1],
}
)
observer.observe(heroRef.current)
return () => observer.disconnect()
}, [setIsHeroVisible])
return (
<div
ref={heroRef}
className={css({
height: '100vh',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'space-between',
bg: 'gray.900',
position: 'relative',
overflow: 'hidden',
px: '4',
py: '12',
})}
>
{/* Background pattern */}
<div
className={css({
position: 'absolute',
inset: 0,
opacity: 0.1,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
})}
/>
{/* Title and Subtitle Section - DIRECT CHILD */}
<div
className={css({
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
zIndex: 10,
})}
>
<h1
className={css({
fontSize: { base: '4xl', md: '6xl', lg: '7xl' },
fontWeight: 'bold',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Abaci One
</h1>
<p
className={css({
fontSize: { base: 'xl', md: '2xl' },
fontWeight: 'medium',
color: 'purple.300',
fontStyle: 'italic',
marginBottom: '8',
opacity: isSubtitleLoaded ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
})}
>
{subtitle.text}
</p>
</div>
{/* Large Interactive Abacus - DIRECT CHILD */}
<div
className={css({
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flex: '1',
width: '100%',
zIndex: 10,
opacity: isAbacusLoaded ? 1 : 0,
transition: 'opacity 0.5s ease-in-out',
})}
>
<div
className={css({
transform: { base: 'scale(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}
interactive={true}
animated={true}
customStyles={structuralStyles}
onValueChange={setAbacusValue}
/>
</div>
</div>
{/* Subtle hint to scroll - DIRECT CHILD */}
<div
className={css({
position: 'relative',
fontSize: 'sm',
color: 'gray.400',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
animation: 'bounce 2s ease-in-out infinite',
zIndex: 10,
})}
>
<span>Scroll to explore</span>
<span></span>
</div>
{/* Keyframes for bounce animation */}
<style
dangerouslySetInnerHTML={{
__html: `
@keyframes bounce {
0%, 100% {
transform: translateY(0);
opacity: 0.7;
}
50% {
transform: translateY(-10px);
opacity: 1;
}
}
`,
}}
/>
</div>
)
}

View File

@@ -0,0 +1,127 @@
'use client'
import type React from 'react'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
import type { Subtitle } from '../data/abaciOneSubtitles'
import { subtitles } from '../data/abaciOneSubtitles'
interface HomeHeroContextValue {
subtitle: Subtitle
abacusValue: number
setAbacusValue: (value: number) => void
isHeroVisible: boolean
setIsHeroVisible: (visible: boolean) => void
isAbacusLoaded: boolean
isSubtitleLoaded: boolean
}
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
export { HomeHeroContext }
export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
// Use first subtitle for SSR, then select random one on client mount
const [subtitle, setSubtitle] = useState<Subtitle>(subtitles[0])
const [isSubtitleLoaded, setIsSubtitleLoaded] = useState(false)
// Select random subtitle only on client side, persist per-session
useEffect(() => {
// Check if we have a stored subtitle index for this session
const storedIndex = sessionStorage.getItem('heroSubtitleIndex')
if (storedIndex !== null) {
// Use the stored subtitle index
const index = parseInt(storedIndex, 10)
if (!Number.isNaN(index) && index >= 0 && index < subtitles.length) {
setSubtitle(subtitles[index])
setIsSubtitleLoaded(true)
return
}
}
// Generate a new random index and store it
const randomIndex = Math.floor(Math.random() * subtitles.length)
sessionStorage.setItem('heroSubtitleIndex', randomIndex.toString())
setSubtitle(subtitles[randomIndex])
setIsSubtitleLoaded(true)
}, [])
// Shared abacus value - always start at 0 for SSR/hydration consistency
const [abacusValue, setAbacusValue] = useState(0)
const [isAbacusLoaded, setIsAbacusLoaded] = useState(false)
const isLoadingFromStorage = useRef(false)
// Load from sessionStorage after mount (client-only, no hydration mismatch)
useEffect(() => {
console.log('[HeroAbacus] Loading from sessionStorage...')
isLoadingFromStorage.current = true // Block saves during load
const saved = sessionStorage.getItem('heroAbacusValue')
console.log('[HeroAbacus] Saved value from storage:', saved)
if (saved) {
const parsedValue = parseInt(saved, 10)
console.log('[HeroAbacus] Parsed value:', parsedValue)
if (!Number.isNaN(parsedValue)) {
console.log('[HeroAbacus] Setting abacus value to:', parsedValue)
setAbacusValue(parsedValue)
}
} else {
console.log('[HeroAbacus] No saved value found, staying at 0')
}
// Use setTimeout to ensure the value has been set before we allow saves
setTimeout(() => {
isLoadingFromStorage.current = false
setIsAbacusLoaded(true)
console.log('[HeroAbacus] Load complete, allowing saves now and fading in')
}, 0)
}, [])
// Persist value to sessionStorage when it changes (but skip during load)
useEffect(() => {
console.log(
'[HeroAbacus] Save effect triggered. Value:',
abacusValue,
'isLoadingFromStorage:',
isLoadingFromStorage.current
)
if (!isLoadingFromStorage.current) {
console.log('[HeroAbacus] Saving to sessionStorage:', abacusValue)
sessionStorage.setItem('heroAbacusValue', abacusValue.toString())
console.log(
'[HeroAbacus] Saved successfully. Storage now contains:',
sessionStorage.getItem('heroAbacusValue')
)
} else {
console.log('[HeroAbacus] Skipping save (currently loading from storage)')
}
}, [abacusValue])
// Track hero visibility for nav branding
const [isHeroVisible, setIsHeroVisible] = useState(true)
const value = useMemo(
() => ({
subtitle,
abacusValue,
setAbacusValue,
isHeroVisible,
setIsHeroVisible,
isAbacusLoaded,
isSubtitleLoaded,
}),
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
)
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>
}
export function useHomeHero() {
const context = useContext(HomeHeroContext)
if (!context) {
throw new Error('useHomeHero must be used within HomeHeroProvider')
}
return context
}

View File

@@ -0,0 +1,97 @@
/**
* Abaci One subtitle options with descriptions
* Three-word rhyming subtitles for the main app navigation
*/
export interface Subtitle {
text: string
description: string
}
export const subtitles: Subtitle[] = [
{ text: 'Speed Bead Lead', description: 'blaze through bead races' },
{ text: 'Rod Mod Nod', description: 'tweak rod technique, approval earned' },
{ text: 'Grid Kid Lid', description: 'lock in neat grid habits' },
{ text: 'Count Mount Amount', description: 'stack up that number sense' },
{ text: 'Stack Track Tack', description: 'line up beads, lock in sums' },
{ text: 'Quick Flick Trick', description: 'rapid-fire bead tactics' },
{ text: 'Flash Dash Math', description: 'fly through numeric challenges' },
{ text: 'Slide Glide Pride', description: 'smooth soroban strokes' },
{ text: 'Shift Sift Gift', description: 'sort beads, reveal talent' },
{ text: 'Beat Seat Meet', description: 'compete head-to-head' },
{ text: 'Brain Train Gain', description: 'mental math muscle building' },
{ text: 'Flow Show Pro', description: 'demonstrate soroban mastery' },
{ text: 'Fast Blast Past', description: 'surpass speed limits' },
{ text: 'Snap Tap Map', description: 'chart your calculation path' },
{ text: 'Row Grow Know', description: 'advance through structured drills' },
{ text: 'Drill Skill Thrill', description: 'practice that excites' },
{ text: 'Think Link Sync', description: 'connect mind and beads' },
{ text: 'Boost Joust Roost', description: 'power up, compete, settle in' },
{ text: 'Add Grad Rad', description: 'level up addition awesomely' },
{ text: 'Sum Fun Run', description: 'enjoy the arithmetic sprint' },
{ text: 'Track Stack Pack', description: 'organize solutions systematically' },
{ text: 'Beat Neat Feat', description: 'clean victories, impressive wins' },
{ text: 'Math Path Wrath', description: 'dominate numeric challenges' },
{ text: 'Spark Mark Arc', description: 'ignite progress, track growth' },
{ text: 'Race Pace Ace', description: 'speed up, master it' },
{ text: 'Flex Hex Reflex', description: 'adapt calculations instantly' },
{ text: 'Glide Pride Stride', description: 'smooth confident progress' },
{ text: 'Flash Dash Smash', description: 'speed through, crush totals' },
{ text: 'Stack Attack Jack', description: 'aggressive bead strategies' },
{ text: 'Quick Pick Click', description: 'rapid bead selection' },
{ text: 'Snap Map Tap', description: 'visualize and execute' },
{ text: 'Mind Find Grind', description: 'discover mental endurance' },
{ text: 'Flip Skip Rip', description: 'fast transitions, tear through' },
{ text: 'Blend Trend Send', description: 'mix methods, share progress' },
{ text: 'Power Tower Hour', description: 'build skills intensively' },
{ text: 'Launch Staunch Haunch', description: 'start strong, stay firm' },
{ text: 'Rush Crush Hush', description: 'speed quietly dominates' },
{ text: 'Swipe Stripe Hype', description: 'sleek moves, excitement' },
{ text: 'Train Gain Sustain', description: 'build lasting ability' },
{ text: 'Frame Claim Flame', description: 'structure your fire' },
{ text: 'Streak Peak Tweak', description: 'hot runs, optimize performance' },
{ text: 'Edge Pledge Wedge', description: 'commit to precision' },
{ text: 'Pace Grace Space', description: 'rhythm, elegance, room to grow' },
{ text: 'Link Think Brink', description: 'connect at breakthrough edge' },
{ text: 'Quest Test Best', description: 'challenge yourself to excel' },
{ text: 'Drive Thrive Arrive', description: 'push hard, succeed, reach goals' },
{ text: 'Smart Start Chart', description: 'begin wisely, track progress' },
{ text: 'Boost Coast Toast', description: 'accelerate, cruise, celebrate' },
{ text: 'Spark Dark Embark', description: 'ignite before dawn journeys' },
{ text: 'Blaze Graze Amaze', description: 'burn through, touch lightly, wow' },
{ text: 'Shift Drift Gift', description: 'adapt smoothly, reveal talent' },
{ text: 'Zone Hone Own', description: 'focus, refine, claim mastery' },
{ text: 'Vault Halt Exalt', description: 'leap high, pause, celebrate' },
{ text: 'Peak Seek Streak', description: 'find heights, maintain momentum' },
{ text: 'Glow Show Grow', description: 'shine, display, expand' },
{ text: 'Scope Hope Rope', description: 'survey possibilities, climb up' },
{ text: 'Core Score More', description: 'fundamentals yield better results' },
{ text: 'Rank Bank Thank', description: 'earn status, save wins, appreciate' },
{ text: 'Merge Surge Verge', description: 'combine forces, power up, edge closer' },
{ text: 'Bold Gold Hold', description: 'brave attempts, prize rewards, maintain' },
{ text: 'Rise Prize Wise', description: 'ascend, win, learn' },
{ text: 'Move Groove Prove', description: 'act, find rhythm, demonstrate' },
{ text: 'Trust Thrust Adjust', description: 'believe, push, refine' },
{ text: 'Beam Dream Team', description: 'radiate, aspire, collaborate' },
{ text: 'Spin Win Grin', description: 'rotate beads, succeed, smile' },
{ text: 'String Ring Bring', description: 'connect, cycle, deliver' },
{ text: 'Clear Gear Steer', description: 'focus, equip, direct' },
{ text: 'Path Math Aftermath', description: 'route, calculate, results' },
{ text: 'Play Slay Day', description: 'engage, dominate, own it' },
{ text: 'Code Mode Road', description: 'pattern, style, journey' },
{ text: 'Craft Draft Shaft', description: 'build, sketch, core structure' },
{ text: 'Light Might Fight', description: 'illuminate, empower, compete' },
{ text: 'Stream Dream Extreme', description: 'flow, envision, push limits' },
{ text: 'Claim Frame Aim', description: 'assert, structure, target' },
{ text: 'Chart Smart Start', description: 'map, intelligent, begin' },
{ text: 'Bright Flight Height', description: 'brilliant, soar, elevation' },
]
/**
* Get a random subtitle from the list
* Uses current timestamp as seed for variety across sessions
*/
export function getRandomSubtitle(): Subtitle {
const index = Math.floor(Math.random() * subtitles.length)
return subtitles[index]
}

View File

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