Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095cdda4ca | ||
|
|
a1a135a858 | ||
|
|
7f516526fb | ||
|
|
9f706e9dce | ||
|
|
6410b21f82 | ||
|
|
3dc9f48d12 | ||
|
|
b6410c7c22 | ||
|
|
b54aaf1a67 | ||
|
|
c6dc210bf8 | ||
|
|
c89aea7444 | ||
|
|
3564bd51dc | ||
|
|
cc315645de | ||
|
|
035d8312c7 | ||
|
|
5f9b2dfe2b | ||
|
|
1bfde8fb25 | ||
|
|
48647e4fb5 | ||
|
|
318f9469a0 |
64
CHANGELOG.md
64
CHANGELOG.md
@@ -1,3 +1,67 @@
|
||||
## [4.49.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.5...v4.49.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add vibrant gradients and equal heights to game cards ([a1a135a](https://github.com/antialias/soroban-abacus-flashcards/commit/a1a135a8586e314c9d695bec6c4e58ec24e5c9cb)), closes [#4](https://github.com/antialias/soroban-abacus-flashcards/issues/4) [#00f2](https://github.com/antialias/soroban-abacus-flashcards/issues/00f2) [#667](https://github.com/antialias/soroban-abacus-flashcards/issues/667) [#764ba2](https://github.com/antialias/soroban-abacus-flashcards/issues/764ba2) [#f093](https://github.com/antialias/soroban-abacus-flashcards/issues/f093) [#f5576](https://github.com/antialias/soroban-abacus-flashcards/issues/f5576) [#43e97](https://github.com/antialias/soroban-abacus-flashcards/issues/43e97) [#38f9d7](https://github.com/antialias/soroban-abacus-flashcards/issues/38f9d7)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* make homepage game cards dynamic from game registry ([7f51652](https://github.com/antialias/soroban-abacus-flashcards/commit/7f516526fb5f5b60c1782db5c8c3e29f05caafa7))
|
||||
|
||||
## [4.48.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.4...v4.48.5) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** add dark gradient overlay for better text contrast on game cards ([6410b21](https://github.com/antialias/soroban-abacus-flashcards/commit/6410b21f829810af27e42d188295630bd67d6b6b))
|
||||
|
||||
## [4.48.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.3...v4.48.4) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** improve text contrast on game cards with text shadows ([b6410c7](https://github.com/antialias/soroban-abacus-flashcards/commit/b6410c7c225f01f42d095ca270b8da7903cbfbb0))
|
||||
|
||||
## [4.48.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.2...v4.48.3) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** display gradient backgrounds on all game cards ([c6dc210](https://github.com/antialias/soroban-abacus-flashcards/commit/c6dc210bf8e3a5b4d7d6e53f2a7427d335c65322))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* **z-index:** add comprehensive z-index and stacking context documentation ([c89aea7](https://github.com/antialias/soroban-abacus-flashcards/commit/c89aea744478696b6f812fe53311a2dba210540f))
|
||||
|
||||
## [4.48.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.1...v4.48.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nav:** ensure nav bar appears above tutorial tooltips ([cc31564](https://github.com/antialias/soroban-abacus-flashcards/commit/cc315645de30218d1b034da3e130458fe2961a69))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero:** unify background with rest of homepage ([035d831](https://github.com/antialias/soroban-abacus-flashcards/commit/035d8312c707cbf5b0e2a725d7b1d8ff406f842d))
|
||||
|
||||
## [4.48.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.0...v4.48.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** prevent SSR hydration mismatch for subtitle ([1bfde8f](https://github.com/antialias/soroban-abacus-flashcards/commit/1bfde8fb251b227ccd2528bfe1c47acffd79fa49))
|
||||
|
||||
## [4.48.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.2...v4.48.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **hero:** persist random subtitle per-session ([318f946](https://github.com/antialias/soroban-abacus-flashcards/commit/318f9469a0805c200c55ce4024a95fd7b8dbe6a2))
|
||||
|
||||
## [4.47.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.1...v4.47.2) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
392
apps/web/.claude/Z_INDEX_MANAGEMENT.md
Normal file
392
apps/web/.claude/Z_INDEX_MANAGEMENT.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# Z-Index & Stacking Context Management
|
||||
|
||||
## Overview
|
||||
|
||||
This document tracks z-index values and stacking contexts across the application to prevent layering conflicts and make reasoning about visual hierarchy easy.
|
||||
|
||||
## The Z-Index Constants System
|
||||
|
||||
**Location:** `src/constants/zIndex.ts`
|
||||
|
||||
All z-index values should be defined in this file and imported where needed:
|
||||
|
||||
```typescript
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
|
||||
// Use it like this:
|
||||
zIndex: Z_INDEX.NAV_BAR
|
||||
zIndex: Z_INDEX.MODAL
|
||||
zIndex: Z_INDEX.GAME_NAV.HAMBURGER_MENU
|
||||
```
|
||||
|
||||
## Z-Index Layering Hierarchy
|
||||
|
||||
From lowest to highest:
|
||||
|
||||
| Layer | Range | Purpose | Examples |
|
||||
|-------|-------|---------|----------|
|
||||
| **Base Content** | 0-99 | Default page content, game elements | Background elements, game tracks, cards |
|
||||
| **Navigation & UI Chrome** | 100-999 | Fixed navigation, sticky headers | AppNavBar, page headers |
|
||||
| **Overlays & Dropdowns** | 1000-9999 | Tooltips, popovers, dropdowns, tutorial tooltips | Tutorial tooltips (50-100), ConfigForm (50), dropdowns (999-1000) |
|
||||
| **Modals & Dialogs** | 10000-19999 | Modal dialogs, confirmation dialogs | Modal backdrop (10000), Modal content (10001) |
|
||||
| **Top-Level Overlays** | 20000+ | Toasts, critical notifications | Toast notifications (20000) |
|
||||
|
||||
## Stacking Context Rules
|
||||
|
||||
### What Creates a Stacking Context?
|
||||
|
||||
These CSS properties create new stacking contexts (z-index values are relative within them):
|
||||
|
||||
1. `position: fixed` or `position: sticky` with z-index
|
||||
2. `position: absolute` or `position: relative` with z-index
|
||||
3. `opacity` < 1
|
||||
4. `transform` (any value)
|
||||
5. `filter` (any value except none)
|
||||
6. `isolation: isolate`
|
||||
|
||||
### Key Insight
|
||||
|
||||
**Z-index values are only compared within the same stacking context!**
|
||||
|
||||
If Element A creates a stacking context with `z-index: 1` and Element B is outside that context with `z-index: 999`, Element B will be on top regardless of child z-indexes inside Element A.
|
||||
|
||||
### Example
|
||||
|
||||
```tsx
|
||||
// Parent creates stacking context
|
||||
<div style={{ position: 'relative', zIndex: 1 }}>
|
||||
{/* This child's z-index is relative to parent, not global! */}
|
||||
<div style={{ position: 'absolute', zIndex: 999999 }}>
|
||||
I'm still under elements with zIndex: 2 outside my parent!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', zIndex: 2 }}>
|
||||
I'm on top of the z-index: 999999 element above!
|
||||
</div>
|
||||
```
|
||||
|
||||
## Current Z-Index Audit (2025-10-20)
|
||||
|
||||
### ✅ Using Z_INDEX Constants (Good!)
|
||||
|
||||
| Component | Value | Source |
|
||||
|-----------|-------|--------|
|
||||
| AppNavBar (Panda section) | `Z_INDEX.NAV_BAR` (100) | `src/components/AppNavBar.tsx:464` |
|
||||
| AppNavBar hamburger | `Z_INDEX.GAME_NAV.HAMBURGER_MENU` (9999) | `src/components/AppNavBar.tsx:165` |
|
||||
| AbacusDisplayDropdown | `Z_INDEX.GAME_NAV.HAMBURGER_NESTED_DROPDOWN` (10000) | `src/components/AbacusDisplayDropdown.tsx:99` |
|
||||
|
||||
### ⚠️ Hardcoded Z-Index Values (Need Migration)
|
||||
|
||||
#### Critical Navigation Issues
|
||||
|
||||
| Component | Line | Value | Issue | Fix |
|
||||
|-----------|------|-------|-------|-----|
|
||||
| **AppNavBar (fixed section)** | 587 | `1000` | ❌ Should use `Z_INDEX.NAV_BAR` (100), but increased to 1000 to fix tutorial tooltip overlap | Define `TUTORIAL_TOOLTIP` in constants, set nav to proper layer |
|
||||
| AppNavBar (badge) | 645 | `50` | Should use constant | Add `Z_INDEX.BADGE` |
|
||||
|
||||
#### Tutorial System
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| TutorialPlayer | 643 | `50` | Tooltip container |
|
||||
| Tutorial shared/EditorComponents | 569, 590 | `50` | Tooltip button |
|
||||
| Tutorial shared/EditorComponents | 612 | `100` | Dropdown content (must be above tooltip) |
|
||||
| Tutorial decomposition CSS | 73 | `50` | Legacy CSS |
|
||||
| TutorialEditor | 65, 812, 2339 | `1000`, `10` | Various overlays |
|
||||
|
||||
#### Modals & Overlays
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| Modal (common) | 59 | `10000` | Modal backdrop |
|
||||
| ModerationPanel | 1994, 2009 | `10001`, `10002` | Moderation overlays |
|
||||
| ToastContext | 171 | `10001` | Toast notifications (should be 20000!) |
|
||||
| Join page | 35 | `10000` | Join page overlay |
|
||||
| EmojiPicker | 636 | `10000` | Emoji picker modal |
|
||||
|
||||
#### Dropdowns & Popovers
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| FormatSelectField | 115 | `999` | Dropdown |
|
||||
| DeploymentInfoModal | 37, 55 | `9998`, `9999` | Info modal layers |
|
||||
| RoomInfo | 338, 562 | `9999`, `10000` | Room tooltips |
|
||||
| GameTitleMenu | 119 | `9999` | Game menu |
|
||||
| PlayerTooltip | 69 | `9999` | Player tooltip |
|
||||
|
||||
#### Game Elements
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| Complement Race Game | Multiple | `0`, `1` | Base game layers |
|
||||
| Complement Race Track | 118, 140, 151 | `10`, `5`, `20` | Track, AI racers, player |
|
||||
| Complement Race HUD | 51, 106, 119, 137, 168 | `10`, `1000` | HUD elements |
|
||||
| GameCountdown | 58 | `1000` | Countdown overlay |
|
||||
| RouteCelebration | 31 | `9999` | Celebration overlay |
|
||||
| Matching GameCard | 203, 229, 243, 272, 386 | `9`, `10`, `-1`, `8`, `1` | Card layers |
|
||||
| Matching PlayerStatusBar | 154, 181, 202 | `10`, `10`, `5` | Status bars |
|
||||
|
||||
#### Misc UI
|
||||
|
||||
| Component | Line | Value | Purpose |
|
||||
|-----------|------|-------|---------|
|
||||
| HeroAbacus | 89, 127, 163 | `10` | Hero section layers |
|
||||
| ChampionArena | 425, 514, 554, 614 | `10`, `1`, `1`, `10` | Arena layers |
|
||||
| NetworkPlayerIndicator | 118, 145, 169, 192, 275 | `-1`, `2`, `1`, `2`, `10` | Player avatars |
|
||||
| ConfigurationForm | 521, 502 | `50` | Config overlays |
|
||||
|
||||
## The Recent Bug: Tutorial Tooltips Over Nav Bar
|
||||
|
||||
**Problem:** Tutorial tooltips (z-index: 50, 100) were appearing over the navigation bar.
|
||||
|
||||
**Root Cause:**
|
||||
- Nav bar was using `Z_INDEX.NAV_BAR` = 100 in one place
|
||||
- But also hardcoded `zIndex: 30` in the fixed positioning section (line 587)
|
||||
- Tutorial tooltips use hardcoded `zIndex: 50` and `zIndex: 100`
|
||||
- Since 50 and 100 > 30, tooltips appeared on top
|
||||
|
||||
**Temporary Fix:** Increased nav bar's hardcoded value from 30 to 1000
|
||||
|
||||
**Proper Fix Needed:**
|
||||
1. Define tutorial tooltip z-indexes in constants file
|
||||
2. Update nav bar to consistently use `Z_INDEX.NAV_BAR`
|
||||
3. Ensure NAV_BAR > TUTORIAL_TOOLTIP in the hierarchy
|
||||
4. Consider: Should tutorial tooltips be in the 1000-9999 range (overlays) rather than 50-100?
|
||||
|
||||
## Guidelines for Choosing Z-Index Values
|
||||
|
||||
### 1. **Always Import and Use Z_INDEX Constants**
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
zIndex: Z_INDEX.NAV_BAR
|
||||
|
||||
// ❌ Bad
|
||||
zIndex: 100 // Magic number!
|
||||
```
|
||||
|
||||
### 2. **Add New Values to Constants File First**
|
||||
|
||||
Before using a new z-index value, add it to `src/constants/zIndex.ts`:
|
||||
|
||||
```typescript
|
||||
export const Z_INDEX = {
|
||||
// ... existing values ...
|
||||
|
||||
TUTORIAL: {
|
||||
TOOLTIP: 500, // Tutorial tooltips (overlays layer)
|
||||
DROPDOWN: 600, // Tutorial dropdown (above tooltip)
|
||||
},
|
||||
} as const
|
||||
```
|
||||
|
||||
### 3. **Choose the Right Layer**
|
||||
|
||||
Ask yourself:
|
||||
- Is this base content? → Use 0-99
|
||||
- Is this navigation/UI chrome? → Use 100-999
|
||||
- Is this a dropdown/tooltip/overlay? → Use 1000-9999
|
||||
- Is this a modal dialog? → Use 10000-19999
|
||||
- Is this a toast notification? → Use 20000+
|
||||
|
||||
### 4. **Understand Your Stacking Context**
|
||||
|
||||
Before setting z-index, ask:
|
||||
- What is my parent's stacking context?
|
||||
- Am I comparing against siblings or global elements?
|
||||
- Does my element create a new stacking context?
|
||||
|
||||
### 5. **Document Special Cases**
|
||||
|
||||
If you must deviate from the constants, document why:
|
||||
|
||||
```typescript
|
||||
// HACK: Needs to be above tutorial tooltips (50) but below modals (10000)
|
||||
// TODO: Migrate to Z_INDEX.TUTORIAL.TOOLTIP system
|
||||
zIndex: 100
|
||||
```
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: Update Constants File ✅ TODO
|
||||
|
||||
Add missing constants to `src/constants/zIndex.ts`:
|
||||
|
||||
```typescript
|
||||
export const Z_INDEX = {
|
||||
// Base content layer (0-99)
|
||||
BASE: 0,
|
||||
CONTENT: 1,
|
||||
HERO_SECTION: 10, // Hero abacus components
|
||||
|
||||
// Game content layers (0-99)
|
||||
GAME_CONTENT: {
|
||||
TRACK: 0,
|
||||
CONTROLS: 1,
|
||||
RACER_AI: 5,
|
||||
RACER_PLAYER: 10,
|
||||
RACER_FLAG: 20,
|
||||
HUD: 50,
|
||||
},
|
||||
|
||||
// Navigation and UI chrome (100-999)
|
||||
NAV_BAR: 1000, // ⚠️ Currently needs to be 1000 due to tutorial tooltips
|
||||
STICKY_HEADER: 100,
|
||||
BADGE: 50,
|
||||
|
||||
// Overlays and dropdowns (1000-9999)
|
||||
TUTORIAL: {
|
||||
TOOLTIP: 500, // Tutorial tooltips
|
||||
DROPDOWN: 600, // Tutorial dropdowns (must be > tooltip)
|
||||
EDITOR: 700, // Tutorial editor
|
||||
},
|
||||
DROPDOWN: 1000,
|
||||
TOOLTIP: 1000,
|
||||
POPOVER: 1000,
|
||||
CONFIG_FORM: 1000,
|
||||
PLAYER_TOOLTIP: 1000,
|
||||
GAME_COUNTDOWN: 1000,
|
||||
|
||||
// High overlays (9000-9999)
|
||||
CELEBRATION: 9000,
|
||||
INFO_MODAL: 9998,
|
||||
|
||||
// Modal and dialog layers (10000-19999)
|
||||
MODAL_BACKDROP: 10000,
|
||||
MODAL: 10001,
|
||||
MODERATION_PANEL: 10001,
|
||||
EMOJI_PICKER: 10000,
|
||||
|
||||
// Top-level overlays (20000+)
|
||||
TOAST: 20000,
|
||||
|
||||
// Special navigation layers for game pages
|
||||
GAME_NAV: {
|
||||
HAMBURGER_MENU: 9999,
|
||||
HAMBURGER_NESTED_DROPDOWN: 10000,
|
||||
},
|
||||
} as const
|
||||
```
|
||||
|
||||
### Phase 2: Migrate High-Priority Components
|
||||
|
||||
Priority order:
|
||||
1. **Navigation components** (AppNavBar, etc.) - most critical for user experience
|
||||
2. **Tutorial system** (TutorialPlayer, tooltips) - currently conflicting
|
||||
3. **Modals and overlays** - ensure they're always on top
|
||||
4. **Game HUDs** - ensure proper layering
|
||||
5. **Everything else**
|
||||
|
||||
### Phase 3: Add Linting Rule
|
||||
|
||||
Consider adding an ESLint rule to prevent raw z-index numbers:
|
||||
|
||||
```javascript
|
||||
// Warn when zIndex is used with a number literal
|
||||
'no-magic-numbers': ['warn', {
|
||||
ignore: [0, 1, -1],
|
||||
ignoreArrayIndexes: true,
|
||||
enforceConst: true,
|
||||
}]
|
||||
```
|
||||
|
||||
## Debugging Z-Index Issues
|
||||
|
||||
### Checklist
|
||||
|
||||
When elements aren't layering correctly:
|
||||
|
||||
1. **Check the value**
|
||||
- [ ] What z-index does each element have?
|
||||
- [ ] Are they using constants or magic numbers?
|
||||
|
||||
2. **Check the stacking context**
|
||||
- [ ] What are the parent elements?
|
||||
- [ ] Do any parents create stacking contexts? (position + z-index, opacity, transform, etc.)
|
||||
- [ ] Are we comparing siblings or elements in different contexts?
|
||||
|
||||
3. **Verify the DOM hierarchy**
|
||||
- [ ] Use browser DevTools to inspect the DOM tree
|
||||
- [ ] Check the "Layers" panel in Chrome DevTools
|
||||
- [ ] Look for transforms, opacity, filters on parent elements
|
||||
|
||||
4. **Test the fix**
|
||||
- [ ] Does the fix work in all scenarios?
|
||||
- [ ] Did we introduce new conflicts?
|
||||
- [ ] Should we update the constants file?
|
||||
|
||||
### DevTools Tips
|
||||
|
||||
**Chrome DevTools:**
|
||||
1. Open DevTools → More Tools → Layers
|
||||
2. Select an element and see its stacking context
|
||||
3. View the 3D layer composition
|
||||
|
||||
**Firefox DevTools:**
|
||||
1. Inspector → Layout → scroll to "Z-index"
|
||||
2. Shows the stacking context parent
|
||||
|
||||
## Examples
|
||||
|
||||
### Good: Using Constants
|
||||
|
||||
```typescript
|
||||
import { Z_INDEX } from '@/constants/zIndex'
|
||||
|
||||
export function MyTooltip() {
|
||||
return (
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: Z_INDEX.TOOLTIP, // ✅ Clear and maintainable
|
||||
})}>
|
||||
Tooltip content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Bad: Magic Numbers
|
||||
|
||||
```typescript
|
||||
export function MyTooltip() {
|
||||
return (
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: 500, // ❌ Where did 500 come from? How does it relate to other elements?
|
||||
})}>
|
||||
Tooltip content
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Good: Documenting Stacking Context
|
||||
|
||||
```typescript
|
||||
// Creates a new stacking context for card contents
|
||||
<div className={css({
|
||||
position: 'relative',
|
||||
zIndex: Z_INDEX.BASE,
|
||||
transform: 'translateZ(0)', // ⚠️ Creates stacking context!
|
||||
})}>
|
||||
{/* Child z-indexes are relative to this context */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
zIndex: Z_INDEX.CONTENT, // Relative to parent, not global
|
||||
})}>
|
||||
Card face
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [MDN: CSS Stacking Context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context)
|
||||
- [What The Heck, z-index??](https://www.joshwcomeau.com/css/stacking-contexts/) by Josh Comeau
|
||||
- [Z-Index Playground](https://thirumanikandan.com/posts/learn-z-index-using-a-visualization-tool)
|
||||
|
||||
## Last Updated
|
||||
|
||||
2025-10-20 - Initial audit and documentation created
|
||||
@@ -103,7 +103,8 @@
|
||||
"Bash(node -e:*)",
|
||||
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - Run \\(.databaseId)\"\"')",
|
||||
"Bash(do ssh nas.home.network '/usr/local/bin/docker inspect soroban-abacus-flashcards --format=\"\"{{index .Config.Labels \\\"\"org.opencontainers.image.revision\\\"\"}}\"\"')",
|
||||
"Bash(git rev-parse HEAD)"
|
||||
"Bash(git rev-parse HEAD)",
|
||||
"Bash(gh run watch --exit-status 18662351595)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -8,6 +8,7 @@ import { HomeHeroProvider } from '@/contexts/HomeHeroContext'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
|
||||
import { getTutorialForEditor } from '@/utils/tutorialConverter'
|
||||
import { getAvailableGames } from '@/lib/arcade/game-registry'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, grid, hstack, stack } from '../../styled-system/patterns'
|
||||
import { token } from '../../styled-system/tokens'
|
||||
@@ -298,42 +299,24 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
|
||||
<GameCard
|
||||
icon="🧠"
|
||||
title="Memory Lightning"
|
||||
description="Memorize soroban numbers"
|
||||
players="1-8 players"
|
||||
tags={['Memory', 'Pattern Recognition']}
|
||||
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
|
||||
href="/games"
|
||||
/>
|
||||
<GameCard
|
||||
icon="⚔️"
|
||||
title="Matching Pairs"
|
||||
description="Match complement numbers"
|
||||
players="1-4 players"
|
||||
tags={['Friends of 5', 'Friends of 10']}
|
||||
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
|
||||
href="/games"
|
||||
/>
|
||||
<GameCard
|
||||
icon="🏁"
|
||||
title="Complement Race"
|
||||
description="Race against time"
|
||||
players="1-4 players"
|
||||
tags={['Speed', 'Practice', 'Survival']}
|
||||
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
|
||||
href="/games"
|
||||
/>
|
||||
<GameCard
|
||||
icon="🔢"
|
||||
title="Card Sorting"
|
||||
description="Arrange numbers visually"
|
||||
players="Solo challenge"
|
||||
tags={['Visual Literacy', 'Ordering']}
|
||||
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
|
||||
href="/games"
|
||||
/>
|
||||
{getAvailableGames().map((game) => {
|
||||
const playersText =
|
||||
game.manifest.maxPlayers === 1
|
||||
? 'Solo challenge'
|
||||
: `1-${game.manifest.maxPlayers} players`
|
||||
return (
|
||||
<GameCard
|
||||
key={game.manifest.name}
|
||||
icon={game.manifest.icon}
|
||||
title={game.manifest.displayName}
|
||||
description={game.manifest.description}
|
||||
players={playersText}
|
||||
tags={game.manifest.chips}
|
||||
gradient={game.manifest.gradient}
|
||||
href="/games"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -607,48 +590,112 @@ function GameCard({
|
||||
href: string
|
||||
}) {
|
||||
return (
|
||||
<Link href={href}>
|
||||
<Link href={href} style={{ height: '100%', display: 'block' }}>
|
||||
<div
|
||||
className={css({
|
||||
background: gradient,
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
shadow: 'lg',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
_hover: {
|
||||
transform: 'translateY(-6px) scale(1.02)',
|
||||
shadow: '0 25px 50px rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
|
||||
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
|
||||
{title}
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'rgba(255, 255, 255, 0.9)', mb: '2' })}>
|
||||
{description}
|
||||
</p>
|
||||
<p className={css({ fontSize: 'xs', color: 'rgba(255, 255, 255, 0.7)', mb: '3' })}>
|
||||
{players}
|
||||
</p>
|
||||
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
px: '2',
|
||||
py: '1',
|
||||
bg: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
rounded: 'full',
|
||||
fontWeight: 'semibold',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{/* Vibrant gradient background */}
|
||||
<div
|
||||
style={{ background: gradient }}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
})}
|
||||
/>
|
||||
{/* Dark gradient overlay for text readability */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: 'linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.5) 100%)',
|
||||
zIndex: 1,
|
||||
})}
|
||||
/>
|
||||
{/* Content */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
mb: '3',
|
||||
textShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '2',
|
||||
textShadow: '0 2px 8px rgba(0, 0, 0, 0.5)',
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
mb: '2',
|
||||
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
|
||||
})}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
mb: '3',
|
||||
textShadow: '0 1px 4px rgba(0, 0, 0, 0.4)',
|
||||
})}
|
||||
>
|
||||
{players}
|
||||
</p>
|
||||
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
px: '2',
|
||||
py: '1',
|
||||
bg: 'rgba(255, 255, 255, 0.2)',
|
||||
color: 'white',
|
||||
rounded: 'full',
|
||||
fontWeight: 'semibold',
|
||||
textShadow: '0 1px 3px rgba(0, 0, 0, 0.4)',
|
||||
})}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -24,7 +24,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 1, // Single player only
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
|
||||
...getGameTheme('teal'),
|
||||
...getGameTheme('green'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 4,
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['👥 Multiplayer', '🎯 Strategic', '🏆 Competitive'],
|
||||
...getGameTheme('purple'),
|
||||
...getGameTheme('pink'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const manifest: GameManifest = {
|
||||
maxPlayers: 8,
|
||||
difficulty: 'Intermediate',
|
||||
chips: ['👥 Multiplayer', '🧠 Memory', '🧮 Soroban'],
|
||||
...getGameTheme('blue'),
|
||||
...getGameTheme('purple'),
|
||||
available: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -584,7 +584,7 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 30,
|
||||
zIndex: 1000,
|
||||
transition: 'all 0.3s ease',
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,14 @@ import { css } from '../../styled-system/css'
|
||||
import { useHomeHero } from '../contexts/HomeHeroContext'
|
||||
|
||||
export function HeroAbacus() {
|
||||
const { subtitle, abacusValue, setAbacusValue, setIsHeroVisible, isAbacusLoaded } = useHomeHero()
|
||||
const {
|
||||
subtitle,
|
||||
abacusValue,
|
||||
setAbacusValue,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
} = useHomeHero()
|
||||
const appConfig = useAbacusConfig()
|
||||
const heroRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -52,8 +59,7 @@ export function HeroAbacus() {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(88, 28, 135, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
|
||||
bg: 'gray.900',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
px: '4',
|
||||
@@ -101,6 +107,8 @@ export function HeroAbacus() {
|
||||
color: 'purple.300',
|
||||
fontStyle: 'italic',
|
||||
marginBottom: '8',
|
||||
opacity: isSubtitleLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type React 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
|
||||
@@ -12,6 +12,7 @@ interface HomeHeroContextValue {
|
||||
isHeroVisible: boolean
|
||||
setIsHeroVisible: (visible: boolean) => void
|
||||
isAbacusLoaded: boolean
|
||||
isSubtitleLoaded: boolean
|
||||
}
|
||||
|
||||
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
|
||||
@@ -21,10 +22,28 @@ 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 - always start at 0 for SSR/hydration consistency
|
||||
@@ -91,8 +110,9 @@ export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
|
||||
isHeroVisible,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
}),
|
||||
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded]
|
||||
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
|
||||
)
|
||||
|
||||
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>
|
||||
|
||||
@@ -14,59 +14,59 @@ export interface GameTheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard theme presets
|
||||
* These use Panda CSS's color system and provide consistent styling
|
||||
* Standard theme presets with vibrant gradients
|
||||
* Updated for eye-catching game cards on the homepage
|
||||
*/
|
||||
export const GAME_THEMES = {
|
||||
blue: {
|
||||
color: 'blue',
|
||||
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
|
||||
borderColor: '#bfdbfe', // blue.200
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', // Vibrant cyan
|
||||
borderColor: '#00f2fe',
|
||||
},
|
||||
purple: {
|
||||
color: 'purple',
|
||||
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
|
||||
borderColor: '#ddd6fe', // purple.200
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', // Vibrant purple
|
||||
borderColor: '#764ba2',
|
||||
},
|
||||
green: {
|
||||
color: 'green',
|
||||
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
|
||||
borderColor: '#a7f3d0', // green.200
|
||||
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', // Vibrant green/teal
|
||||
borderColor: '#38f9d7',
|
||||
},
|
||||
teal: {
|
||||
color: 'teal',
|
||||
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
|
||||
borderColor: '#99f6e4', // teal.200
|
||||
gradient: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', // Vibrant teal
|
||||
borderColor: '#38ef7d',
|
||||
},
|
||||
indigo: {
|
||||
color: 'indigo',
|
||||
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
|
||||
borderColor: '#c7d2fe', // indigo.200
|
||||
gradient: 'linear-gradient(135deg, #5f72bd 0%, #9b23ea 100%)', // Vibrant indigo
|
||||
borderColor: '#9b23ea',
|
||||
},
|
||||
pink: {
|
||||
color: 'pink',
|
||||
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
|
||||
borderColor: '#fbcfe8', // pink.200
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', // Vibrant pink
|
||||
borderColor: '#f5576c',
|
||||
},
|
||||
orange: {
|
||||
color: 'orange',
|
||||
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
|
||||
borderColor: '#fed7aa', // orange.200
|
||||
gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', // Vibrant orange/coral
|
||||
borderColor: '#fee140',
|
||||
},
|
||||
yellow: {
|
||||
color: 'yellow',
|
||||
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
|
||||
borderColor: '#fde68a', // yellow.200
|
||||
gradient: 'linear-gradient(135deg, #ffd89b 0%, #19547b 100%)', // Vibrant yellow/blue
|
||||
borderColor: '#ffd89b',
|
||||
},
|
||||
red: {
|
||||
color: 'red',
|
||||
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
|
||||
borderColor: '#fecaca', // red.200
|
||||
gradient: 'linear-gradient(135deg, #f85032 0%, #e73827 100%)', // Vibrant red
|
||||
borderColor: '#e73827',
|
||||
},
|
||||
gray: {
|
||||
color: 'gray',
|
||||
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
|
||||
borderColor: '#e5e7eb', // gray.200
|
||||
gradient: 'linear-gradient(135deg, #868f96 0%, #596164 100%)', // Vibrant gray
|
||||
borderColor: '#596164',
|
||||
},
|
||||
} as const satisfies Record<string, GameTheme>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.47.2",
|
||||
"version": "4.49.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user