feat(practice): add session HUD with tape-deck controls and PageWithNav

Major UI improvements to the practice session:
- Add dark control bar at top with session info and transport controls
- Replace pause/end buttons with tape-deck style buttons (⏸️/▶ and ⏹️)
- Move part type, problem count, and progress info into compact HUD
- Add overall progress counter (X/Y total) and health indicator
- Wrap practice page with PageWithNav for consistent app navigation
- Begin dark mode support with isDark prop from useTheme

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-06 19:16:13 -06:00
parent 871390d8e1
commit b19c6d0eca
2 changed files with 563 additions and 524 deletions

View File

@ -17,6 +17,8 @@ import {
OfflineSessionForm,
} from '@/components/practice/OfflineSessionForm'
import { PlacementTest } from '@/components/practice/PlacementTest'
import { PageWithNav } from '@/components/PageWithNav'
import { useTheme } from '@/contexts/ThemeContext'
import type { SlotResult } from '@/db/schema/session-plans'
import { usePlayerCurriculum } from '@/hooks/usePlayerCurriculum'
import {
@ -85,6 +87,9 @@ interface SessionConfig {
* 6. View summary
*/
export default function PracticePage() {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [viewState, setViewState] = useState<ViewState>('selecting')
const [selectedStudent, setSelectedStudent] = useState<StudentWithProgress | null>(null)
const [sessionConfig, setSessionConfig] = useState<SessionConfig>({
@ -397,446 +402,448 @@ export default function PracticePage() {
}
return (
<main
data-component="practice-page"
className={css({
minHeight: '100vh',
backgroundColor: 'gray.50',
padding: viewState === 'practicing' ? '0' : '2rem',
})}
>
<div
<PageWithNav>
<main
data-component="practice-page"
className={css({
maxWidth: viewState === 'practicing' ? '100%' : '800px',
margin: '0 auto',
minHeight: '100vh',
backgroundColor: isDark ? 'gray.900' : 'gray.50',
padding: viewState === 'practicing' ? '0' : '2rem',
})}
>
{/* Header - hide during practice */}
{viewState !== 'practicing' && (
<header
className={css({
textAlign: 'center',
marginBottom: '2rem',
})}
>
<h1
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'gray.800',
marginBottom: '0.5rem',
})}
>
Daily Practice
</h1>
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
})}
>
Build your soroban skills one step at a time
</p>
</header>
)}
{/* Content based on view state */}
{viewState === 'selecting' &&
(isLoadingStudents ? (
<div
<div
className={css({
maxWidth: viewState === 'practicing' ? '100%' : '800px',
margin: '0 auto',
})}
>
{/* Header - hide during practice */}
{viewState !== 'practicing' && (
<header
className={css({
textAlign: 'center',
padding: '3rem',
color: 'gray.500',
marginBottom: '2rem',
})}
>
Loading students...
</div>
) : (
<StudentSelector
students={students}
selectedStudent={selectedStudent ?? undefined}
onSelectStudent={handleSelectStudent}
onAddStudent={handleAddStudent}
/>
))}
{viewState === 'dashboard' && selectedStudent && (
<ProgressDashboard
student={selectedStudent}
currentPhase={currentPhase}
recentSkills={recentSkills}
onContinuePractice={handleContinuePractice}
onViewFullProgress={handleViewFullProgress}
onGenerateWorksheet={handleGenerateWorksheet}
onChangeStudent={handleChangeStudent}
onRunPlacementTest={handleRunPlacementTest}
onSetSkillsManually={handleSetSkillsManually}
onRecordOfflinePractice={handleRecordOfflinePractice}
/>
)}
{viewState === 'configuring' && selectedStudent && (
<div
data-section="session-config"
className={css({
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
padding: '2rem',
backgroundColor: 'white',
borderRadius: '16px',
boxShadow: 'md',
})}
>
<h2
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
textAlign: 'center',
})}
>
Configure Practice Session
</h2>
{/* Duration selector */}
<div>
<label
<h1
className={css({
display: 'block',
fontSize: '0.875rem',
fontSize: '2rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'white' : 'gray.800',
marginBottom: '0.5rem',
})}
>
Session Duration
</label>
<div
Daily Practice
</h1>
<p
className={css({
display: 'flex',
gap: '0.5rem',
fontSize: '1rem',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
{[5, 10, 15, 20].map((mins) => (
<button
key={mins}
type="button"
onClick={() => setSessionConfig((c) => ({ ...c, durationMinutes: mins }))}
className={css({
flex: 1,
padding: '1rem',
fontSize: '1.25rem',
fontWeight: 'bold',
color: sessionConfig.durationMinutes === mins ? 'white' : 'gray.700',
backgroundColor:
sessionConfig.durationMinutes === mins ? 'blue.500' : 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor:
sessionConfig.durationMinutes === mins ? 'blue.600' : 'gray.200',
},
})}
>
{mins} min
</button>
))}
</div>
</div>
Build your soroban skills one step at a time
</p>
</header>
)}
{/* Session structure preview */}
{/* Content based on view state */}
{viewState === 'selecting' &&
(isLoadingStudents ? (
<div
className={css({
textAlign: 'center',
padding: '3rem',
color: 'gray.500',
})}
>
Loading students...
</div>
) : (
<StudentSelector
students={students}
selectedStudent={selectedStudent ?? undefined}
onSelectStudent={handleSelectStudent}
onAddStudent={handleAddStudent}
/>
))}
{viewState === 'dashboard' && selectedStudent && (
<ProgressDashboard
student={selectedStudent}
currentPhase={currentPhase}
recentSkills={recentSkills}
onContinuePractice={handleContinuePractice}
onViewFullProgress={handleViewFullProgress}
onGenerateWorksheet={handleGenerateWorksheet}
onChangeStudent={handleChangeStudent}
onRunPlacementTest={handleRunPlacementTest}
onSetSkillsManually={handleSetSkillsManually}
onRecordOfflinePractice={handleRecordOfflinePractice}
/>
)}
{viewState === 'configuring' && selectedStudent && (
<div
data-section="session-config"
className={css({
padding: '1rem',
backgroundColor: 'gray.50',
borderRadius: '8px',
border: '1px solid',
borderColor: 'gray.200',
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
padding: '2rem',
backgroundColor: 'white',
borderRadius: '16px',
boxShadow: 'md',
})}
>
<div
<h2
className={css({
fontSize: '0.875rem',
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.700',
marginBottom: '0.75rem',
color: 'gray.800',
textAlign: 'center',
})}
>
Today's Practice Structure
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
fontSize: '0.875rem',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>🧮</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 1:</strong> Use abacus
</span>
</div>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>🧠</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 2:</strong> Mental math (visualization)
</span>
</div>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>💭</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 3:</strong> Mental math (linear)
</span>
</div>
</div>
</div>
Configure Practice Session
</h2>
{/* Error display for plan generation */}
{error?.context === 'generate' && (
{/* Duration selector */}
<div>
<label
className={css({
display: 'block',
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'gray.700',
marginBottom: '0.5rem',
})}
>
Session Duration
</label>
<div
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
{[5, 10, 15, 20].map((mins) => (
<button
key={mins}
type="button"
onClick={() => setSessionConfig((c) => ({ ...c, durationMinutes: mins }))}
className={css({
flex: 1,
padding: '1rem',
fontSize: '1.25rem',
fontWeight: 'bold',
color: sessionConfig.durationMinutes === mins ? 'white' : 'gray.700',
backgroundColor:
sessionConfig.durationMinutes === mins ? 'blue.500' : 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor:
sessionConfig.durationMinutes === mins ? 'blue.600' : 'gray.200',
},
})}
>
{mins} min
</button>
))}
</div>
</div>
{/* Session structure preview */}
<div
data-element="error-banner"
className={css({
padding: '1rem',
backgroundColor: 'red.50',
backgroundColor: 'gray.50',
borderRadius: '8px',
border: '1px solid',
borderColor: 'red.200',
borderColor: 'gray.200',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.75rem',
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'gray.700',
marginBottom: '0.75rem',
})}
>
<span className={css({ fontSize: '1.25rem' })}></span>
<div>
<div
className={css({
fontWeight: 'bold',
color: 'red.700',
marginBottom: '0.25rem',
})}
>
{error.message}
</div>
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
{error.suggestion}
</div>
Today's Practice Structure
</div>
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
fontSize: '0.875rem',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>🧮</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 1:</strong> Use abacus
</span>
</div>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>🧠</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 2:</strong> Mental math (visualization)
</span>
</div>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span>💭</span>
<span className={css({ color: 'gray.700' })}>
<strong>Part 3:</strong> Mental math (linear)
</span>
</div>
</div>
</div>
)}
{/* Action buttons */}
{/* Error display for plan generation */}
{error?.context === 'generate' && (
<div
data-element="error-banner"
className={css({
padding: '1rem',
backgroundColor: 'red.50',
borderRadius: '8px',
border: '1px solid',
borderColor: 'red.200',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.75rem',
})}
>
<span className={css({ fontSize: '1.25rem' })}></span>
<div>
<div
className={css({
fontWeight: 'bold',
color: 'red.700',
marginBottom: '0.25rem',
})}
>
{error.message}
</div>
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
{error.suggestion}
</div>
</div>
</div>
</div>
)}
{/* Action buttons */}
<div
className={css({
display: 'flex',
gap: '0.75rem',
marginTop: '1rem',
})}
>
<button
type="button"
onClick={() => {
generatePlan.reset()
setViewState('dashboard')
}}
className={css({
flex: 1,
padding: '1rem',
fontSize: '1rem',
color: 'gray.600',
backgroundColor: 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.200',
},
})}
>
Cancel
</button>
<button
type="button"
onClick={handleGeneratePlan}
disabled={generatePlan.isPending}
className={css({
flex: 2,
padding: '1rem',
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'white',
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.500',
borderRadius: '8px',
border: 'none',
cursor: generatePlan.isPending ? 'not-allowed' : 'pointer',
_hover: {
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.600',
},
})}
>
{generatePlan.isPending ? 'Generating...' : 'Generate Plan'}
</button>
</div>
</div>
)}
{viewState === 'reviewing' && selectedStudent && currentPlan && (
<div data-section="plan-review-wrapper">
{/* Error display for session start */}
{error?.context === 'start' && (
<div
data-element="error-banner"
className={css({
padding: '1rem',
marginBottom: '1rem',
backgroundColor: 'red.50',
borderRadius: '12px',
border: '1px solid',
borderColor: 'red.200',
maxWidth: '600px',
margin: '0 auto 1rem auto',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.75rem',
})}
>
<span className={css({ fontSize: '1.25rem' })}></span>
<div>
<div
className={css({
fontWeight: 'bold',
color: 'red.700',
marginBottom: '0.25rem',
})}
>
{error.message}
</div>
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
{error.suggestion}
</div>
</div>
</div>
</div>
)}
<PlanReview
plan={currentPlan}
studentName={selectedStudent.name}
onApprove={handleApprovePlan}
onCancel={handleCancelPlan}
/>
</div>
)}
{viewState === 'practicing' && selectedStudent && currentPlan && (
<ActiveSession
plan={currentPlan}
studentName={selectedStudent.name}
onAnswer={handleAnswer}
onEndEarly={handleEndEarly}
onComplete={handleSessionComplete}
/>
)}
{viewState === 'summary' && selectedStudent && currentPlan && (
<SessionSummary
plan={currentPlan}
studentName={selectedStudent.name}
onPracticeAgain={handlePracticeAgain}
onBackToDashboard={handleBackToDashboard}
/>
)}
{viewState === 'creating' && (
<div
data-section="create-student"
className={css({
display: 'flex',
gap: '0.75rem',
marginTop: '1rem',
textAlign: 'center',
padding: '3rem',
})}
>
<h2
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
marginBottom: '1rem',
})}
>
Add New Student
</h2>
<p
className={css({
color: 'gray.600',
marginBottom: '2rem',
})}
>
Student creation form coming soon!
</p>
<button
type="button"
onClick={() => {
generatePlan.reset()
setViewState('dashboard')
}}
onClick={() => setViewState('selecting')}
className={css({
flex: 1,
padding: '1rem',
padding: '0.75rem 2rem',
fontSize: '1rem',
color: 'gray.600',
backgroundColor: 'gray.100',
color: 'gray.700',
backgroundColor: 'gray.200',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.200',
backgroundColor: 'gray.300',
},
})}
>
Cancel
</button>
<button
type="button"
onClick={handleGeneratePlan}
disabled={generatePlan.isPending}
className={css({
flex: 2,
padding: '1rem',
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'white',
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.500',
borderRadius: '8px',
border: 'none',
cursor: generatePlan.isPending ? 'not-allowed' : 'pointer',
_hover: {
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.600',
},
})}
>
{generatePlan.isPending ? 'Generating...' : 'Generate Plan'}
Back to Student Selection
</button>
</div>
</div>
)}
)}
{viewState === 'reviewing' && selectedStudent && currentPlan && (
<div data-section="plan-review-wrapper">
{/* Error display for session start */}
{error?.context === 'start' && (
<div
data-element="error-banner"
className={css({
padding: '1rem',
marginBottom: '1rem',
backgroundColor: 'red.50',
borderRadius: '12px',
border: '1px solid',
borderColor: 'red.200',
maxWidth: '600px',
margin: '0 auto 1rem auto',
})}
>
<div
className={css({
display: 'flex',
alignItems: 'flex-start',
gap: '0.75rem',
})}
>
<span className={css({ fontSize: '1.25rem' })}></span>
<div>
<div
className={css({
fontWeight: 'bold',
color: 'red.700',
marginBottom: '0.25rem',
})}
>
{error.message}
</div>
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
{error.suggestion}
</div>
</div>
</div>
</div>
)}
<PlanReview
plan={currentPlan}
{viewState === 'placement-test' && selectedStudent && (
<PlacementTest
studentName={selectedStudent.name}
onApprove={handleApprovePlan}
onCancel={handleCancelPlan}
playerId={selectedStudent.id}
onComplete={handlePlacementTestComplete}
onCancel={handlePlacementTestCancel}
/>
</div>
)}
)}
</div>
{viewState === 'practicing' && selectedStudent && currentPlan && (
<ActiveSession
plan={currentPlan}
studentName={selectedStudent.name}
onAnswer={handleAnswer}
onEndEarly={handleEndEarly}
onComplete={handleSessionComplete}
/>
)}
{viewState === 'summary' && selectedStudent && currentPlan && (
<SessionSummary
plan={currentPlan}
studentName={selectedStudent.name}
onPracticeAgain={handlePracticeAgain}
onBackToDashboard={handleBackToDashboard}
/>
)}
{viewState === 'creating' && (
<div
data-section="create-student"
className={css({
textAlign: 'center',
padding: '3rem',
})}
>
<h2
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
marginBottom: '1rem',
})}
>
Add New Student
</h2>
<p
className={css({
color: 'gray.600',
marginBottom: '2rem',
})}
>
Student creation form coming soon!
</p>
<button
type="button"
onClick={() => setViewState('selecting')}
className={css({
padding: '0.75rem 2rem',
fontSize: '1rem',
color: 'gray.700',
backgroundColor: 'gray.200',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.300',
},
})}
>
Back to Student Selection
</button>
</div>
)}
{viewState === 'placement-test' && selectedStudent && (
<PlacementTest
{/* Manual Skill Selector Modal */}
{selectedStudent && (
<ManualSkillSelector
studentName={selectedStudent.name}
playerId={selectedStudent.id}
onComplete={handlePlacementTestComplete}
onCancel={handlePlacementTestCancel}
open={showManualSkillModal}
onClose={() => setShowManualSkillModal(false)}
onSave={handleSaveManualSkills}
/>
)}
</div>
{/* Manual Skill Selector Modal */}
{selectedStudent && (
<ManualSkillSelector
studentName={selectedStudent.name}
playerId={selectedStudent.id}
open={showManualSkillModal}
onClose={() => setShowManualSkillModal(false)}
onSave={handleSaveManualSkills}
/>
)}
{/* Offline Session Form Modal */}
{selectedStudent && (
<OfflineSessionForm
studentName={selectedStudent.name}
playerId={selectedStudent.id}
open={showOfflineSessionModal}
onClose={() => setShowOfflineSessionModal(false)}
onSubmit={handleSubmitOfflineSession}
/>
)}
</main>
{/* Offline Session Form Modal */}
{selectedStudent && (
<OfflineSessionForm
studentName={selectedStudent.name}
playerId={selectedStudent.id}
open={showOfflineSessionModal}
onClose={() => setShowOfflineSessionModal(false)}
onSubmit={handleSubmitOfflineSession}
/>
)}
</main>
</PageWithNav>
)
}

