feat(tutorial): add hideNavigation prop to TutorialPlayer

Add `hideNavigation` prop to TutorialPlayer component that hides
the header and footer navigation controls, allowing the tutorial
content to be embedded cleanly without navigation chrome.

Perfect for single-step tutorial demos like the homepage.

Changes:
- Add hideNavigation prop to TutorialPlayerProps
- Wrap header section in conditional rendering
- Wrap navigation footer in conditional rendering
- Update homepage to use hideNavigation={true}
- Adjust minHeight when navigation is hidden

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-19 12:47:54 -05:00
parent 6b017b0fe9
commit 79ea52af80
4 changed files with 230 additions and 229 deletions

View File

@@ -283,10 +283,6 @@ export default function RoomPage() {
{/* Registry games */}
{getAllGames().map((gameDef) => {
if (gameDef.manifest.name === 'card-sorting') {
console.log('[Arcade Page] Card Sorting gameDef.manifest:', gameDef.manifest)
console.log('[Arcade Page] gradient value:', gameDef.manifest.gradient)
}
const isAvailable = gameDef.manifest.available
const isDisabled = !isHost || !isAvailable
return (
@@ -294,8 +290,10 @@ export default function RoomPage() {
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={isDisabled}
className={css({
style={{
background: gameDef.manifest.gradient,
}}
className={css({
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',

View File

@@ -222,6 +222,7 @@ export default function HomePage() {
tutorial={friendsOf5Tutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
/>
</div>

View File

@@ -12,9 +12,6 @@ import { CardSortingProvider } from './Provider'
import type { CardSortingConfig, CardSortingMove, CardSortingState } from './types'
import { cardSortingValidator } from './Validator'
const theme = getGameTheme('teal')
console.log('[Card Sorting] Theme object:', theme)
const manifest: GameManifest = {
name: 'card-sorting',
displayName: 'Card Sorting Challenge',
@@ -27,14 +24,10 @@ const manifest: GameManifest = {
maxPlayers: 1, // Single player only
difficulty: 'Intermediate',
chips: ['🧠 Pattern Recognition', '🎯 Solo Challenge', '📊 Smart Scoring'],
color: theme.color,
gradient: theme.gradient,
borderColor: theme.borderColor,
...getGameTheme('teal'),
available: true,
}
console.log('[Card Sorting] Final manifest:', manifest)
const defaultConfig: CardSortingConfig = {
cardCount: 8,
showNumbers: true,

View File

@@ -215,6 +215,7 @@ interface TutorialPlayerProps {
initialStepIndex?: number
isDebugMode?: boolean
showDebugPanel?: boolean
hideNavigation?: boolean
onStepChange?: (stepIndex: number, step: TutorialStep) => void
onStepComplete?: (stepIndex: number, step: TutorialStep, success: boolean) => void
onTutorialComplete?: (score: number, timeSpent: number) => void
@@ -227,6 +228,7 @@ function TutorialPlayerContent({
initialStepIndex = 0,
isDebugMode = false,
showDebugPanel = false,
hideNavigation = false,
onStepChange,
onStepComplete,
onTutorialComplete,
@@ -1061,183 +1063,187 @@ function TutorialPlayerContent({
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: '600px',
minHeight: hideNavigation ? 'auto' : '600px',
})} ${className || ''}`}
>
{/* Header */}
<div
className={css({
borderBottom: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'white',
})}
>
{!hideNavigation && (
<div
className={hstack({
justifyContent: 'space-between',
alignItems: 'center',
className={css({
borderBottom: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'white',
})}
>
<div>
<h1 className={css({ fontSize: 'xl', fontWeight: 'bold' })}>{tutorial.title}</h1>
<p className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {tutorial.steps.length}: {currentStep.title}
</p>
</div>
<div className={hstack({ gap: 2 })}>
{isDebugMode && (
<>
<button
onClick={toggleDebugPanel}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'blue.300',
borderRadius: 'md',
bg: uiState.showDebugPanel ? 'blue.100' : 'white',
color: 'blue.700',
cursor: 'pointer',
_hover: { bg: 'blue.50' },
})}
>
Debug
</button>
<button
onClick={toggleStepList}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: uiState.showStepList ? 'gray.100' : 'white',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
})}
>
Steps
</button>
{/* Multi-step navigation controls */}
{currentStep.multiStepInstructions &&
currentStep.multiStepInstructions.length > 1 && (
<>
<div
className={css({
fontSize: 'xs',
color: 'gray.600',
px: 2,
borderLeft: '1px solid',
borderColor: 'gray.300',
ml: 2,
pl: 3,
})}
>
Multi-Step: {currentMultiStep + 1} /{' '}
{currentStep.multiStepInstructions.length}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
First
</button>
<button
onClick={() => previousMultiStep()}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
Prev
</button>
<button
onClick={() => advanceMultiStep()}
disabled={currentMultiStep >= currentStep.multiStepInstructions.length - 1}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.200'
: 'green.300',
borderRadius: 'md',
bg:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.100'
: 'white',
color:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.400'
: 'green.700',
cursor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'not-allowed'
: 'pointer',
_hover:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? {}
: { bg: 'green.50' },
})}
>
Next
</button>
</>
)}
<label className={hstack({ gap: 2, fontSize: 'sm' })}>
<input
type="checkbox"
checked={uiState.autoAdvance}
onChange={toggleAutoAdvance}
/>
Auto-advance
</label>
</>
)}
</div>
</div>
{/* Progress bar */}
<div className={css({ mt: 2, bg: 'gray.200', borderRadius: 'full', h: 2 })}>
<div
className={css({
bg: 'blue.500',
h: 'full',
borderRadius: 'full',
transition: 'width 0.3s ease',
className={hstack({
justifyContent: 'space-between',
alignItems: 'center',
})}
style={{ width: `${navigationState.completionPercentage}%` }}
/>
>
<div>
<h1 className={css({ fontSize: 'xl', fontWeight: 'bold' })}>{tutorial.title}</h1>
<p className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {tutorial.steps.length}: {currentStep.title}
</p>
</div>
<div className={hstack({ gap: 2 })}>
{isDebugMode && (
<>
<button
onClick={toggleDebugPanel}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'blue.300',
borderRadius: 'md',
bg: uiState.showDebugPanel ? 'blue.100' : 'white',
color: 'blue.700',
cursor: 'pointer',
_hover: { bg: 'blue.50' },
})}
>
Debug
</button>
<button
onClick={toggleStepList}
className={css({
px: 3,
py: 1,
fontSize: 'sm',
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: uiState.showStepList ? 'gray.100' : 'white',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
})}
>
Steps
</button>
{/* Multi-step navigation controls */}
{currentStep.multiStepInstructions &&
currentStep.multiStepInstructions.length > 1 && (
<>
<div
className={css({
fontSize: 'xs',
color: 'gray.600',
px: 2,
borderLeft: '1px solid',
borderColor: 'gray.300',
ml: 2,
pl: 3,
})}
>
Multi-Step: {currentMultiStep + 1} /{' '}
{currentStep.multiStepInstructions.length}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
First
</button>
<button
onClick={() => previousMultiStep()}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' },
})}
>
Prev
</button>
<button
onClick={() => advanceMultiStep()}
disabled={
currentMultiStep >= currentStep.multiStepInstructions.length - 1
}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.200'
: 'green.300',
borderRadius: 'md',
bg:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.100'
: 'white',
color:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'gray.400'
: 'green.700',
cursor:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? 'not-allowed'
: 'pointer',
_hover:
currentMultiStep >= currentStep.multiStepInstructions.length - 1
? {}
: { bg: 'green.50' },
})}
>
Next
</button>
</>
)}
<label className={hstack({ gap: 2, fontSize: 'sm' })}>
<input
type="checkbox"
checked={uiState.autoAdvance}
onChange={toggleAutoAdvance}
/>
Auto-advance
</label>
</>
)}
</div>
</div>
{/* Progress bar */}
<div className={css({ mt: 2, bg: 'gray.200', borderRadius: 'full', h: 2 })}>
<div
className={css({
bg: 'blue.500',
h: 'full',
borderRadius: 'full',
transition: 'width 0.3s ease',
})}
style={{ width: `${navigationState.completionPercentage}%` }}
/>
</div>
</div>
</div>
)}
<div className={hstack({ flex: 1, gap: 0 })}>
{/* Step list sidebar */}
@@ -1596,57 +1602,60 @@ function TutorialPlayerContent({
</div>
{/* Navigation controls */}
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'gray.50',
})}
>
<div className={hstack({ justifyContent: 'space-between' })}>
<button
onClick={goToPreviousStep}
disabled={!navigationState.canGoPrevious}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: 'white',
cursor: navigationState.canGoPrevious ? 'pointer' : 'not-allowed',
opacity: navigationState.canGoPrevious ? 1 : 0.5,
_hover: navigationState.canGoPrevious ? { bg: 'gray.50' } : {},
})}
>
Previous
</button>
{!hideNavigation && (
<div
className={css({
borderTop: '1px solid',
borderColor: 'gray.200',
p: 4,
bg: 'gray.50',
})}
>
<div className={hstack({ justifyContent: 'space-between' })}>
<button
onClick={goToPreviousStep}
disabled={!navigationState.canGoPrevious}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
bg: 'white',
cursor: navigationState.canGoPrevious ? 'pointer' : 'not-allowed',
opacity: navigationState.canGoPrevious ? 1 : 0.5,
_hover: navigationState.canGoPrevious ? { bg: 'gray.50' } : {},
})}
>
Previous
</button>
<div className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {navigationState.totalSteps}
<div className={css({ fontSize: 'sm', color: 'gray.600' })}>
Step {currentStepIndex + 1} of {navigationState.totalSteps}
</div>
<button
onClick={goToNextStep}
disabled={!navigationState.canGoNext && !isStepCompleted}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor:
navigationState.canGoNext || isStepCompleted ? 'blue.300' : 'gray.300',
borderRadius: 'md',
bg: navigationState.canGoNext || isStepCompleted ? 'blue.500' : 'gray.200',
color: navigationState.canGoNext || isStepCompleted ? 'white' : 'gray.500',
cursor:
navigationState.canGoNext || isStepCompleted ? 'pointer' : 'not-allowed',
_hover: navigationState.canGoNext || isStepCompleted ? { bg: 'blue.600' } : {},
})}
>
{navigationState.canGoNext ? 'Next →' : 'Complete Tutorial'}
</button>
</div>
<button
onClick={goToNextStep}
disabled={!navigationState.canGoNext && !isStepCompleted}
className={css({
px: 4,
py: 2,
border: '1px solid',
borderColor:
navigationState.canGoNext || isStepCompleted ? 'blue.300' : 'gray.300',
borderRadius: 'md',
bg: navigationState.canGoNext || isStepCompleted ? 'blue.500' : 'gray.200',
color: navigationState.canGoNext || isStepCompleted ? 'white' : 'gray.500',
cursor: navigationState.canGoNext || isStepCompleted ? 'pointer' : 'not-allowed',
_hover: navigationState.canGoNext || isStepCompleted ? { bg: 'blue.600' } : {},
})}
>
{navigationState.canGoNext ? 'Next →' : 'Complete Tutorial'}
</button>
</div>
</div>
)}
</div>
{/* Debug panel */}