feat(tutorial): add dark theme and column control props

Add `theme` and `abacusColumns` props to TutorialPlayer for better customization:
- theme: 'light' | 'dark' controls all color schemes
- abacusColumns: number controls abacus column count (default 5)

Updated homepage to use:
- abacusColumns={2} for simpler 2+3 demo
- theme="dark" for cohesive integration with dark page design
- Vertical layout with "What You'll Learn" below tutorial

Dark theme styling:
- Transparent dark backgrounds for all containers
- Muted text colors (gray.200-gray.400)
- Subtle borders and shadows
- Removed bright yellow/amber gradients

All changes maintain backward compatibility - defaults to light theme with 5 columns.

🤖 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 13:12:18 -05:00
parent faaefbacff
commit d42f9b2d9a
4 changed files with 92 additions and 138 deletions

View File

@@ -292,10 +292,10 @@ export default function RoomPage() {
disabled={isDisabled}
style={{
background: gameDef.manifest.gradient,
borderColor: gameDef.manifest.borderColor,
}}
className={css({
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: isDisabled ? 'not-allowed' : 'pointer',

View File

@@ -206,122 +206,49 @@ export default function HomePage() {
</p>
</div>
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8' })}>
{/* Live demo */}
{/* Live demo and learning objectives */}
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
maxW: '900px',
mx: 'auto',
})}
>
<TutorialPlayer
tutorial={friendsOf5Tutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
abacusColumns={2}
theme="dark"
/>
{/* What you'll learn - below tutorial */}
<div
className={`homepage-tutorial-demo ${css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '6',
border: '1px solid',
className={css({
mt: '8',
pt: '6',
borderTop: '1px solid',
borderColor: 'gray.700',
shadow: 'lg',
})}`}
>
<style jsx>{`
/* Hide close button on homepage tutorial */
:global(.homepage-tutorial-demo .coachbar__hide) {
display: none !important;
}
/* Soften white backgrounds to light gray */
:global(.homepage-tutorial-demo) {
/* Override abacus container white background */
& :global(> div > div > div) {
background: #f9fafb !important; /* gray.50 equivalent */
}
}
/* Mute text colors to darker grays */
:global(.homepage-tutorial-demo h2) {
color: #1f2937 !important; /* gray.800 */
}
:global(.homepage-tutorial-demo p) {
color: #4b5563 !important; /* gray.600 - muted from gray.700 */
}
/* Soften guidance box - add more transparency and muted colors */
:global(.homepage-tutorial-demo) :global(div[style*='linear-gradient'][style*='255,248,225']) {
background: linear-gradient(
135deg,
rgba(255, 248, 225, 0.7) 0%,
rgba(254, 252, 232, 0.75) 50%,
rgba(255, 245, 157, 0.1) 100%
) !important;
padding: 1.75rem !important; /* Increase from p-5 (1.25rem) */
box-shadow: 0 4px 16px rgba(251, 191, 36, 0.06),
0 1px 4px rgba(0, 0, 0, 0.03),
inset 0 1px 0 rgba(255, 255, 255, 0.4) !important;
}
/* Soften pedagogical decomposition inner box */
:global(.homepage-tutorial-demo) :global(div[style*='linear-gradient'][style*='248,250,252']) {
background: linear-gradient(
135deg,
rgba(249, 250, 251, 0.6) 0%,
rgba(243, 244, 246, 0.7) 100%
) !important; /* Softer gray gradient */
padding: 1rem !important; /* Increase from 0.75rem */
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.5) !important;
}
/* Soften shadows on abacus container */
:global(.homepage-tutorial-demo) :global(div[style*='box-shadow']) {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05),
0 2px 4px -1px rgba(0, 0, 0, 0.03) !important;
}
/* Mute amber text colors */
:global(.homepage-tutorial-demo) :global([style*='color'][style*='amber']) {
color: #78716c !important; /* stone.500 - more muted than amber */
}
/* Mute slate text colors in decomposition */
:global(.homepage-tutorial-demo) :global([style*='color'][style*='slate']) {
color: #64748b !important; /* slate.600 - muted from slate.800 */
}
/* Soften CoachBar if visible */
:global(.homepage-tutorial-demo .coachbar) {
background: #f9fafb !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02) !important;
padding: 10px 14px !important; /* Slight increase */
}
/* Soften reason tooltip backgrounds */
:global(.homepage-tutorial-demo .reason-tooltip) {
background: #f9fafb !important;
border: 1px solid #e5e7eb !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06) !important;
padding: 14px !important; /* Increase from 12px */
}
`}</style>
<TutorialPlayer
tutorial={friendsOf5Tutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
/>
</div>
{/* What you'll learn */}
<div
className={stack({
gap: '4',
bg: 'rgba(0, 0, 0, 0.4)',
p: '6',
borderRadius: 'xl',
border: '1px solid',
borderColor: 'gray.700',
justifyContent: 'center',
})}
>
<h3 className={css({ fontSize: 'xl', fontWeight: 'bold', color: 'white' })}>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'white',
mb: '4',
textAlign: 'center',
})}
>
What You'll Learn
</h3>
<div className={stack({ gap: '3' })}>
<div className={grid({ columns: { base: 1, sm: 2 }, gap: '3' })}>
{[
'Read and set numbers on an abacus',
'Add and subtract with "friends" techniques',
@@ -330,7 +257,7 @@ export default function HomePage() {
].map((skill, i) => (
<div key={i} className={hstack({ gap: '3' })}>
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}>✓</span>
<span className={css({ color: 'gray.300', fontSize: 'md' })}>{skill}</span>
<span className={css({ color: 'gray.300', fontSize: 'sm' })}>{skill}</span>
</div>
))}
</div>

View File

@@ -216,6 +216,8 @@ interface TutorialPlayerProps {
isDebugMode?: boolean
showDebugPanel?: boolean
hideNavigation?: boolean
abacusColumns?: number
theme?: 'light' | 'dark'
onStepChange?: (stepIndex: number, step: TutorialStep) => void
onStepComplete?: (stepIndex: number, step: TutorialStep, success: boolean) => void
onTutorialComplete?: (score: number, timeSpent: number) => void
@@ -229,6 +231,8 @@ function TutorialPlayerContent({
isDebugMode = false,
showDebugPanel = false,
hideNavigation = false,
abacusColumns = 5,
theme = 'light',
onStepChange,
onStepComplete,
onTutorialComplete,
@@ -1319,11 +1323,18 @@ function TutorialPlayerContent({
fontSize: '2xl',
fontWeight: 'bold',
mb: 2,
color: theme === 'dark' ? 'gray.200' : 'gray.900',
})}
>
{currentStep.problem}
</h2>
<p className={css({ fontSize: 'lg', color: 'gray.700', mb: 4 })}>
<p
className={css({
fontSize: 'lg',
color: theme === 'dark' ? 'gray.400' : 'gray.700',
mb: 4,
})}
>
{currentStep.description}
</p>
{/* Hide action description for multi-step problems since it duplicates pedagogical decomposition */}
@@ -1341,12 +1352,19 @@ function TutorialPlayerContent({
className={css({
p: 5,
background:
'linear-gradient(135deg, rgba(255,248,225,0.95) 0%, rgba(254,252,232,0.95) 50%, rgba(255,245,157,0.15) 100%)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(40,40,50,0.6) 0%, rgba(50,50,60,0.6) 50%, rgba(60,50,70,0.3) 100%)'
: 'linear-gradient(135deg, rgba(255,248,225,0.95) 0%, rgba(254,252,232,0.95) 50%, rgba(255,245,157,0.15) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(251,191,36,0.3)',
border:
theme === 'dark'
? '1px solid rgba(255,255,255,0.1)'
: '1px solid rgba(251,191,36,0.3)',
borderRadius: 'xl',
boxShadow:
'0 8px 32px rgba(251,191,36,0.1), 0 2px 8px rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.6)',
theme === 'dark'
? '0 4px 16px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'
: '0 8px 32px rgba(251,191,36,0.1), 0 2px 8px rgba(0,0,0,0.05), inset 0 1px 0 rgba(255,255,255,0.6)',
position: 'relative',
maxW: '600px',
w: 'full',
@@ -1356,7 +1374,9 @@ function TutorialPlayerContent({
inset: '0',
borderRadius: 'xl',
background:
'linear-gradient(135deg, rgba(251,191,36,0.1) 0%, rgba(168,85,247,0.05) 100%)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(100,100,120,0.1) 0%, rgba(80,60,100,0.05) 100%)'
: 'linear-gradient(135deg, rgba(251,191,36,0.1) 0%, rgba(168,85,247,0.05) 100%)',
zIndex: -1,
},
})}
@@ -1365,10 +1385,10 @@ function TutorialPlayerContent({
className={css({
fontSize: 'base',
fontWeight: '600',
color: 'amber.900',
color: theme === 'dark' ? 'gray.300' : 'amber.900',
mb: 4,
letterSpacing: 'wide',
textShadow: '0 1px 2px rgba(0,0,0,0.1)',
textShadow: theme === 'dark' ? 'none' : '0 1px 2px rgba(0,0,0,0.1)',
})}
>
Guidance
@@ -1381,18 +1401,25 @@ function TutorialPlayerContent({
mb: 4,
p: 3,
background:
'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.9) 100%)',
border: '1px solid rgba(203,213,225,0.4)',
theme === 'dark'
? 'linear-gradient(135deg, rgba(50,50,60,0.4) 0%, rgba(40,40,50,0.5) 100%)'
: 'linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(248,250,252,0.9) 100%)',
border:
theme === 'dark'
? '1px solid rgba(255,255,255,0.1)'
: '1px solid rgba(203,213,225,0.4)',
borderRadius: 'lg',
boxShadow:
'0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.7)',
theme === 'dark'
? '0 1px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05)'
: '0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.7)',
backdropFilter: 'blur(4px)',
})}
>
<div
className={css({
fontSize: 'base',
color: 'slate.800',
color: theme === 'dark' ? 'gray.300' : 'slate.800',
fontFamily: 'mono',
fontWeight: '500',
letterSpacing: 'tight',
@@ -1411,7 +1438,7 @@ function TutorialPlayerContent({
<div
className={css({
fontSize: 'sm',
color: 'amber.800',
color: theme === 'dark' ? 'gray.400' : 'amber.800',
fontWeight: '500',
lineHeight: '1.6',
})}
@@ -1489,17 +1516,17 @@ function TutorialPlayerContent({
{/* Abacus */}
<div
className={css({
bg: 'white',
bg: theme === 'dark' ? 'rgba(30, 30, 40, 0.4)' : 'white',
border: '2px solid',
borderColor: 'gray.200',
borderColor: theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'gray.200',
borderRadius: 'lg',
p: 6,
shadow: 'lg',
shadow: theme === 'dark' ? '0 4px 6px rgba(0, 0, 0, 0.3)' : 'lg',
})}
>
<AbacusReact
value={currentValue}
columns={5}
columns={abacusColumns}
interactive={true}
animated={true}
scaleFactor={2.5}

View File

@@ -21,52 +21,52 @@ export const GAME_THEMES = {
blue: {
color: 'blue',
gradient: 'linear-gradient(135deg, #dbeafe, #bfdbfe)', // blue.100 to blue.200
borderColor: 'blue.200',
borderColor: '#bfdbfe', // blue.200
},
purple: {
color: 'purple',
gradient: 'linear-gradient(135deg, #e9d5ff, #ddd6fe)', // purple.100 to purple.200
borderColor: 'purple.200',
borderColor: '#ddd6fe', // purple.200
},
green: {
color: 'green',
gradient: 'linear-gradient(135deg, #d1fae5, #a7f3d0)', // green.100 to green.200
borderColor: 'green.200',
borderColor: '#a7f3d0', // green.200
},
teal: {
color: 'teal',
gradient: 'linear-gradient(135deg, #ccfbf1, #99f6e4)', // teal.100 to teal.200
borderColor: 'teal.200',
borderColor: '#99f6e4', // teal.200
},
indigo: {
color: 'indigo',
gradient: 'linear-gradient(135deg, #e0e7ff, #c7d2fe)', // indigo.100 to indigo.200
borderColor: 'indigo.200',
borderColor: '#c7d2fe', // indigo.200
},
pink: {
color: 'pink',
gradient: 'linear-gradient(135deg, #fce7f3, #fbcfe8)', // pink.100 to pink.200
borderColor: 'pink.200',
borderColor: '#fbcfe8', // pink.200
},
orange: {
color: 'orange',
gradient: 'linear-gradient(135deg, #ffedd5, #fed7aa)', // orange.100 to orange.200
borderColor: 'orange.200',
borderColor: '#fed7aa', // orange.200
},
yellow: {
color: 'yellow',
gradient: 'linear-gradient(135deg, #fef3c7, #fde68a)', // yellow.100 to yellow.200
borderColor: 'yellow.200',
borderColor: '#fde68a', // yellow.200
},
red: {
color: 'red',
gradient: 'linear-gradient(135deg, #fee2e2, #fecaca)', // red.100 to red.200
borderColor: 'red.200',
borderColor: '#fecaca', // red.200
},
gray: {
color: 'gray',
gradient: 'linear-gradient(135deg, #f3f4f6, #e5e7eb)', // gray.100 to gray.200
borderColor: 'gray.200',
borderColor: '#e5e7eb', // gray.200
},
} as const satisfies Record<string, GameTheme>