Compare commits

...

22 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
8 changed files with 751 additions and 117 deletions

View File

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

@@ -609,8 +609,8 @@ function GameCard({
return (
<Link href={href}>
<div
style={{ background: gradient }}
className={css({
background: gradient,
rounded: 'xl',
p: '6',
shadow: 'lg',
@@ -622,14 +622,44 @@ function GameCard({
},
})}
>
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
<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.9)', mb: '2' })}>
<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.7)', mb: '3' })}>
<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' })}>
@@ -644,6 +674,7 @@ function GameCard({
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)',
})}
>
{tag}

View File

@@ -569,17 +569,23 @@ 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' : 'white',
shadow: isTransparent ? 'none' : 'sm',
borderBottom: isTransparent ? 'none' : '1px solid',
borderColor: isTransparent ? 'transparent' : 'gray.200',
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' })}>
@@ -656,13 +662,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,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))
@@ -716,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',
@@ -725,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

@@ -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(2)', md: 'scale(3)', lg: 'scale(4)' },
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

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

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