feat(practice): add dark mode support and fix doubled answer digits

- Add dark mode support to all practice components:
  - ActiveSession, VerticalProblem, NumericKeypad, HelpAbacus
  - StudentSelector, ProgressDashboard, PlanReview, SessionSummary
  - OfflineSessionForm, ManualSkillSelector, PlacementTest, PracticeHelpPanel
- Fix doubled answer digit cells in VerticalProblem by consolidating
  two separate cell-rendering loops into a single unified loop

🤖 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:59:23 -06:00
parent a50b268d35
commit 026993cb05
13 changed files with 756 additions and 390 deletions

View File

@@ -98,7 +98,40 @@
"Bash(apps/web/src/arcade-games/know-your-world/features/interaction/ )",
"Bash(apps/web/src/arcade-games/know-your-world/utils/heatStyles.ts)",
"Bash(ping:*)",
"WebFetch(domain:typst.app)"
"WebFetch(domain:typst.app)",
"WebFetch(domain:finemotormath.com)",
"WebFetch(domain:learnabacusathome.com)",
"WebFetch(domain:totton.idirect.com)",
"Bash(npx drizzle-kit:*)",
"Bash(npm run db:migrate:*)",
"mcp__sqlite__list_tables",
"Bash(sqlite3:*)",
"Bash(npx eslint:*)",
"Bash(src/hooks/useDeviceCapabilities.ts )",
"Bash(src/arcade-games/know-your-world/hooks/useDeviceCapabilities.ts )",
"Bash(src/components/practice/hooks/useDeviceDetection.ts )",
"Bash(src/arcade-games/memory-quiz/components/InputPhase.tsx )",
"Bash(src/app/api/curriculum/*/sessions/plans/route.ts)",
"Bash(src/app/api/curriculum/*/sessions/plans/*/route.ts)",
"Bash(src/components/practice/SessionSummary.tsx )",
"Bash(src/components/practice/ )",
"Bash(src/app/practice/ )",
"Bash(src/app/api/curriculum/ )",
"Bash(src/hooks/usePlayerCurriculum.ts )",
"Bash(src/hooks/useSessionPlan.ts )",
"Bash(src/lib/curriculum/ )",
"Bash(src/db/schema/player-curriculum.ts )",
"Bash(src/db/schema/player-skill-mastery.ts )",
"Bash(src/db/schema/practice-sessions.ts )",
"Bash(src/db/schema/session-plans.ts )",
"Bash(src/db/schema/index.ts )",
"Bash(src/types/tutorial.ts )",
"Bash(src/utils/problemGenerator.ts )",
"Bash(drizzle/ )",
"Bash(docs/DAILY_PRACTICE_SYSTEM.md )",
"Bash(../../README.md )",
"Bash(.claude/CLAUDE.md)",
"Bash(mcp__sqlite__describe_table:*)"
],
"deny": [],
"ask": []

View File

