feat: create unified difficulty interface with 2-tab selector

Replace 3-way mode tabs (Smart/Manual/Mastery) with unified interface:
- Smart and Mastery are now "quick presets" via 2-tab selector
- Manual controls (thermometers) always visible for customization
- All modes share same displayRules system (1:1 correspondence)

Changes:
- Add DifficultyMethodSelector component with tab-style UI
- Update ConfigPanel to use 2-tab selector
- Smart/Mastery populate the always-visible display controls
- Preserve displayRules when switching between methods

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-10 18:12:18 -06:00
parent 995966ffbc
commit 0b7382f1b6
2 changed files with 128 additions and 65 deletions

View File

@ -3,14 +3,13 @@
import { stack } from '../../../../../../styled-system/patterns' import { stack } from '../../../../../../styled-system/patterns'
import type { WorksheetFormState } from '../types' import type { WorksheetFormState } from '../types'
import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas' import { defaultAdditionConfig } from '@/app/create/worksheets/config-schemas'
import { ModeSelector } from './ModeSelector' import { DifficultyMethodSelector } from './DifficultyMethodSelector'
import { StudentNameInput } from './config-panel/StudentNameInput' import { StudentNameInput } from './config-panel/StudentNameInput'
import { DigitRangeSection } from './config-panel/DigitRangeSection'
import { OperatorSection } from './config-panel/OperatorSection' import { OperatorSection } from './config-panel/OperatorSection'
import { ProgressiveDifficultyToggle } from './config-panel/ProgressiveDifficultyToggle' import { ProgressiveDifficultyToggle } from './config-panel/ProgressiveDifficultyToggle'
import { SmartModeControls } from './config-panel/SmartModeControls' import { SmartModeControls } from './config-panel/SmartModeControls'
import { ManualModeControls } from './config-panel/ManualModeControls'
import { MasteryModePanel } from './config-panel/MasteryModePanel' import { MasteryModePanel } from './config-panel/MasteryModePanel'
import { DisplayControlsPanel } from './DisplayControlsPanel'
interface ConfigPanelProps { interface ConfigPanelProps {
formState: WorksheetFormState formState: WorksheetFormState
@ -19,55 +18,23 @@ interface ConfigPanelProps {
} }
export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanelProps) { export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanelProps) {
// Handler for mode switching // Handler for difficulty method switching (smart vs mastery)
const handleModeChange = (newMode: 'smart' | 'manual' | 'mastery') => { const handleMethodChange = (newMethod: 'smart' | 'mastery') => {
if (formState.mode === newMode) { const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
if (currentMethod === newMethod) {
return // No change needed return // No change needed
} }
if (newMode === 'smart') { // Preserve displayRules when switching
// Switching to Smart mode const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
// Use current displayRules if available, otherwise default to earlyLearner
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules if (newMethod === 'smart') {
onChange({ onChange({
mode: 'smart', mode: 'smart',
displayRules, displayRules,
difficultyProfile: 'earlyLearner', difficultyProfile: 'earlyLearner',
} as unknown as Partial<WorksheetFormState>) } as unknown as Partial<WorksheetFormState>)
} else if (newMode === 'manual') {
// Switching to Manual mode
// Convert current displayRules to boolean flags if available
let booleanFlags = {
showCarryBoxes: true,
showAnswerBoxes: true,
showPlaceValueColors: true,
showTenFrames: false,
showProblemNumbers: true,
showCellBorder: true,
showTenFramesForAll: false,
}
if (formState.displayRules) {
// Convert 'always' to true, everything else to false
booleanFlags = {
showCarryBoxes: formState.displayRules.carryBoxes === 'always',
showAnswerBoxes: formState.displayRules.answerBoxes === 'always',
showPlaceValueColors: formState.displayRules.placeValueColors === 'always',
showTenFrames: formState.displayRules.tenFrames === 'always',
showProblemNumbers: formState.displayRules.problemNumbers === 'always',
showCellBorder: formState.displayRules.cellBorders === 'always',
showTenFramesForAll: false,
}
}
onChange({
mode: 'manual',
...booleanFlags,
} as unknown as Partial<WorksheetFormState>)
} else { } else {
// Switching to Mastery mode
// Mastery mode uses Smart mode under the hood with skill-based configuration
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
onChange({ onChange({
mode: 'mastery', mode: 'mastery',
displayRules, displayRules,
@ -75,6 +42,9 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
} }
} }
// Determine current method for selector
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
return ( return (
<div data-component="config-panel" className={stack({ gap: '3' })}> <div data-component="config-panel" className={stack({ gap: '3' })}>
{/* Student Name */} {/* Student Name */}
@ -84,13 +54,6 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
isDark={isDark} isDark={isDark}
/> />
{/* Digit Range Selector */}
<DigitRangeSection
digitRange={formState.digitRange}
onChange={(digitRange) => onChange({ digitRange })}
isDark={isDark}
/>
{/* Operator Selector */} {/* Operator Selector */}
<OperatorSection <OperatorSection
operator={formState.operator} operator={formState.operator}
@ -98,33 +61,29 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
isDark={isDark} isDark={isDark}
/> />
{/* Progressive Difficulty Toggle - Available for all modes */} {/* Progressive Difficulty Toggle */}
<ProgressiveDifficultyToggle <ProgressiveDifficultyToggle
interpolate={formState.interpolate} interpolate={formState.interpolate}
onChange={(interpolate) => onChange({ interpolate })} onChange={(interpolate) => onChange({ interpolate })}
isDark={isDark} isDark={isDark}
/> />
{/* Mode Selector Tabs with description */} {/* Display Controls - Always visible for manual adjustment */}
<ModeSelector <DisplayControlsPanel formState={formState} onChange={onChange} isDark={isDark} />
currentMode={formState.mode ?? 'smart'}
onChange={handleModeChange} {/* Difficulty Method Selector (Smart vs Mastery) */}
<DifficultyMethodSelector
currentMethod={currentMethod}
onChange={handleMethodChange}
isDark={isDark} isDark={isDark}
/> />
{/* Mode-specific controls - no wrapper, let controls style themselves */} {/* Method-specific preset controls */}
{/* Smart Mode Controls */} {currentMethod === 'smart' && (
{(!formState.mode || formState.mode === 'smart') && (
<SmartModeControls formState={formState} onChange={onChange} isDark={isDark} /> <SmartModeControls formState={formState} onChange={onChange} isDark={isDark} />
)} )}
{/* Manual Mode Controls */} {currentMethod === 'mastery' && (
{formState.mode === 'manual' && (
<ManualModeControls formState={formState} onChange={onChange} isDark={isDark} />
)}
{/* Mastery Mode Controls */}
{formState.mode === 'mastery' && (
<MasteryModePanel formState={formState} onChange={onChange} isDark={isDark} /> <MasteryModePanel formState={formState} onChange={onChange} isDark={isDark} />
)} )}
</div> </div>

View File

@ -0,0 +1,104 @@
'use client'
import { css } from '../../../../../../styled-system/css'
interface DifficultyMethodSelectorProps {
currentMethod: 'smart' | 'mastery'
onChange: (method: 'smart' | 'mastery') => void
isDark?: boolean
}
export function DifficultyMethodSelector({
currentMethod,
onChange,
isDark = false,
}: DifficultyMethodSelectorProps) {
return (
<div data-component="difficulty-method-selector">
{/* Tab buttons */}
<div
className={css({
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '0',
bg: isDark ? 'gray.800' : 'gray.100',
p: '1',
rounded: 'lg',
})}
>
{/* Smart Difficulty Tab */}
<button
type="button"
data-action="select-smart"
onClick={() => onChange('smart')}
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '2',
px: '4',
py: '2.5',
bg: currentMethod === 'smart' ? (isDark ? 'gray.700' : 'white') : 'transparent',
color: currentMethod === 'smart' ? (isDark ? 'gray.100' : 'gray.900') : (isDark ? 'gray.400' : 'gray.600'),
fontWeight: currentMethod === 'smart' ? 'semibold' : 'medium',
fontSize: 'sm',
rounded: 'md',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: currentMethod === 'smart' ? (isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)') : 'none',
_hover: {
color: isDark ? 'gray.200' : 'gray.700',
},
})}
>
<span>🎯</span>
<span>Smart Difficulty</span>
</button>
{/* Mastery Progression Tab */}
<button
type="button"
data-action="select-mastery"
onClick={() => onChange('mastery')}
className={css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '2',
px: '4',
py: '2.5',
bg: currentMethod === 'mastery' ? (isDark ? 'gray.700' : 'white') : 'transparent',
color: currentMethod === 'mastery' ? (isDark ? 'gray.100' : 'gray.900') : (isDark ? 'gray.400' : 'gray.600'),
fontWeight: currentMethod === 'mastery' ? 'semibold' : 'medium',
fontSize: 'sm',
rounded: 'md',
cursor: 'pointer',
transition: 'all 0.2s',
boxShadow: currentMethod === 'mastery' ? (isDark ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.1)') : 'none',
_hover: {
color: isDark ? 'gray.200' : 'gray.700',
},
})}
>
<span>🎓</span>
<span>Mastery Progression</span>
</button>
</div>
{/* Description text */}
<div
className={css({
mt: '2',
px: '1',
fontSize: 'xs',
color: isDark ? 'gray.400' : 'gray.600',
textAlign: 'center',
})}
>
{currentMethod === 'smart'
? 'Choose a difficulty preset, then customize display options below'
: 'Follow a structured skill progression with recommended scaffolding'}
</div>
</div>
)
}