View File

@ -560,114 +560,226 @@ export function ActiveSession({
minHeight: '100vh',
})}
>
{/* Header with progress and health */}
{/* Practice Session HUD - Control bar with session info and tape-deck controls */}
<div
data-section="session-header"
data-section="session-hud"
className={css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.75rem',
backgroundColor: 'white',
gap: '0.75rem',
padding: '0.75rem 1rem',
backgroundColor: 'gray.900',
borderRadius: '12px',
boxShadow: 'sm',
boxShadow: 'lg',
})}
>
<div>
<div
{/* Tape deck controls */}
<div
data-element="transport-controls"
className={css({
display: 'flex',
gap: '0.5rem',
})}
>
{/* Pause/Play button */}
<button
type="button"
data-action={isPaused ? 'resume' : 'pause'}
onClick={isPaused ? handleResume : handlePause}
className={css({
fontSize: '0.875rem',
color: 'gray.500',
width: '48px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.5rem',
color: 'white',
backgroundColor: isPaused ? 'green.500' : 'gray.700',
borderRadius: '8px',
border: '2px solid',
borderColor: isPaused ? 'green.400' : 'gray.600',
cursor: 'pointer',
transition: 'all 0.15s ease',
_hover: {
backgroundColor: isPaused ? 'green.400' : 'gray.600',
transform: 'scale(1.05)',
},
_active: {
transform: 'scale(0.95)',
},
})}
aria-label={isPaused ? 'Resume session' : 'Pause session'}
>
{studentName}'s Practice
</div>
<div
{isPaused ? '▶' : '⏸'}
</button>
{/* Stop button */}
<button
type="button"
data-action="end-early"
onClick={() => onEndEarly('Session ended')}
className={css({
fontSize: '1.25rem',
fontWeight: 'bold',
color: 'gray.800',
width: '48px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.5rem',
color: 'red.300',
backgroundColor: 'gray.700',
borderRadius: '8px',
border: '2px solid',
borderColor: 'gray.600',
cursor: 'pointer',
transition: 'all 0.15s ease',
_hover: {
backgroundColor: 'red.900',
borderColor: 'red.700',
color: 'red.200',
transform: 'scale(1.05)',
},
_active: {
transform: 'scale(0.95)',
},
})}
aria-label="End session"
>
Problem {completedProblems + 1} of {totalProblems}
</div>
</button>
</div>
{sessionHealth && (
{/* Session info display */}
<div
data-element="session-info"
className={css({
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: '0.125rem',
})}
>
{/* Part type with emoji */}
<div
data-element="session-health"
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1rem',
borderRadius: '20px',
backgroundColor: 'gray.50',
})}
>
<span>{getHealthEmoji(sessionHealth.overall)}</span>
<span
className={css({
fontSize: '1rem',
})}
>
{getPartTypeEmoji(currentPart.type)}
</span>
<span
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: getHealthColor(sessionHealth.overall),
color: 'white',
})}
>
{Math.round(sessionHealth.accuracy * 100)}%
Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)}
</span>
</div>
)}
{/* Progress within part */}
<div
className={css({
fontSize: '0.75rem',
color: 'gray.400',
})}
>
Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part
</div>
</div>
{/* Overall progress and health */}
<div
data-element="progress-display"
className={css({
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
})}
>
{/* Problem counter */}
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
})}
>
<div
className={css({
fontSize: '1rem',
fontWeight: 'bold',
color: 'white',
fontFamily: 'monospace',
})}
>
{completedProblems + 1}/{totalProblems}
</div>
<div
className={css({
fontSize: '0.625rem',
color: 'gray.500',
textTransform: 'uppercase',
})}
>
Total
</div>
</div>
{/* Health indicator */}
{sessionHealth && (
<div
data-element="session-health"
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: '0.25rem 0.5rem',
backgroundColor: 'gray.800',
borderRadius: '6px',
})}
>
<span className={css({ fontSize: '1rem' })}>
{getHealthEmoji(sessionHealth.overall)}
</span>
<span
className={css({
fontSize: '0.625rem',
fontWeight: 'bold',
color: getHealthColor(sessionHealth.overall),
})}
>
{Math.round(sessionHealth.accuracy * 100)}%
</span>
</div>
)}
</div>
</div>
{/* Part indicator */}
{/* Part instruction banner - brief contextual hint */}
<div
data-element="part-indicator"
data-element="part-instruction"
className={css({
padding: '1rem',
padding: '0.5rem 1rem',
backgroundColor: partColors.bg,
borderRadius: '12px',
border: '2px solid',
borderRadius: '8px',
border: '1px solid',
borderColor: partColors.border,
textAlign: 'center',
fontSize: '0.875rem',
color: partColors.text,
})}
>
<div
className={css({
fontSize: '1.5rem',
marginBottom: '0.25rem',
})}
>
{getPartTypeEmoji(currentPart.type)}
</div>
<div
className={css({
fontSize: '1.25rem',
fontWeight: 'bold',
color: partColors.text,
marginBottom: '0.25rem',
})}
>
Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)}
</div>
<div
className={css({
fontSize: '0.875rem',
color: partColors.text,
})}
>
{currentPart.type === 'abacus' && 'Use your physical abacus to solve these problems'}
{currentPart.type === 'visualization' && 'Picture the beads moving in your mind'}
{currentPart.type === 'linear' && 'Calculate the answer mentally'}
</div>
<div
className={css({
fontSize: '0.75rem',
color: partColors.text,
marginTop: '0.5rem',
})}
>
Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part
</div>
{currentPart.type === 'abacus' && '🧮 Use your physical abacus'}
{currentPart.type === 'visualization' && '🧠 Picture the beads moving in your mind'}
{currentPart.type === 'linear' && '💭 Calculate the answer mentally'}
</div>
{/* Problem display */}
@ -906,86 +1018,6 @@ export function ActiveSession({
</div>
)}
{/* Teacher controls */}
<div
data-section="teacher-controls"
className={css({
display: 'flex',
gap: '0.75rem',
marginTop: 'auto',
paddingTop: '1rem',
borderTop: '1px solid',
borderColor: 'gray.200',
})}
>
{isPaused ? (
<button
type="button"
data-action="resume"
onClick={handleResume}
className={css({
flex: 1,
padding: '0.75rem',
fontSize: '1rem',
fontWeight: 'bold',
color: 'white',
backgroundColor: 'green.500',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor: 'green.600',
},
})}
>
Resume Practice
</button>
) : (
<button
type="button"
data-action="pause"
onClick={handlePause}
className={css({
flex: 1,
padding: '0.75rem',
fontSize: '0.875rem',
color: 'gray.600',
backgroundColor: 'gray.100',
borderRadius: '8px',
border: '1px solid',
borderColor: 'gray.200',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.200',
},
})}
>
Pause
</button>
)}
<button
type="button"
data-action="end-early"
onClick={() => onEndEarly('Teacher ended session')}
className={css({
padding: '0.75rem 1.5rem',
fontSize: '0.875rem',
color: 'red.600',
backgroundColor: 'red.50',
borderRadius: '8px',
border: '1px solid',
borderColor: 'red.200',
cursor: 'pointer',
_hover: {
backgroundColor: 'red.100',
},
})}
>
End Session
</button>
</div>
{/* Pause overlay */}
{isPaused && (
<div