@@ -11,6 +11,7 @@ import type {
SlotResult,
} from '@/db/schema/session-plans'
import type { StudentHelpSettings } from '@/db/schema/players'
import { useTheme } from '@/contexts/ThemeContext'
import { usePracticeHelp } from '@/hooks/usePracticeHelp'
import { createBasicSkillSet, type SkillSet } from '@/types/tutorial'
import {
@@ -79,20 +80,29 @@ function getPartTypeEmoji(type: SessionPart['type']): string {
}
/**
* Get part type colors
* Get part type colors (dark mode aware)
*/
function getPartTypeColors(type: SessionPart['type']): {
function getPartTypeColors(
type: SessionPart['type'],
isDark: boolean
): {
bg: string
border: string
text: string
} {
switch (type) {
case 'abacus':
return { bg: 'blue.50', border: 'blue.200', text: 'blue.700' }
return isDark
? { bg: 'blue.900', border: 'blue.700', text: 'blue.200' }
: { bg: 'blue.50', border: 'blue.200', text: 'blue.700' }
case 'visualization':
return { bg: 'purple.50', border: 'purple.200', text: 'purple.700' }
return isDark
? { bg: 'purple.900', border: 'purple.700', text: 'purple.200' }
: { bg: 'purple.50', border: 'purple.200', text: 'purple.700' }
case 'linear':
return { bg: 'orange.50', border: 'orange.200', text: 'orange.700' }
return isDark
? { bg: 'orange.900', border: 'orange.700', text: 'orange.200' }
: { bg: 'orange.50', border: 'orange.200', text: 'orange.700' }
}
}
@@ -105,12 +115,14 @@ function LinearProblem({
isFocused,
isCompleted,
correctAnswer,
isDark,
}: {
terms: number[]
userAnswer: string
isFocused: boolean
isCompleted: boolean
correctAnswer: number
isDark: boolean
}) {
// Build the equation string
const equation = terms
@@ -133,7 +145,7 @@ function LinearProblem({
fontWeight: 'bold',
})}
>
<span className={css({ color: 'gray.800' })}>{equation} =</span>
<span className={css({ color: isDark ? 'gray.200' : 'gray.800' })}>{equation} =</span>
<span
className={css({
minWidth: '80px',
@@ -142,16 +154,28 @@ function LinearProblem({
textAlign: 'center',
backgroundColor: isCompleted
? userAnswer === String(correctAnswer)
? 'green.100'
: 'red.100'
: 'gray.100',
? isDark
? 'green.900'
: 'green.100'
: isDark
? 'red.900'
: 'red.100'
: isDark
? 'gray.800'
: 'gray.100',
color: isCompleted
? userAnswer === String(correctAnswer)
? 'green.700'
: 'red.700'
: 'gray.800',
? isDark
? 'green.200'
: 'green.700'
: isDark
? 'red.200'
: 'red.700'
: isDark
? 'gray.200'
: 'gray.800',
border: '2px solid',
borderColor: isFocused ? 'blue.400' : 'gray.300',
borderColor: isFocused ? 'blue.400' : isDark ? 'gray.600' : 'gray.300',
})}
>
{userAnswer || (isFocused ? '?' : '')}
@@ -182,6 +206,9 @@ export function ActiveSession({
helpSettings,
isBeginnerMode = false,
}: ActiveSessionProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [currentProblem, setCurrentProblem] = useState<CurrentProblem | null>(null)
const [userAnswer, setUserAnswer] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
@@ -534,7 +561,7 @@ export function ActiveSession({
<div
className={css({
fontSize: '1.25rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
Loading next problem...
@@ -543,7 +570,7 @@ export function ActiveSession({
)
}
const partColors = getPartTypeColors(currentPart.type)
const partColors = getPartTypeColors(currentPart.type, isDark)
return (
<div
@@ -791,7 +818,7 @@ export function ActiveSession({
alignItems: 'center',
gap: '1.5rem',
padding: '2rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '16px',
boxShadow: 'md',
})}
@@ -807,20 +834,36 @@ export function ActiveSession({
textTransform: 'uppercase',
backgroundColor:
currentSlot?.purpose === 'focus'
? 'blue.100'
? isDark
? 'blue.900'
: 'blue.100'
: currentSlot?.purpose === 'reinforce'
? 'orange.100'
? isDark
? 'orange.900'
: 'orange.100'
: currentSlot?.purpose === 'review'
? 'green.100'
: 'purple.100',
? isDark
? 'green.900'
: 'green.100'
: isDark
? 'purple.900'
: 'purple.100',
color:
currentSlot?.purpose === 'focus'
? 'blue.700'
? isDark
? 'blue.200'
: 'blue.700'
: currentSlot?.purpose === 'reinforce'
? 'orange.700'
? isDark
? 'orange.200'
: 'orange.700'
: currentSlot?.purpose === 'review'
? 'green.700'
: 'purple.700',
? isDark
? 'green.200'
: 'green.700'
: isDark
? 'purple.200'
: 'purple.700',
})}
>
{currentSlot?.purpose}
@@ -856,6 +899,7 @@ export function ActiveSession({
isFocused={!isPaused && !isSubmitting}
isCompleted={feedback !== 'none'}
correctAnswer={currentProblem.problem.answer}
isDark={isDark}
/>
)}
@@ -865,10 +909,10 @@ export function ActiveSession({
data-section="term-help"
className={css({
padding: '1rem',
backgroundColor: 'purple.50',
backgroundColor: isDark ? 'purple.900' : 'purple.50',
borderRadius: '12px',
border: '2px solid',
borderColor: 'purple.200',
borderColor: isDark ? 'purple.700' : 'purple.200',
minWidth: '200px',
})}
>
@@ -884,7 +928,7 @@ export function ActiveSession({
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'purple.700',
color: isDark ? 'purple.200' : 'purple.700',
})}
>
{helpContext.term >= 0 ? '+' : ''}
@@ -895,12 +939,12 @@ export function ActiveSession({
onClick={handleDismissHelp}
className={css({
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
_hover: { color: 'gray.700' },
_hover: { color: isDark ? 'gray.200' : 'gray.700' },
})}
>
@@ -928,8 +972,22 @@ export function ActiveSession({
borderRadius: '8px',
fontSize: '1.25rem',
fontWeight: 'bold',
backgroundColor: feedback === 'correct' ? 'green.100' : 'red.100',
color: feedback === 'correct' ? 'green.700' : 'red.700',
backgroundColor:
feedback === 'correct'
? isDark
? 'green.900'
: 'green.100'
: isDark
? 'red.900'
: 'red.100',
color:
feedback === 'correct'
? isDark
? 'green.200'
: 'green.700'
: isDark
? 'red.200'
: 'red.700',
})}
>
{feedback === 'correct'
@@ -969,8 +1027,10 @@ export function ActiveSession({
? 'purple.500'
: buttonState === 'submit'
? 'blue.500'
: 'gray.300',
color: buttonState === 'disabled' ? 'gray.500' : 'white',
: isDark
? 'gray.700'
: 'gray.300',
color: buttonState === 'disabled' ? (isDark ? 'gray.400' : 'gray.500') : 'white',
opacity: buttonState === 'disabled' ? 0.5 : 1,
_hover: {
backgroundColor:
@@ -978,7 +1038,9 @@ export function ActiveSession({
? 'purple.600'
: buttonState === 'submit'
? 'blue.600'
: 'gray.300',
: isDark
? 'gray.600'
: 'gray.300',
},
})}
>
@@ -995,7 +1057,7 @@ export function ActiveSession({
<div
className={css({
textAlign: 'center',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
fontSize: '0.875rem',
marginBottom: '1rem',
})}
@@ -1028,7 +1090,7 @@ export function ActiveSession({
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -1038,7 +1100,7 @@ export function ActiveSession({
<div
className={css({
padding: '2rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '16px',
textAlign: 'center',
})}
@@ -1055,7 +1117,7 @@ export function ActiveSession({
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
marginBottom: '0.5rem',
})}
>
@@ -1064,7 +1126,7 @@ export function ActiveSession({
<div
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
Take a break! Tap Resume when ready.

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import {
AbacusReact,
calculateBeadDiffFromValues,
@@ -51,6 +52,8 @@ export function HelpAbacus({
onValueChange,
interactive = false,
}: HelpAbacusProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const { config: abacusConfig } = useAbacusDisplay()
const [currentStep] = useState(0)
@@ -115,10 +118,10 @@ export function HelpAbacus({
return {
// Subtle background to indicate this is a help visualization
frame: {
fill: 'rgba(59, 130, 246, 0.05)',
fill: isDark ? 'rgba(59, 130, 246, 0.15)' : 'rgba(59, 130, 246, 0.05)',
},
}
}, [])
}, [isDark])
if (!hasChanges) {
return (
@@ -128,7 +131,7 @@ export function HelpAbacus({
className={css({
textAlign: 'center',
padding: '1rem',
color: 'green.600',
color: isDark ? 'green.400' : 'green.600',
fontSize: '0.875rem',
})}
>
@@ -153,10 +156,10 @@ export function HelpAbacus({
data-element="help-summary"
className={css({
padding: '0.5rem 1rem',
backgroundColor: 'blue.50',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
borderRadius: '8px',
fontSize: '0.875rem',
color: 'blue.700',
color: isDark ? 'blue.200' : 'blue.700',
fontWeight: 'medium',
textAlign: 'center',
})}
@@ -169,10 +172,10 @@ export function HelpAbacus({
<div
className={css({
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
border: '2px solid',
borderColor: 'blue.200',
borderColor: isDark ? 'blue.700' : 'blue.200',
boxShadow: 'md',
})}
>
@@ -203,18 +206,44 @@ export function HelpAbacus({
fontSize: '0.875rem',
})}
>
<div className={css({ color: isAtTarget ? 'green.600' : 'gray.600' })}>
<div
className={css({
color: isAtTarget ? (isDark ? 'green.400' : 'green.600') : isDark ? 'gray.400' : 'gray.600',
})}
>
Current:{' '}
<span
className={css({ fontWeight: 'bold', color: isAtTarget ? 'green.700' : 'gray.800' })}
className={css({
fontWeight: 'bold',
color: isAtTarget
? isDark
? 'green.300'
: 'green.700'
: isDark
? 'gray.200'
: 'gray.800',
})}
>
{displayedValue}
</span>
</div>
<div className={css({ color: isAtTarget ? 'green.600' : 'blue.600' })}>
<div
className={css({
color: isAtTarget ? (isDark ? 'green.400' : 'green.600') : isDark ? 'blue.400' : 'blue.600',
})}
>
Target:{' '}
<span
className={css({ fontWeight: 'bold', color: isAtTarget ? 'green.700' : 'blue.800' })}
className={css({
fontWeight: 'bold',
color: isAtTarget
? isDark
? 'green.300'
: 'green.700'
: isDark
? 'blue.300'
: 'blue.800',
})}
>
{targetValue}
</span>
@@ -227,10 +256,10 @@ export function HelpAbacus({
data-element="target-reached"
className={css({
padding: '0.5rem 1rem',
backgroundColor: 'green.100',
backgroundColor: isDark ? 'green.900' : 'green.100',
borderRadius: '8px',
fontSize: '0.875rem',
color: 'green.700',
color: isDark ? 'green.200' : 'green.700',
fontWeight: 'bold',
textAlign: 'center',
})}

View File

@@ -1,8 +1,9 @@
'use client'
import { useState } from 'react'
import * as Dialog from '@radix-ui/react-dialog'
import * as Accordion from '@radix-ui/react-accordion'
import * as Dialog from '@radix-ui/react-dialog'
import { useState } from 'react'
import { useTheme } from '@/contexts/ThemeContext'
import { css } from '../../../styled-system/css'
/**
@@ -185,6 +186,8 @@ export function ManualSkillSelector({
currentMasteredSkills = [],
onSave,
}: ManualSkillSelectorProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [selectedSkills, setSelectedSkills] = useState<Set<string>>(new Set(currentMasteredSkills))
const [isSaving, setIsSaving] = useState(false)
const [expandedCategories, setExpandedCategories] = useState<string[]>([])
@@ -276,7 +279,7 @@ export function ManualSkillSelector({
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bg: 'white',
bg: isDark ? 'gray.800' : 'white',
borderRadius: 'xl',
boxShadow: 'xl',
p: '6',
@@ -293,7 +296,7 @@ export function ManualSkillSelector({
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
color: isDark ? 'gray.100' : 'gray.900',
})}
>
Set Skills for {studentName}
@@ -301,7 +304,7 @@ export function ManualSkillSelector({
<Dialog.Description
className={css({
fontSize: 'sm',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
mt: '1',
})}
>
@@ -318,7 +321,7 @@ export function ManualSkillSelector({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '2',
})}
>
@@ -333,9 +336,10 @@ export function ManualSkillSelector({
px: '3',
py: '2',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
bg: 'white',
bg: isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
cursor: 'pointer',
_focus: {
@@ -359,7 +363,7 @@ export function ManualSkillSelector({
<div
className={css({
fontSize: 'sm',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
mb: '3',
display: 'flex',
justifyContent: 'space-between',
@@ -374,7 +378,7 @@ export function ManualSkillSelector({
onClick={() => setSelectedSkills(new Set())}
className={css({
fontSize: 'xs',
color: 'red.600',
color: isDark ? 'red.400' : 'red.600',
bg: 'transparent',
border: 'none',
cursor: 'pointer',
@@ -392,7 +396,7 @@ export function ManualSkillSelector({
onValueChange={setExpandedCategories}
className={css({
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.600' : 'gray.200',
borderRadius: 'lg',
overflow: 'hidden',
})}
@@ -418,7 +422,7 @@ export function ManualSkillSelector({
value={categoryKey}
className={css({
borderBottom: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.600' : 'gray.200',
_last: { borderBottom: 'none' },
})}
>
@@ -430,11 +434,11 @@ export function ManualSkillSelector({
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
bg: 'gray.50',
bg: isDark ? 'gray.700' : 'gray.50',
border: 'none',
cursor: 'pointer',
textAlign: 'left',
_hover: { bg: 'gray.100' },
_hover: { bg: isDark ? 'gray.600' : 'gray.100' },
})}
>
<div
@@ -464,7 +468,7 @@ export function ManualSkillSelector({
<span
className={css({
fontWeight: 'semibold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{category.name}
@@ -480,7 +484,7 @@ export function ManualSkillSelector({
<span
className={css({
fontSize: 'xs',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
{selectedInCategory}/{categorySkillIds.length}
@@ -498,7 +502,7 @@ export function ManualSkillSelector({
<Accordion.Content
className={css({
overflow: 'hidden',
bg: 'white',
bg: isDark ? 'gray.800' : 'white',
})}
>
<div className={css({ p: '3' })}>
@@ -516,7 +520,7 @@ export function ManualSkillSelector({
padding: '8px 12px',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
_hover: { bg: isDark ? 'gray.700' : 'gray.50' },
})}
>
<input
@@ -532,7 +536,13 @@ export function ManualSkillSelector({
<span
className={css({
fontSize: 'sm',
color: isSelected ? 'green.700' : 'gray.700',
color: isSelected
? isDark
? 'green.400'
: 'green.700'
: isDark
? 'gray.300'
: 'gray.700',
fontWeight: isSelected ? 'medium' : 'normal',
})}
>
@@ -542,8 +552,8 @@ export function ManualSkillSelector({
<span
className={css({
fontSize: 'xs',
color: 'green.600',
bg: 'green.50',
color: isDark ? 'green.300' : 'green.600',
bg: isDark ? 'green.900' : 'green.50',
px: '2',
py: '0.5',
borderRadius: 'full',
@@ -581,13 +591,13 @@ export function ManualSkillSelector({
py: '2',
fontSize: 'sm',
fontWeight: 'medium',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
bg: 'transparent',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
_hover: { bg: isDark ? 'gray.700' : 'gray.50' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
})}
>

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import { useCallback, useRef } from 'react'
import Keyboard from 'react-simple-keyboard'
import 'react-simple-keyboard/build/css/index.css'
@@ -29,6 +30,8 @@ export function NumericKeypad({
disabled = false,
currentValue = '',
}: NumericKeypadProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const keyboardRef = useRef<any>(null)
// Numeric layout with backspace and submit
@@ -69,20 +72,20 @@ export function NumericKeypad({
>
<style>{`
.practice-numeric-keyboard .simple-keyboard {
background: #f8fafc;
background: ${isDark ? '#1f2937' : '#f8fafc'};
border-radius: 12px;
padding: 8px;
border: 1px solid #e2e8f0;
border: 1px solid ${isDark ? '#374151' : '#e2e8f0'};
}
.practice-numeric-keyboard .hg-button {
height: 56px;
border-radius: 8px;
background: white;
color: #1e293b;
border: 1px solid #e2e8f0;
background: ${isDark ? '#374151' : 'white'};
color: ${isDark ? '#f3f4f6' : '#1e293b'};
border: 1px solid ${isDark ? '#4b5563' : '#e2e8f0'};
font-size: 24px;
font-weight: 600;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
box-shadow: ${isDark ? '0 1px 3px rgba(0, 0, 0, 0.3)' : '0 1px 3px rgba(0, 0, 0, 0.1)'};
transition: all 0.1s ease;
flex: 1;
margin: 3px;
@@ -93,18 +96,18 @@ export function NumericKeypad({
transform: scale(0.95);
}
.practice-numeric-keyboard .hg-button[data-skbtn="{bksp}"] {
background: #fee2e2;
color: #dc2626;
border-color: #fecaca;
background: ${isDark ? '#7f1d1d' : '#fee2e2'};
color: ${isDark ? '#fca5a5' : '#dc2626'};
border-color: ${isDark ? '#991b1b' : '#fecaca'};
}
.practice-numeric-keyboard .hg-button[data-skbtn="{bksp}"]:active {
background: #dc2626;
color: white;
}
.practice-numeric-keyboard .hg-button[data-skbtn="{enter}"] {
background: #dcfce7;
color: #16a34a;
border-color: #bbf7d0;
background: ${isDark ? '#14532d' : '#dcfce7'};
color: ${isDark ? '#86efac' : '#16a34a'};
border-color: ${isDark ? '#166534' : '#bbf7d0'};
}
.practice-numeric-keyboard .hg-button[data-skbtn="{enter}"]:active {
background: #16a34a;

View File

@@ -1,7 +1,8 @@
'use client'
import { useState } from 'react'
import * as Dialog from '@radix-ui/react-dialog'
import { useState } from 'react'
import { useTheme } from '@/contexts/ThemeContext'
import { css } from '../../../styled-system/css'
/**
@@ -52,6 +53,8 @@ export function OfflineSessionForm({
playerId,
onSubmit,
}: OfflineSessionFormProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
// Form state
const [date, setDate] = useState(() => new Date().toISOString().split('T')[0])
const [problemCount, setProblemCount] = useState(20)
@@ -140,7 +143,7 @@ export function OfflineSessionForm({
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bg: 'white',
bg: isDark ? 'gray.800' : 'white',
borderRadius: 'xl',
boxShadow: 'xl',
p: '6',
@@ -157,7 +160,7 @@ export function OfflineSessionForm({
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
color: isDark ? 'gray.100' : 'gray.900',
})}
>
Record Offline Practice
@@ -165,7 +168,7 @@ export function OfflineSessionForm({
<Dialog.Description
className={css({
fontSize: 'sm',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
mt: '1',
})}
>
@@ -183,7 +186,7 @@ export function OfflineSessionForm({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '1',
})}
>
@@ -203,9 +206,11 @@ export function OfflineSessionForm({
px: '3',
py: '2',
border: '1px solid',
borderColor: errors.date ? 'red.500' : 'gray.300',
borderColor: errors.date ? 'red.500' : isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
fontSize: 'sm',
bg: isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900',
_focus: {
outline: 'none',
borderColor: 'blue.500',
@@ -215,7 +220,15 @@ export function OfflineSessionForm({
})}
/>
{errors.date && (
<p className={css({ fontSize: 'xs', color: 'red.600', mt: '1' })}>{errors.date}</p>
<p
className={css({
fontSize: 'xs',
color: isDark ? 'red.400' : 'red.600',
mt: '1',
})}
>
{errors.date}
</p>
)}
</div>
@@ -227,7 +240,7 @@ export function OfflineSessionForm({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '1',
})}
>
@@ -248,9 +261,11 @@ export function OfflineSessionForm({
px: '3',
py: '2',
border: '1px solid',
borderColor: errors.problemCount ? 'red.500' : 'gray.300',
borderColor: errors.problemCount ? 'red.500' : isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
fontSize: 'sm',
bg: isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900',
_focus: {
outline: 'none',
borderColor: 'blue.500',
@@ -260,7 +275,13 @@ export function OfflineSessionForm({
})}
/>
{errors.problemCount && (
<p className={css({ fontSize: 'xs', color: 'red.600', mt: '1' })}>
<p
className={css({
fontSize: 'xs',
color: isDark ? 'red.400' : 'red.600',
mt: '1',
})}
>
{errors.problemCount}
</p>
)}
@@ -276,7 +297,7 @@ export function OfflineSessionForm({
alignItems: 'baseline',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '1',
})}
>
@@ -284,7 +305,17 @@ export function OfflineSessionForm({
<span
className={css({
fontWeight: 'bold',
color: accuracy >= 85 ? 'green.600' : accuracy >= 70 ? 'yellow.600' : 'red.600',
color: isDark
? accuracy >= 85
? 'green.400'
: accuracy >= 70
? 'yellow.400'
: 'red.400'
: accuracy >= 85
? 'green.600'
: accuracy >= 70
? 'yellow.600'
: 'red.600',
})}
>
{accuracy}%
@@ -311,7 +342,7 @@ export function OfflineSessionForm({
display: 'flex',
justifyContent: 'space-between',
fontSize: 'xs',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
<span>0%</span>
@@ -321,7 +352,13 @@ export function OfflineSessionForm({
<span>100%</span>
</div>
{errors.accuracy && (
<p className={css({ fontSize: 'xs', color: 'red.600', mt: '1' })}>
<p
className={css({
fontSize: 'xs',
color: isDark ? 'red.400' : 'red.600',
mt: '1',
})}
>
{errors.accuracy}
</p>
)}
@@ -335,7 +372,7 @@ export function OfflineSessionForm({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '1',
})}
>
@@ -353,9 +390,10 @@ export function OfflineSessionForm({
px: '3',
py: '2',
border: '1px solid',
borderColor: errors.focusSkill ? 'red.500' : 'gray.300',
borderColor: errors.focusSkill ? 'red.500' : isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
bg: 'white',
bg: isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900',
fontSize: 'sm',
cursor: 'pointer',
_focus: {
@@ -373,7 +411,13 @@ export function OfflineSessionForm({
))}
</select>
{errors.focusSkill && (
<p className={css({ fontSize: 'xs', color: 'red.600', mt: '1' })}>
<p
className={css({
fontSize: 'xs',
color: isDark ? 'red.400' : 'red.600',
mt: '1',
})}
>
{errors.focusSkill}
</p>
)}
@@ -387,7 +431,7 @@ export function OfflineSessionForm({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '1',
})}
>
@@ -404,16 +448,21 @@ export function OfflineSessionForm({
px: '3',
py: '2',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
fontSize: 'sm',
resize: 'vertical',
bg: isDark ? 'gray.700' : 'white',
color: isDark ? 'gray.100' : 'gray.900',
_focus: {
outline: 'none',
borderColor: 'blue.500',
ring: '2px',
ringColor: 'blue.500/20',
},
_placeholder: {
color: isDark ? 'gray.500' : 'gray.400',
},
})}
/>
</div>
@@ -422,13 +471,13 @@ export function OfflineSessionForm({
<div
className={css({
p: '3',
bg: 'blue.50',
bg: isDark ? 'blue.900' : 'blue.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'blue.100',
borderColor: isDark ? 'blue.700' : 'blue.100',
})}
>
<p className={css({ fontSize: 'sm', color: 'blue.800' })}>
<p className={css({ fontSize: 'sm', color: isDark ? 'blue.200' : 'blue.800' })}>
This will record <strong>{problemCount} problems</strong> at{' '}
<strong>{accuracy}% accuracy</strong> (~{estimatedCorrect} correct) for{' '}
<strong>{studentName}</strong> on{' '}
@@ -456,13 +505,13 @@ export function OfflineSessionForm({
py: '2',
fontSize: 'sm',
fontWeight: 'medium',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
bg: 'transparent',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
borderRadius: 'md',
cursor: 'pointer',
_hover: { bg: 'gray.50' },
_hover: { bg: isDark ? 'gray.700' : 'gray.50' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
})}
>

View File

@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { css } from '../../../styled-system/css'
import { useTheme } from '@/contexts/ThemeContext'
import {
DEFAULT_THRESHOLDS,
generateProblemForSkill,
@@ -9,12 +9,13 @@ import {
initializePlacementTest,
type PlacementTestState,
type PlacementThresholds,
type PresetKey,
recordAnswer,
SKILL_NAMES,
SKILL_ORDER,
THRESHOLD_PRESETS,
type PresetKey,
} from '@/lib/curriculum/placement-test'
import { css } from '../../../styled-system/css'
import { NumericKeypad } from './NumericKeypad'
import { VerticalProblem } from './VerticalProblem'
@@ -54,6 +55,8 @@ export function PlacementTest({
onCancel,
initialThresholds = DEFAULT_THRESHOLDS,
}: PlacementTestProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [phase, setPhase] = useState<TestPhase>('setup')
const [thresholds, setThresholds] = useState<PlacementThresholds>(initialThresholds)
const [selectedPreset, setSelectedPreset] = useState<PresetKey>('standard')
@@ -166,7 +169,7 @@ export function PlacementTest({
className={css({
fontSize: '1.75rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
textAlign: 'center',
})}
>
@@ -176,7 +179,7 @@ export function PlacementTest({
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
textAlign: 'center',
})}
>
@@ -190,7 +193,7 @@ export function PlacementTest({
display: 'block',
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '2',
})}
>
@@ -213,19 +216,35 @@ export function PlacementTest({
px: '3',
borderRadius: 'lg',
border: '2px solid',
borderColor: selectedPreset === key ? 'blue.500' : 'gray.200',
bg: selectedPreset === key ? 'blue.50' : 'white',
borderColor:
selectedPreset === key ? 'blue.500' : isDark ? 'gray.600' : 'gray.200',
bg:
selectedPreset === key
? isDark
? 'blue.900'
: 'blue.50'
: isDark
? 'gray.800'
: 'white',
cursor: 'pointer',
transition: 'all 0.2s',
_hover: {
borderColor: selectedPreset === key ? 'blue.500' : 'gray.300',
borderColor:
selectedPreset === key ? 'blue.500' : isDark ? 'gray.500' : 'gray.300',
},
})}
>
<div
className={css({
fontWeight: 'bold',
color: selectedPreset === key ? 'blue.700' : 'gray.800',
color:
selectedPreset === key
? isDark
? 'blue.200'
: 'blue.700'
: isDark
? 'gray.100'
: 'gray.800',
fontSize: 'sm',
})}
>
@@ -234,7 +253,14 @@ export function PlacementTest({
<div
className={css({
fontSize: 'xs',
color: selectedPreset === key ? 'blue.600' : 'gray.500',
color:
selectedPreset === key
? isDark
? 'blue.300'
: 'blue.600'
: isDark
? 'gray.400'
: 'gray.500',
mt: '1',
})}
>
@@ -250,17 +276,17 @@ export function PlacementTest({
className={css({
width: '100%',
p: '4',
bg: 'gray.50',
bg: isDark ? 'gray.700' : 'gray.50',
borderRadius: 'lg',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.600' : 'gray.200',
})}
>
<h3
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '2',
})}
>
@@ -269,7 +295,7 @@ export function PlacementTest({
<ul
className={css({
fontSize: 'sm',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
listStyle: 'none',
display: 'flex',
flexDirection: 'column',
@@ -295,12 +321,12 @@ export function PlacementTest({
py: '3',
fontSize: '1rem',
fontWeight: 'medium',
color: 'gray.700',
bg: 'gray.100',
color: isDark ? 'gray.300' : 'gray.700',
bg: isDark ? 'gray.700' : 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
_hover: { bg: 'gray.200' },
_hover: { bg: isDark ? 'gray.600' : 'gray.200' },
})}
>
Cancel
@@ -365,8 +391,10 @@ export function PlacementTest({
mb: '2',
})}
>
<span className={css({ fontSize: 'sm', color: 'gray.600' })}>Testing: {skillName}</span>
<span className={css({ fontSize: 'sm', color: 'gray.500' })}>
<span className={css({ fontSize: 'sm', color: isDark ? 'gray.400' : 'gray.600' })}>
Testing: {skillName}
</span>
<span className={css({ fontSize: 'sm', color: isDark ? 'gray.500' : 'gray.500' })}>
{testState.problemsAnswered} problems answered
</span>
</div>
@@ -374,7 +402,7 @@ export function PlacementTest({
className={css({
width: '100%',
height: '8px',
bg: 'gray.200',
bg: isDark ? 'gray.700' : 'gray.200',
borderRadius: '4px',
overflow: 'hidden',
})}
@@ -395,7 +423,7 @@ export function PlacementTest({
gap: '3',
mt: '2',
fontSize: 'xs',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
<span>
@@ -426,7 +454,13 @@ export function PlacementTest({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: lastAnswerCorrect ? 'green.600' : 'red.600',
color: lastAnswerCorrect
? isDark
? 'green.400'
: 'green.600'
: isDark
? 'red.400'
: 'red.600',
animation: 'pulse 0.5s ease-in-out',
})}
>
@@ -451,11 +485,11 @@ export function PlacementTest({
className={css({
mt: '2',
fontSize: 'sm',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
bg: 'transparent',
border: 'none',
cursor: 'pointer',
_hover: { color: 'gray.700', textDecoration: 'underline' },
_hover: { color: isDark ? 'gray.200' : 'gray.700', textDecoration: 'underline' },
})}
>
End Test Early
@@ -487,13 +521,13 @@ export function PlacementTest({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
mb: '2',
})}
>
Placement Complete!
</h1>
<p className={css({ fontSize: '1.25rem', color: 'blue.600' })}>
<p className={css({ fontSize: '1.25rem', color: isDark ? 'blue.300' : 'blue.600' })}>
{studentName} placed at: <strong>{results.suggestedLevel}</strong>
</p>
</div>
@@ -510,7 +544,7 @@ export function PlacementTest({
className={css({
flex: 1,
p: '3',
bg: 'green.50',
bg: isDark ? 'green.900' : 'green.50',
borderRadius: 'lg',
textAlign: 'center',
})}
@@ -519,18 +553,20 @@ export function PlacementTest({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'green.700',
color: isDark ? 'green.200' : 'green.700',
})}
>
{results.masteredSkills.length}
</div>
<div className={css({ fontSize: 'sm', color: 'green.600' })}>Skills Mastered</div>
<div className={css({ fontSize: 'sm', color: isDark ? 'green.300' : 'green.600' })}>
Skills Mastered
</div>
</div>
<div
className={css({
flex: 1,
p: '3',
bg: 'yellow.50',
bg: isDark ? 'yellow.900' : 'yellow.50',
borderRadius: 'lg',
textAlign: 'center',
})}
@@ -539,18 +575,20 @@ export function PlacementTest({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'yellow.700',
color: isDark ? 'yellow.200' : 'yellow.700',
})}
>
{results.practicingSkills.length}
</div>
<div className={css({ fontSize: 'sm', color: 'yellow.600' })}>Skills Practicing</div>
<div className={css({ fontSize: 'sm', color: isDark ? 'yellow.300' : 'yellow.600' })}>
Skills Practicing
</div>
</div>
<div
className={css({
flex: 1,
p: '3',
bg: 'blue.50',
bg: isDark ? 'blue.900' : 'blue.50',
borderRadius: 'lg',
textAlign: 'center',
})}
@@ -559,12 +597,14 @@ export function PlacementTest({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'blue.700',
color: isDark ? 'blue.200' : 'blue.700',
})}
>
{Math.round(results.overallAccuracy * 100)}%
</div>
<div className={css({ fontSize: 'sm', color: 'blue.600' })}>Accuracy</div>
<div className={css({ fontSize: 'sm', color: isDark ? 'blue.300' : 'blue.600' })}>
Accuracy
</div>
</div>
</div>
@@ -575,7 +615,7 @@ export function PlacementTest({
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '2',
})}
>
@@ -588,8 +628,8 @@ export function PlacementTest({
className={css({
px: '3',
py: '1',
bg: 'green.100',
color: 'green.700',
bg: isDark ? 'green.900' : 'green.100',
color: isDark ? 'green.200' : 'green.700',
borderRadius: 'full',
fontSize: 'xs',
fontWeight: 'medium',
@@ -608,7 +648,7 @@ export function PlacementTest({
className={css({
fontSize: 'sm',
fontWeight: 'semibold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
mb: '2',
})}
>
@@ -621,8 +661,8 @@ export function PlacementTest({
className={css({
px: '3',
py: '1',
bg: 'yellow.100',
color: 'yellow.700',
bg: isDark ? 'yellow.900' : 'yellow.100',
color: isDark ? 'yellow.200' : 'yellow.700',
borderRadius: 'full',
fontSize: 'xs',
fontWeight: 'medium',

View File

@@ -1,6 +1,6 @@
'use client'
import { useState } from 'react'
import { useTheme } from '@/contexts/ThemeContext'
import type {
PartSummary,
ProblemSlot,
@@ -8,6 +8,7 @@ import type {
SessionPlan,
SessionSummary,
} from '@/db/schema/session-plans'
import { useState } from 'react'
import { css } from '../../../styled-system/css'
interface PlanReviewProps {
@@ -32,20 +33,29 @@ function getPartTypeEmoji(type: SessionPart['type']): string {
}
/**
* Get part type colors
* Get part type colors (dark mode aware)
*/
function getPartTypeColors(type: SessionPart['type']): {
function getPartTypeColors(
type: SessionPart['type'],
isDark: boolean
): {
bg: string
border: string
text: string
} {
switch (type) {
case 'abacus':
return { bg: 'blue.50', border: 'blue.200', text: 'blue.700' }
return isDark
? { bg: 'blue.900', border: 'blue.700', text: 'blue.200' }
: { bg: 'blue.50', border: 'blue.200', text: 'blue.700' }
case 'visualization':
return { bg: 'purple.50', border: 'purple.200', text: 'purple.700' }
return isDark
? { bg: 'purple.900', border: 'purple.700', text: 'purple.200' }
: { bg: 'purple.50', border: 'purple.200', text: 'purple.700' }
case 'linear':
return { bg: 'orange.50', border: 'orange.200', text: 'orange.700' }
return isDark
? { bg: 'orange.900', border: 'orange.700', text: 'orange.200' }
: { bg: 'orange.50', border: 'orange.200', text: 'orange.700' }
}
}
@@ -59,6 +69,8 @@ function getPartTypeColors(type: SessionPart['type']): {
* - "Let's Go!" button to start
*/
export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanReviewProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const [showConfig, setShowConfig] = useState(false)
const summary = plan.summary as SessionSummary
@@ -94,7 +106,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1.75rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
marginBottom: '0.5rem',
})}
>
@@ -103,7 +115,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
Review your plan before starting
@@ -117,10 +129,10 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
width: '100%',
padding: '1.5rem',
borderRadius: '12px',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
boxShadow: 'md',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
{/* Time and problem count */}
@@ -132,7 +144,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
marginBottom: '1.5rem',
paddingBottom: '1rem',
borderBottom: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<div>
@@ -140,7 +152,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'blue.600',
color: isDark ? 'blue.400' : 'blue.600',
})}
>
~{summary.estimatedMinutes} min
@@ -148,7 +160,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.875rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
{summary.totalProblemCount} problems
@@ -162,7 +174,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.875rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
Focus: <strong>{summary.focusDescription}</strong>
@@ -180,7 +192,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
marginBottom: '0.75rem',
})}
>
@@ -195,7 +207,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
})}
>
{summary.parts.map((partSummary: PartSummary) => {
const colors = getPartTypeColors(partSummary.type)
const colors = getPartTypeColors(partSummary.type, isDark)
return (
<div
key={partSummary.partNumber}
@@ -251,7 +263,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.5rem',
borderRadius: '8px',
backgroundColor: 'blue.50',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
textAlign: 'center',
})}
>
@@ -259,7 +271,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'blue.700',
color: isDark ? 'blue.200' : 'blue.700',
})}
>
{focusCount}
@@ -267,7 +279,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.625rem',
color: 'blue.600',
color: isDark ? 'blue.300' : 'blue.600',
})}
>
Focus
@@ -278,7 +290,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.5rem',
borderRadius: '8px',
backgroundColor: 'orange.50',
backgroundColor: isDark ? 'orange.900' : 'orange.50',
textAlign: 'center',
})}
>
@@ -286,7 +298,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'orange.700',
color: isDark ? 'orange.200' : 'orange.700',
})}
>
{reinforceCount}
@@ -294,7 +306,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.625rem',
color: 'orange.600',
color: isDark ? 'orange.300' : 'orange.600',
})}
>
Reinforce
@@ -305,7 +317,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.5rem',
borderRadius: '8px',
backgroundColor: 'green.50',
backgroundColor: isDark ? 'green.900' : 'green.50',
textAlign: 'center',
})}
>
@@ -313,7 +325,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'green.700',
color: isDark ? 'green.200' : 'green.700',
})}
>
{reviewCount}
@@ -321,7 +333,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.625rem',
color: 'green.600',
color: isDark ? 'green.300' : 'green.600',
})}
>
Review
@@ -332,7 +344,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.5rem',
borderRadius: '8px',
backgroundColor: 'purple.50',
backgroundColor: isDark ? 'purple.900' : 'purple.50',
textAlign: 'center',
})}
>
@@ -340,7 +352,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '1.125rem',
fontWeight: 'bold',
color: 'purple.700',
color: isDark ? 'purple.200' : 'purple.700',
})}
>
{challengeCount}
@@ -348,7 +360,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<div
className={css({
fontSize: '0.625rem',
color: 'purple.600',
color: isDark ? 'purple.300' : 'purple.600',
})}
>
Challenge
@@ -368,14 +380,14 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
gap: '0.5rem',
padding: '0.5rem 1rem',
fontSize: '0.875rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
backgroundColor: 'transparent',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
borderRadius: '6px',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
},
})}
>
@@ -391,9 +403,9 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
width: '100%',
padding: '1rem',
borderRadius: '8px',
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
fontFamily: 'monospace',
fontSize: '0.75rem',
overflow: 'auto',
@@ -404,7 +416,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
marginBottom: '1rem',
})}
>
@@ -416,7 +428,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
<div>
@@ -438,7 +450,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
<strong>Created:</strong> {new Date(plan.createdAt).toLocaleString()}
</div>
<hr className={css({ margin: '0.5rem 0', borderColor: 'gray.300' })} />
<hr className={css({ margin: '0.5rem 0', borderColor: isDark ? 'gray.600' : 'gray.300' })} />
{parts.map((part: SessionPart) => (
<details key={part.partNumber}>
@@ -456,7 +468,7 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.25rem 0',
borderBottom: '1px dashed',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.600' : 'gray.200',
})}
>
<div>
@@ -517,14 +529,14 @@ export function PlanReview({ plan, studentName, onApprove, onCancel }: PlanRevie
className={css({
padding: '0.75rem',
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
backgroundColor: 'transparent',
borderRadius: '8px',
border: '1px solid',
borderColor: 'gray.300',
borderColor: isDark ? 'gray.600' : 'gray.300',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
},
})}
>

View File

@@ -1,6 +1,7 @@
'use client'
import { useCallback, useState } from 'react'
import { useTheme } from '@/contexts/ThemeContext'
import type { HelpLevel } from '@/db/schema/session-plans'
import type { PracticeHelpState } from '@/hooks/usePracticeHelp'
import { css } from '../../../styled-system/css'
@@ -58,6 +59,8 @@ export function PracticeHelpPanel({
currentValue,
targetValue,
}: PracticeHelpPanelProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const { currentLevel, content, isAvailable, maxLevelUsed } = helpState
const [isExpanded, setIsExpanded] = useState(false)
@@ -104,16 +107,16 @@ export function PracticeHelpPanel({
width: '100%',
padding: '0.75rem',
fontSize: '0.875rem',
color: 'blue.600',
backgroundColor: 'blue.50',
color: isDark ? 'blue.300' : 'blue.600',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
border: '1px solid',
borderColor: 'blue.200',
borderColor: isDark ? 'blue.700' : 'blue.200',
borderRadius: '8px',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
backgroundColor: 'blue.100',
borderColor: 'blue.300',
backgroundColor: isDark ? 'blue.800' : 'blue.100',
borderColor: isDark ? 'blue.600' : 'blue.300',
},
})}
>
@@ -134,10 +137,10 @@ export function PracticeHelpPanel({
flexDirection: 'column',
gap: '0.75rem',
padding: '1rem',
backgroundColor: 'blue.50',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
borderRadius: '12px',
border: '2px solid',
borderColor: 'blue.200',
borderColor: isDark ? 'blue.700' : 'blue.200',
})}
>
{/* Header with level indicator */}
@@ -161,7 +164,7 @@ export function PracticeHelpPanel({
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'blue.700',
color: isDark ? 'blue.200' : 'blue.700',
})}
>
{HELP_LEVEL_LABELS[currentLevel]}
@@ -181,7 +184,8 @@ export function PracticeHelpPanel({
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: level <= currentLevel ? 'blue.500' : 'blue.200',
backgroundColor:
level <= currentLevel ? 'blue.500' : isDark ? 'blue.700' : 'blue.200',
})}
/>
))}
@@ -195,12 +199,12 @@ export function PracticeHelpPanel({
className={css({
padding: '0.25rem 0.5rem',
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
backgroundColor: 'transparent',
border: 'none',
cursor: 'pointer',
_hover: {
color: 'gray.700',
color: isDark ? 'gray.200' : 'gray.700',
},
})}
>
@@ -214,16 +218,16 @@ export function PracticeHelpPanel({
data-element="coach-hint"
className={css({
padding: '0.75rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '8px',
border: '1px solid',
borderColor: 'blue.100',
borderColor: isDark ? 'blue.800' : 'blue.100',
})}
>
<p
className={css({
fontSize: '1rem',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
lineHeight: '1.5',
})}
>
@@ -238,17 +242,17 @@ export function PracticeHelpPanel({
data-element="decomposition"
className={css({
padding: '0.75rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '8px',
border: '1px solid',
borderColor: 'blue.100',
borderColor: isDark ? 'blue.800' : 'blue.100',
})}
>
<div
className={css({
fontSize: '0.75rem',
fontWeight: 'bold',
color: 'blue.600',
color: isDark ? 'blue.300' : 'blue.600',
marginBottom: '0.5rem',
textTransform: 'uppercase',
})}
@@ -259,7 +263,7 @@ export function PracticeHelpPanel({
className={css({
fontFamily: 'monospace',
fontSize: '1.125rem',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
wordBreak: 'break-word',
})}
>
@@ -281,7 +285,7 @@ export function PracticeHelpPanel({
key={segment.id}
className={css({
padding: '0.5rem',
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.700' : 'gray.50',
borderRadius: '6px',
fontSize: '0.875rem',
})}
@@ -289,12 +293,12 @@ export function PracticeHelpPanel({
<span
className={css({
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.200' : 'gray.700',
})}
>
{segment.readable?.title || `Column ${segment.place + 1}`}:
</span>{' '}
<span className={css({ color: 'gray.600' })}>
<span className={css({ color: isDark ? 'gray.400' : 'gray.600' })}>
{segment.readable?.summary || segment.expression}
</span>
</div>
@@ -310,17 +314,17 @@ export function PracticeHelpPanel({
data-element="help-abacus"
className={css({
padding: '0.75rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '8px',
border: '1px solid',
borderColor: 'purple.200',
borderColor: isDark ? 'purple.700' : 'purple.200',
})}
>
<div
className={css({
fontSize: '0.75rem',
fontWeight: 'bold',
color: 'purple.600',
color: isDark ? 'purple.300' : 'purple.600',
marginBottom: '0.75rem',
textTransform: 'uppercase',
textAlign: 'center',
@@ -341,10 +345,10 @@ export function PracticeHelpPanel({
className={css({
marginTop: '0.75rem',
padding: '0.5rem',
backgroundColor: 'purple.50',
backgroundColor: isDark ? 'purple.900' : 'purple.50',
borderRadius: '6px',
fontSize: '0.75rem',
color: 'purple.700',
color: isDark ? 'purple.200' : 'purple.700',
textAlign: 'center',
})}
>
@@ -363,17 +367,17 @@ export function PracticeHelpPanel({
data-element="bead-steps-text"
className={css({
padding: '0.75rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '8px',
border: '1px solid',
borderColor: 'blue.100',
borderColor: isDark ? 'blue.800' : 'blue.100',
})}
>
<div
className={css({
fontSize: '0.75rem',
fontWeight: 'bold',
color: 'purple.600',
color: isDark ? 'purple.300' : 'purple.600',
marginBottom: '0.5rem',
textTransform: 'uppercase',
})}
@@ -394,19 +398,22 @@ export function PracticeHelpPanel({
key={index}
className={css({
fontSize: '0.875rem',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
})}
>
<span
className={css({
fontWeight: 'bold',
color: 'purple.700',
color: isDark ? 'purple.300' : 'purple.700',
})}
>
{step.mathematicalTerm}
</span>
{step.englishInstruction && (
<span className={css({ color: 'gray.600' })}> {step.englishInstruction}</span>
<span className={css({ color: isDark ? 'gray.400' : 'gray.600' })}>
{' '}
{step.englishInstruction}
</span>
)}
</li>
))}
@@ -428,15 +435,15 @@ export function PracticeHelpPanel({
width: '100%',
padding: '0.5rem',
fontSize: '0.875rem',
color: 'blue.600',
backgroundColor: 'white',
color: isDark ? 'blue.300' : 'blue.600',
backgroundColor: isDark ? 'gray.800' : 'white',
border: '1px solid',
borderColor: 'blue.200',
borderColor: isDark ? 'blue.700' : 'blue.200',
borderRadius: '6px',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
backgroundColor: 'blue.50',
backgroundColor: isDark ? 'gray.700' : 'blue.50',
},
})}
>
@@ -450,7 +457,7 @@ export function PracticeHelpPanel({
<div
className={css({
fontSize: '0.75rem',
color: 'gray.400',
color: isDark ? 'gray.500' : 'gray.400',
textAlign: 'center',
})}
>

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import type { MasteryLevel } from '@/db/schema/player-skill-mastery'
import { css } from '../../../styled-system/css'
import type { StudentWithProgress } from './StudentSelector'
@@ -58,17 +59,23 @@ interface ProgressDashboardProps {
}
/**
* Mastery level badge colors
* Mastery level badge colors (dark mode aware)
*/
function getMasteryColor(level: MasteryLevel): { bg: string; text: string } {
function getMasteryColor(level: MasteryLevel, isDark: boolean): { bg: string; text: string } {
switch (level) {
case 'mastered':
return { bg: 'green.100', text: 'green.700' }
return isDark
? { bg: 'green.900', text: 'green.200' }
: { bg: 'green.100', text: 'green.700' }
case 'practicing':
return { bg: 'yellow.100', text: 'yellow.700' }
return isDark
? { bg: 'yellow.900', text: 'yellow.200' }
: { bg: 'yellow.100', text: 'yellow.700' }
default:
// 'learning' and any unknown values use gray
return { bg: 'gray.100', text: 'gray.600' }
return isDark
? { bg: 'gray.700', text: 'gray.300' }
: { bg: 'gray.100', text: 'gray.600' }
}
}
@@ -96,6 +103,9 @@ export function ProgressDashboard({
onClearReinforcement,
onClearAllReinforcement,
}: ProgressDashboardProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const progressPercent =
currentPhase.totalSkills > 0
? Math.round((currentPhase.masteredSkills / currentPhase.totalSkills) * 100)
@@ -141,7 +151,7 @@ export function ProgressDashboard({
className={css({
fontSize: '1.75rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
Hi {student.name}!
@@ -151,7 +161,7 @@ export function ProgressDashboard({
onClick={onChangeStudent}
className={css({
fontSize: '0.875rem',
color: 'blue.500',
color: isDark ? 'blue.400' : 'blue.500',
background: 'none',
border: 'none',
cursor: 'pointer',
@@ -173,10 +183,10 @@ export function ProgressDashboard({
width: '100%',
padding: '1.5rem',
borderRadius: '12px',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
boxShadow: 'md',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<div
@@ -192,7 +202,7 @@ export function ProgressDashboard({
className={css({
fontSize: '1.25rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{currentPhase.levelName}
@@ -200,7 +210,7 @@ export function ProgressDashboard({
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
{currentPhase.phaseName}
@@ -210,7 +220,7 @@ export function ProgressDashboard({
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'blue.600',
color: isDark ? 'blue.400' : 'blue.600',
})}
>
{progressPercent}% mastered
@@ -222,7 +232,7 @@ export function ProgressDashboard({
className={css({
width: '100%',
height: '12px',
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.700' : 'gray.200',
borderRadius: '6px',
overflow: 'hidden',
marginBottom: '1rem',
@@ -231,7 +241,7 @@ export function ProgressDashboard({
<div
className={css({
height: '100%',
backgroundColor: 'green.500',
backgroundColor: isDark ? 'green.400' : 'green.500',
borderRadius: '6px',
transition: 'width 0.5s ease',
})}
@@ -242,7 +252,7 @@ export function ProgressDashboard({
<p
className={css({
fontSize: '0.875rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
{currentPhase.description}
@@ -294,14 +304,14 @@ export function ProgressDashboard({
flex: 1,
padding: '0.75rem',
fontSize: '1rem',
color: 'gray.700',
backgroundColor: 'gray.100',
color: isDark ? 'gray.200' : 'gray.700',
backgroundColor: isDark ? 'gray.700' : 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
_hover: {
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.600' : 'gray.200',
},
})}
>
@@ -316,14 +326,14 @@ export function ProgressDashboard({
flex: 1,
padding: '0.75rem',
fontSize: '1rem',
color: 'gray.700',
backgroundColor: 'gray.100',
color: isDark ? 'gray.200' : 'gray.700',
backgroundColor: isDark ? 'gray.700' : 'gray.100',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.2s ease',
_hover: {
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.600' : 'gray.200',
},
})}
>
@@ -339,10 +349,10 @@ export function ProgressDashboard({
className={css({
width: '100%',
padding: '1rem',
backgroundColor: 'orange.50',
backgroundColor: isDark ? 'orange.900' : 'orange.50',
borderRadius: '12px',
border: '1px solid',
borderColor: 'orange.200',
borderColor: isDark ? 'orange.700' : 'orange.200',
})}
>
<div
@@ -357,7 +367,7 @@ export function ProgressDashboard({
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'orange.700',
color: isDark ? 'orange.200' : 'orange.700',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
@@ -373,12 +383,12 @@ export function ProgressDashboard({
onClick={onClearAllReinforcement}
className={css({
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
background: 'none',
border: 'none',
cursor: 'pointer',
_hover: {
color: 'gray.700',
color: isDark ? 'gray.200' : 'gray.700',
textDecoration: 'underline',
},
})}
@@ -390,7 +400,7 @@ export function ProgressDashboard({
<p
className={css({
fontSize: '0.75rem',
color: 'orange.600',
color: isDark ? 'orange.300' : 'orange.600',
marginBottom: '0.75rem',
})}
>
@@ -411,17 +421,17 @@ export function ProgressDashboard({
justifyContent: 'space-between',
alignItems: 'center',
padding: '0.5rem 0.75rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '6px',
border: '1px solid',
borderColor: 'orange.100',
borderColor: isDark ? 'orange.800' : 'orange.100',
})}
>
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
<span
className={css({
fontSize: '0.875rem',
color: 'gray.700',
color: isDark ? 'gray.200' : 'gray.700',
fontWeight: 'medium',
})}
>
@@ -431,7 +441,7 @@ export function ProgressDashboard({
<span
className={css({
fontSize: '0.75rem',
color: 'green.600',
color: isDark ? 'green.400' : 'green.600',
})}
title={`${skill.reinforcementStreak} correct answers toward clearing`}
>
@@ -446,13 +456,13 @@ export function ProgressDashboard({
onClick={() => onClearReinforcement(skill.skillId)}
className={css({
fontSize: '0.75rem',
color: 'gray.400',
color: isDark ? 'gray.500' : 'gray.400',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
_hover: {
color: 'gray.600',
color: isDark ? 'gray.300' : 'gray.600',
},
})}
title="Mark as mastered (teacher only)"
@@ -473,17 +483,17 @@ export function ProgressDashboard({
className={css({
width: '100%',
padding: '1rem',
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
borderRadius: '12px',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<h3
className={css({
fontSize: '0.875rem',
fontWeight: 'semibold',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
marginBottom: '0.75rem',
})}
>
@@ -504,16 +514,16 @@ export function ProgressDashboard({
className={css({
padding: '0.5rem 0.75rem',
fontSize: '0.875rem',
color: 'blue.700',
backgroundColor: 'blue.50',
color: isDark ? 'blue.300' : 'blue.700',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
borderRadius: '6px',
border: '1px solid',
borderColor: 'blue.200',
borderColor: isDark ? 'blue.700' : 'blue.200',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
backgroundColor: 'blue.100',
borderColor: 'blue.300',
backgroundColor: isDark ? 'blue.800' : 'blue.100',
borderColor: isDark ? 'blue.600' : 'blue.300',
},
})}
>
@@ -528,16 +538,16 @@ export function ProgressDashboard({
className={css({
padding: '0.5rem 0.75rem',
fontSize: '0.875rem',
color: 'purple.700',
backgroundColor: 'purple.50',
color: isDark ? 'purple.300' : 'purple.700',
backgroundColor: isDark ? 'purple.900' : 'purple.50',
borderRadius: '6px',
border: '1px solid',
borderColor: 'purple.200',
borderColor: isDark ? 'purple.700' : 'purple.200',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
backgroundColor: 'purple.100',
borderColor: 'purple.300',
backgroundColor: isDark ? 'purple.800' : 'purple.100',
borderColor: isDark ? 'purple.600' : 'purple.300',
},
})}
>
@@ -552,16 +562,16 @@ export function ProgressDashboard({
className={css({
padding: '0.5rem 0.75rem',
fontSize: '0.875rem',
color: 'green.700',
backgroundColor: 'green.50',
color: isDark ? 'green.300' : 'green.700',
backgroundColor: isDark ? 'green.900' : 'green.50',
borderRadius: '6px',
border: '1px solid',
borderColor: 'green.200',
borderColor: isDark ? 'green.700' : 'green.200',
cursor: 'pointer',
transition: 'all 0.2s ease',
_hover: {
backgroundColor: 'green.100',
borderColor: 'green.300',
backgroundColor: isDark ? 'green.800' : 'green.100',
borderColor: isDark ? 'green.600' : 'green.300',
},
})}
>
@@ -584,7 +594,7 @@ export function ProgressDashboard({
className={css({
fontSize: '1rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
marginBottom: '0.75rem',
})}
>
@@ -598,7 +608,7 @@ export function ProgressDashboard({
})}
>
{recentSkills.map((skill) => {
const colors = getMasteryColor(skill.masteryLevel)
const colors = getMasteryColor(skill.masteryLevel, isDark)
return (
<span
key={skill.skillId}

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
import { css } from '../../../styled-system/css'
@@ -35,6 +36,8 @@ export function SessionSummary({
onPracticeAgain,
onBackToDashboard,
}: SessionSummaryProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const results = plan.results as SlotResult[]
const totalProblems = results.length
const correctProblems = results.filter((r) => r.isCorrect).length
@@ -78,12 +81,30 @@ export function SessionSummary({
className={css({
textAlign: 'center',
padding: '1.5rem',
backgroundColor:
accuracy >= 0.8 ? 'green.50' : accuracy >= 0.6 ? 'yellow.50' : 'orange.50',
backgroundColor: isDark
? accuracy >= 0.8
? 'green.900'
: accuracy >= 0.6
? 'yellow.900'
: 'orange.900'
: accuracy >= 0.8
? 'green.50'
: accuracy >= 0.6
? 'yellow.50'
: 'orange.50',
borderRadius: '16px',
border: '2px solid',
borderColor:
accuracy >= 0.8 ? 'green.200' : accuracy >= 0.6 ? 'yellow.200' : 'orange.200',
borderColor: isDark
? accuracy >= 0.8
? 'green.700'
: accuracy >= 0.6
? 'yellow.700'
: 'orange.700'
: accuracy >= 0.8
? 'green.200'
: accuracy >= 0.6
? 'yellow.200'
: 'orange.200',
})}
>
<div
@@ -98,7 +119,7 @@ export function SessionSummary({
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
marginBottom: '0.25rem',
})}
>
@@ -107,7 +128,7 @@ export function SessionSummary({
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
{performanceMessage}
@@ -128,7 +149,7 @@ export function SessionSummary({
className={css({
textAlign: 'center',
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
boxShadow: 'sm',
})}
@@ -137,7 +158,17 @@ export function SessionSummary({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: accuracy >= 0.8 ? 'green.600' : accuracy >= 0.6 ? 'yellow.600' : 'orange.600',
color: isDark
? accuracy >= 0.8
? 'green.400'
: accuracy >= 0.6
? 'yellow.400'
: 'orange.400'
: accuracy >= 0.8
? 'green.600'
: accuracy >= 0.6
? 'yellow.600'
: 'orange.600',
})}
>
{Math.round(accuracy * 100)}%
@@ -145,7 +176,7 @@ export function SessionSummary({
<div
className={css({
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
Accuracy
@@ -157,7 +188,7 @@ export function SessionSummary({
className={css({
textAlign: 'center',
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
boxShadow: 'sm',
})}
@@ -166,7 +197,7 @@ export function SessionSummary({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'blue.600',
color: isDark ? 'blue.400' : 'blue.600',
})}
>
{correctProblems}/{totalProblems}
@@ -174,7 +205,7 @@ export function SessionSummary({
<div
className={css({
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
Correct
@@ -186,7 +217,7 @@ export function SessionSummary({
className={css({
textAlign: 'center',
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
boxShadow: 'sm',
})}
@@ -195,7 +226,7 @@ export function SessionSummary({
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: 'purple.600',
color: isDark ? 'purple.400' : 'purple.600',
})}
>
{Math.round(sessionDurationMinutes)}
@@ -203,7 +234,7 @@ export function SessionSummary({
<div
className={css({
fontSize: '0.75rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
Minutes
@@ -219,7 +250,7 @@ export function SessionSummary({
flexDirection: 'column',
gap: '0.75rem',
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
boxShadow: 'sm',
})}
@@ -228,7 +259,7 @@ export function SessionSummary({
className={css({
fontSize: '1rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
marginBottom: '0.5rem',
})}
>
@@ -242,8 +273,10 @@ export function SessionSummary({
fontSize: '0.875rem',
})}
>
<span className={css({ color: 'gray.600' })}>Average time per problem</span>
<span className={css({ fontWeight: 'bold', color: 'gray.800' })}>
<span className={css({ color: isDark ? 'gray.400' : 'gray.600' })}>
Average time per problem
</span>
<span className={css({ fontWeight: 'bold', color: isDark ? 'gray.200' : 'gray.800' })}>
{Math.round(avgTimeMs / 1000)}s
</span>
</div>
@@ -255,8 +288,10 @@ export function SessionSummary({
fontSize: '0.875rem',
})}
>
<span className={css({ color: 'gray.600' })}>On-screen abacus used</span>
<span className={css({ fontWeight: 'bold', color: 'gray.800' })}>
<span className={css({ color: isDark ? 'gray.400' : 'gray.600' })}>
On-screen abacus used
</span>
<span className={css({ fontWeight: 'bold', color: isDark ? 'gray.200' : 'gray.800' })}>
{abacusUsageCount} times ({Math.round(abacusUsagePercent)}%)
</span>
</div>
@@ -268,7 +303,7 @@ export function SessionSummary({
data-section="skill-breakdown"
className={css({
padding: '1rem',
backgroundColor: 'white',
backgroundColor: isDark ? 'gray.800' : 'white',
borderRadius: '12px',
boxShadow: 'sm',
})}
@@ -277,7 +312,7 @@ export function SessionSummary({
className={css({
fontSize: '1rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
marginBottom: '1rem',
})}
>
@@ -305,7 +340,7 @@ export function SessionSummary({
className={css({
flex: 1,
fontSize: '0.875rem',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
})}
>
{formatSkillName(skill.skillId)}
@@ -314,7 +349,7 @@ export function SessionSummary({
className={css({
width: '120px',
height: '8px',
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.700' : 'gray.200',
borderRadius: '4px',
overflow: 'hidden',
})}
@@ -322,8 +357,13 @@ export function SessionSummary({
<div
className={css({
height: '100%',
backgroundColor:
skill.accuracy >= 0.8
backgroundColor: isDark
? skill.accuracy >= 0.8
? 'green.400'
: skill.accuracy >= 0.6
? 'yellow.400'
: 'red.400'
: skill.accuracy >= 0.8
? 'green.500'
: skill.accuracy >= 0.6
? 'yellow.500'
@@ -338,8 +378,13 @@ export function SessionSummary({
className={css({
fontSize: '0.75rem',
fontWeight: 'bold',
color:
skill.accuracy >= 0.8
color: isDark
? skill.accuracy >= 0.8
? 'green.400'
: skill.accuracy >= 0.6
? 'yellow.400'
: 'red.400'
: skill.accuracy >= 0.8
? 'green.600'
: skill.accuracy >= 0.6
? 'yellow.600'
@@ -361,20 +406,20 @@ export function SessionSummary({
data-section="problem-review"
className={css({
padding: '1rem',
backgroundColor: 'gray.50',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
borderRadius: '12px',
border: '1px solid',
borderColor: 'gray.200',
borderColor: isDark ? 'gray.700' : 'gray.200',
})}
>
<summary
className={css({
fontSize: '0.875rem',
fontWeight: 'bold',
color: 'gray.700',
color: isDark ? 'gray.300' : 'gray.700',
cursor: 'pointer',
_hover: {
color: 'gray.900',
color: isDark ? 'gray.100' : 'gray.900',
},
})}
>
@@ -398,9 +443,16 @@ export function SessionSummary({
alignItems: 'center',
gap: '0.75rem',
padding: '0.5rem',
backgroundColor: result.isCorrect ? 'green.50' : 'red.50',
backgroundColor: isDark
? result.isCorrect
? 'green.900'
: 'red.900'
: result.isCorrect
? 'green.50'
: 'red.50',
borderRadius: '8px',
fontSize: '0.875rem',
color: isDark ? 'gray.200' : 'inherit',
})}
>
<span>{result.isCorrect ? '✓' : '✗'}</span>
@@ -415,7 +467,7 @@ export function SessionSummary({
{!result.isCorrect && (
<span
className={css({
color: 'red.600',
color: isDark ? 'red.400' : 'red.600',
textDecoration: 'line-through',
})}
>
@@ -465,13 +517,13 @@ export function SessionSummary({
className={css({
padding: '0.75rem',
fontSize: '1rem',
color: 'gray.600',
backgroundColor: 'gray.100',
color: isDark ? 'gray.300' : 'gray.600',
backgroundColor: isDark ? 'gray.700' : 'gray.100',
borderRadius: '12px',
border: 'none',
cursor: 'pointer',
_hover: {
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.600' : 'gray.200',
},
})}
>

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import type { Player } from '@/types/player'
import { css } from '../../../styled-system/css'
@@ -22,6 +23,8 @@ interface StudentCardProps {
* Individual student card showing avatar, name, and progress
*/
function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const levelLabel = student.currentLevel ? `Lv.${student.currentLevel}` : 'New'
return (
@@ -38,8 +41,8 @@ function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
padding: '1rem',
borderRadius: '12px',
border: isSelected ? '3px solid' : '2px solid',
borderColor: isSelected ? 'blue.500' : 'gray.200',
backgroundColor: isSelected ? 'blue.50' : 'white',
borderColor: isSelected ? 'blue.500' : isDark ? 'gray.600' : 'gray.200',
backgroundColor: isSelected ? (isDark ? 'blue.900' : 'blue.50') : isDark ? 'gray.800' : 'white',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '100px',
@@ -71,7 +74,7 @@ function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
className={css({
fontWeight: 'bold',
fontSize: '1rem',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{student.name}
@@ -83,8 +86,8 @@ function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
fontSize: '0.75rem',
padding: '0.125rem 0.5rem',
borderRadius: '9999px',
backgroundColor: 'gray.100',
color: 'gray.600',
backgroundColor: isDark ? 'gray.700' : 'gray.100',
color: isDark ? 'gray.300' : 'gray.600',
})}
>
{levelLabel}
@@ -96,7 +99,7 @@ function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
className={css({
width: '100%',
height: '4px',
backgroundColor: 'gray.200',
backgroundColor: isDark ? 'gray.700' : 'gray.200',
borderRadius: '2px',
overflow: 'hidden',
})}
@@ -104,7 +107,7 @@ function StudentCard({ student, isSelected, onSelect }: StudentCardProps) {
<div
className={css({
height: '100%',
backgroundColor: 'green.500',
backgroundColor: isDark ? 'green.400' : 'green.500',
transition: 'width 0.3s ease',
})}
style={{ width: `${student.masteryPercent}%` }}
@@ -123,6 +126,9 @@ interface AddStudentButtonProps {
* Button to add a new student
*/
function AddStudentButton({ onClick }: AddStudentButtonProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
return (
<button
type="button"
@@ -137,22 +143,22 @@ function AddStudentButton({ onClick }: AddStudentButtonProps) {
padding: '1rem',
borderRadius: '12px',
border: '2px dashed',
borderColor: 'gray.300',
backgroundColor: 'gray.50',
borderColor: isDark ? 'gray.600' : 'gray.300',
backgroundColor: isDark ? 'gray.800' : 'gray.50',
cursor: 'pointer',
transition: 'all 0.2s ease',
minWidth: '100px',
minHeight: '140px',
_hover: {
borderColor: 'blue.400',
backgroundColor: 'blue.50',
backgroundColor: isDark ? 'blue.900' : 'blue.50',
},
})}
>
<span
className={css({
fontSize: '2rem',
color: 'gray.400',
color: isDark ? 'gray.500' : 'gray.400',
})}
>
@@ -160,7 +166,7 @@ function AddStudentButton({ onClick }: AddStudentButtonProps) {
<span
className={css({
fontSize: '0.875rem',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
Add New
@@ -191,6 +197,9 @@ export function StudentSelector({
onAddStudent,
title = 'Who is practicing today?',
}: StudentSelectorProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
return (
<div
data-component="student-selector"
@@ -207,7 +216,7 @@ export function StudentSelector({
className={css({
fontSize: '1.5rem',
fontWeight: 'bold',
color: 'gray.800',
color: isDark ? 'gray.100' : 'gray.800',
})}
>
{title}
@@ -245,11 +254,11 @@ export function StudentSelector({
<p
className={css({
fontSize: '1rem',
color: 'gray.600',
color: isDark ? 'gray.400' : 'gray.600',
marginBottom: '1rem',
})}
>
Selected: <strong>{selectedStudent.name}</strong> {selectedStudent.emoji}
Selected: <strong className={css({ color: isDark ? 'gray.100' : 'inherit' })}>{selectedStudent.name}</strong> {selectedStudent.emoji}
</p>
<button
type="button"

View File

@@ -1,5 +1,6 @@
'use client'
import { useTheme } from '@/contexts/ThemeContext'
import { css } from '../../../styled-system/css'
interface VerticalProblemProps {
@@ -40,6 +41,9 @@ export function VerticalProblem({
confirmedTermCount = 0,
currentHelpTermIndex,
}: VerticalProblemProps) {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
// Calculate max digits needed for alignment
const maxDigits = Math.max(
...terms.map((t) => Math.abs(t).toString().length),
@@ -70,19 +74,33 @@ export function VerticalProblem({
borderRadius: '8px',
backgroundColor: isCompleted
? isCorrect
? 'green.50'
: 'red.50'
? isDark
? 'green.900'
: 'green.50'
: isDark
? 'red.900'
: 'red.50'
: isFocused
? 'blue.50'
: 'gray.50',
? isDark
? 'blue.900'
: 'blue.50'
: isDark
? 'gray.800'
: 'gray.50',
border: '2px solid',
borderColor: isCompleted
? isCorrect
? 'green.400'
: 'red.400'
? isDark
? 'green.600'
: 'green.400'
: isDark
? 'red.600'
: 'red.400'
: isFocused
? 'blue.400'
: 'gray.200',
: isDark
? 'gray.600'
: 'gray.200',
transition: 'all 0.2s ease',
})}
>
@@ -109,7 +127,11 @@ export function VerticalProblem({
// Confirmed terms are dimmed with checkmark
opacity: isConfirmed ? 0.5 : 1,
// Current help term is highlighted
backgroundColor: isCurrentHelp ? 'purple.100' : 'transparent',
backgroundColor: isCurrentHelp
? isDark
? 'purple.800'
: 'purple.100'
: 'transparent',
borderRadius: isCurrentHelp ? '4px' : '0',
padding: isCurrentHelp ? '2px 4px' : '0',
marginLeft: isCurrentHelp ? '-4px' : '0',
@@ -123,7 +145,7 @@ export function VerticalProblem({
className={css({
position: 'absolute',
left: '-1.5rem',
color: 'green.500',
color: isDark ? 'green.400' : 'green.500',
fontSize: '0.875rem',
})}
>
@@ -138,7 +160,7 @@ export function VerticalProblem({
className={css({
position: 'absolute',
left: '-1.5rem',
color: 'purple.600',
color: isDark ? 'purple.300' : 'purple.600',
fontSize: '0.875rem',
})}
>
@@ -155,7 +177,7 @@ export function VerticalProblem({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: isNegative ? 'red.600' : 'transparent',
color: isNegative ? (isDark ? 'red.400' : 'red.600') : 'transparent',
})}
>
{isNegative ? '' : ''}
@@ -172,7 +194,13 @@ export function VerticalProblem({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: isCurrentHelp ? 'purple.800' : 'gray.800',
color: isCurrentHelp
? isDark
? 'purple.200'
: 'purple.800'
: isDark
? 'gray.200'
: 'gray.800',
fontWeight: isCurrentHelp ? 'bold' : 'inherit',
})}
>
@@ -189,7 +217,7 @@ export function VerticalProblem({
className={css({
width: '100%',
height: '2px',
backgroundColor: 'gray.400',
backgroundColor: isDark ? 'gray.600' : 'gray.400',
marginTop: '4px',
marginBottom: '4px',
})}
@@ -213,58 +241,80 @@ export function VerticalProblem({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'gray.500',
color: isDark ? 'gray.400' : 'gray.500',
})}
>
=
</div>
{/* Answer digit cells */}
{(isCompleted && isIncorrect ? correctAnswer?.toString() || '' : userAnswer)
.padStart(maxDigits, ' ')
.split('')
.map((digit, index) => (
<div
key={index}
data-element="answer-cell"
className={css({
width: cellWidth,
height: cellHeight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: isCompleted ? (isCorrect ? 'green.100' : 'red.100') : 'white',
borderRadius: '4px',
border: '1px solid',
borderColor: isCompleted ? (isCorrect ? 'green.300' : 'red.300') : 'gray.300',
color: isCompleted ? (isCorrect ? 'green.700' : 'red.700') : 'gray.800',
})}
>
{digit}
</div>
))}
{/* Answer digit cells - show maxDigits cells total */}
{Array(maxDigits)
.fill(null)
.map((_, index) => {
// Determine what to show in this cell
const displayValue =
isCompleted && isIncorrect ? correctAnswer?.toString() || '' : userAnswer
const paddedValue = displayValue.padStart(maxDigits, '')
const digit = paddedValue[index] || ''
const isEmpty = digit === ''
{/* Empty cells to fill remaining space */}
{!isCompleted &&
Array(Math.max(0, maxDigits - userAnswer.length))
.fill(null)
.map((_, index) => (
return (
<div
key={`empty-${index}`}
data-element="empty-cell"
key={index}
data-element={isEmpty ? 'empty-cell' : 'answer-cell'}
className={css({
width: cellWidth,
height: cellHeight,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
backgroundColor: isCompleted
? isCorrect
? isDark
? 'green.800'
: 'green.100'
: isDark
? 'red.800'
: 'red.100'
: isDark
? 'gray.700'
: 'white',
borderRadius: '4px',
border: '1px dashed',
borderColor: isFocused ? 'blue.300' : 'gray.300',
border: isEmpty && !isCompleted ? '1px dashed' : '1px solid',
borderColor: isCompleted
? isCorrect
? isDark
? 'green.600'
: 'green.300'
: isDark
? 'red.600'
: 'red.300'
: isEmpty
? isFocused
? 'blue.400'
: isDark
? 'gray.600'
: 'gray.300'
: isDark
? 'gray.600'
: 'gray.300',
color: isCompleted
? isCorrect
? isDark
? 'green.200'
: 'green.700'
: isDark
? 'red.200'
: 'red.700'
: isDark
? 'gray.200'
: 'gray.800',
})}
/>
))}
>
{digit}
</div>
)
})}
</div>
{/* Show user's incorrect answer below correct answer */}
@@ -273,7 +323,7 @@ export function VerticalProblem({
data-element="user-answer"
className={css({
fontSize: '0.875rem',
color: 'red.500',
color: isDark ? 'red.400' : 'red.500',
marginTop: '4px',
textDecoration: 'line-through',
})}