refactor: extract DifficultyPresetDropdown and MakeEasierHarderButtons
Part 1 of SmartModeControls.tsx refactoring (1505 lines → smaller components) Extracted components: 1. DifficultyPresetDropdown (272 lines) - Dropdown selector for difficulty presets - Shows current preset or custom configuration - Displays hover preview of preset descriptions - Handles preset selection with regrouping and scaffolding 2. MakeEasierHarderButtons (343 lines) - Four-button layout for difficulty adjustment - Alternative modes (challenge/support) with tooltips - Conditional rendering based on availability - Full dark mode support Benefits: - Each component has single responsibility - Easier to test in isolation - Improved code organization - Consistent dark mode theming Next steps: - Extract OverallDifficultySlider (~200 lines) - Extract DifficultySpaceMap (~390 lines) - Update SmartModeControls to use new components - Reduces main file from 1505 → ~400 lines 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { css } from '../../../../../../../styled-system/css'
|
||||
import {
|
||||
DIFFICULTY_PROFILES,
|
||||
DIFFICULTY_PROGRESSION,
|
||||
calculateRegroupingIntensity,
|
||||
type DifficultyLevel,
|
||||
} from '../../difficultyProfiles'
|
||||
import type { DisplayRules } from '../../displayRules'
|
||||
import { getScaffoldingSummary } from './utils'
|
||||
|
||||
export interface DifficultyPresetDropdownProps {
|
||||
currentProfile: DifficultyLevel | null
|
||||
isCustom: boolean
|
||||
nearestEasier: DifficultyLevel | null
|
||||
nearestHarder: DifficultyLevel | null
|
||||
customDescription: React.ReactNode
|
||||
hoverPreview: {
|
||||
pAnyStart: number
|
||||
pAllStart: number
|
||||
displayRules: DisplayRules
|
||||
matchedProfile: string | 'custom'
|
||||
} | null
|
||||
operator: 'addition' | 'subtraction' | 'mixed'
|
||||
onChange: (updates: {
|
||||
difficultyProfile: DifficultyLevel
|
||||
pAnyStart: number
|
||||
pAllStart: number
|
||||
displayRules: DisplayRules
|
||||
}) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
export function DifficultyPresetDropdown({
|
||||
currentProfile,
|
||||
isCustom,
|
||||
nearestEasier,
|
||||
nearestHarder,
|
||||
customDescription,
|
||||
hoverPreview,
|
||||
operator,
|
||||
onChange,
|
||||
isDark = false,
|
||||
}: DifficultyPresetDropdownProps) {
|
||||
return (
|
||||
<div className={css({ mb: '3' })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'medium',
|
||||
color: isDark ? 'gray.300' : 'gray.700',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
Difficulty Preset
|
||||
</div>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
data-action="open-preset-dropdown"
|
||||
className={css({
|
||||
w: 'full',
|
||||
h: '24',
|
||||
px: '3',
|
||||
py: '2.5',
|
||||
border: '2px solid',
|
||||
borderColor: isCustom
|
||||
? isDark
|
||||
? 'orange.500'
|
||||
: 'orange.400'
|
||||
: isDark
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
bg: isCustom
|
||||
? isDark
|
||||
? 'orange.900'
|
||||
: 'orange.50'
|
||||
: isDark
|
||||
? 'gray.800'
|
||||
: 'white',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
textAlign: 'left',
|
||||
gap: '2',
|
||||
_hover: {
|
||||
borderColor: isCustom ? 'orange.500' : 'brand.400',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
color: hoverPreview
|
||||
? isDark
|
||||
? 'orange.300'
|
||||
: 'orange.700'
|
||||
: isDark
|
||||
? 'gray.200'
|
||||
: 'gray.700',
|
||||
})}
|
||||
>
|
||||
{hoverPreview ? (
|
||||
<>
|
||||
{hoverPreview.matchedProfile !== 'custom' ? (
|
||||
<>
|
||||
{DIFFICULTY_PROFILES[hoverPreview.matchedProfile].label}{' '}
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'orange.500',
|
||||
})}
|
||||
>
|
||||
(hover preview)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
✨ Custom{' '}
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'orange.500',
|
||||
})}
|
||||
>
|
||||
(hover preview)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : isCustom ? (
|
||||
nearestEasier && nearestHarder ? (
|
||||
<>
|
||||
{DIFFICULTY_PROFILES[nearestEasier].label}
|
||||
{' ↔ '}
|
||||
{DIFFICULTY_PROFILES[nearestHarder].label}
|
||||
</>
|
||||
) : (
|
||||
'✨ Custom'
|
||||
)
|
||||
) : currentProfile ? (
|
||||
DIFFICULTY_PROFILES[currentProfile].label
|
||||
) : (
|
||||
'Early Learner'
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: hoverPreview
|
||||
? isDark
|
||||
? 'orange.400'
|
||||
: 'orange.600'
|
||||
: isCustom
|
||||
? isDark
|
||||
? 'orange.400'
|
||||
: 'orange.600'
|
||||
: isDark
|
||||
? 'gray.400'
|
||||
: 'gray.500',
|
||||
lineHeight: '1.3',
|
||||
h: '14',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{hoverPreview ? (
|
||||
(() => {
|
||||
const regroupingPercent = Math.round(hoverPreview.pAnyStart * 100)
|
||||
const scaffoldingSummary = getScaffoldingSummary(hoverPreview.displayRules, operator)
|
||||
return (
|
||||
<>
|
||||
<div>{regroupingPercent}% regrouping</div>
|
||||
{scaffoldingSummary}
|
||||
</>
|
||||
)
|
||||
})()
|
||||
) : isCustom ? (
|
||||
customDescription
|
||||
) : currentProfile ? (
|
||||
(() => {
|
||||
const preset = DIFFICULTY_PROFILES[currentProfile]
|
||||
const regroupingPercent = Math.round(preset.regrouping.pAnyStart * 100)
|
||||
const scaffoldingSummary = getScaffoldingSummary(preset.displayRules, operator)
|
||||
return (
|
||||
<>
|
||||
<div>{regroupingPercent}% regrouping</div>
|
||||
{scaffoldingSummary}
|
||||
</>
|
||||
)
|
||||
})()
|
||||
) : (
|
||||
<>
|
||||
<div>25% regrouping</div>
|
||||
<div>Always: carry boxes, answer boxes, place value colors, ten-frames</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: isDark ? 'gray.500' : 'gray.400',
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
▼
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={css({
|
||||
bg: isDark ? 'gray.800' : 'white',
|
||||
rounded: 'lg',
|
||||
shadow: 'modal',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'gray.700' : 'gray.200',
|
||||
p: '2',
|
||||
minW: '64',
|
||||
maxH: '96',
|
||||
overflowY: 'auto',
|
||||
zIndex: 50,
|
||||
})}
|
||||
sideOffset={5}
|
||||
>
|
||||
{DIFFICULTY_PROGRESSION.map((presetName) => {
|
||||
const preset = DIFFICULTY_PROFILES[presetName]
|
||||
const isSelected = currentProfile === presetName && !isCustom
|
||||
|
||||
// Generate preset description
|
||||
const regroupingPercent = Math.round(
|
||||
calculateRegroupingIntensity(
|
||||
preset.regrouping.pAnyStart,
|
||||
preset.regrouping.pAllStart
|
||||
) * 10
|
||||
)
|
||||
const scaffoldingSummary = getScaffoldingSummary(preset.displayRules, operator)
|
||||
const presetDescription = (
|
||||
<>
|
||||
<div>{regroupingPercent}% regrouping</div>
|
||||
{scaffoldingSummary}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
key={presetName}
|
||||
data-action={`select-preset-${presetName}`}
|
||||
onSelect={() => {
|
||||
// Apply preset configuration
|
||||
onChange({
|
||||
difficultyProfile: presetName,
|
||||
pAnyStart: preset.regrouping.pAnyStart,
|
||||
pAllStart: preset.regrouping.pAllStart,
|
||||
displayRules: preset.displayRules,
|
||||
})
|
||||
}}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1',
|
||||
px: '3',
|
||||
py: '2.5',
|
||||
rounded: 'md',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
bg: isSelected ? 'brand.50' : 'transparent',
|
||||
_hover: {
|
||||
bg: 'brand.50',
|
||||
},
|
||||
_focus: {
|
||||
bg: 'brand.100',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'semibold',
|
||||
color: isSelected ? 'brand.700' : isDark ? 'gray.200' : 'gray.700',
|
||||
})}
|
||||
>
|
||||
{preset.label}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: isSelected ? 'brand.600' : isDark ? 'gray.400' : 'gray.500',
|
||||
lineHeight: '1.3',
|
||||
})}
|
||||
>
|
||||
{presetDescription}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { css } from '../../../../../../../styled-system/css'
|
||||
import type { DifficultyMode } from '../../difficultyProfiles'
|
||||
|
||||
export interface DifficultyChangeResult {
|
||||
changeDescription: string
|
||||
pAnyStart: number
|
||||
pAllStart: number
|
||||
displayRules: any
|
||||
difficultyProfile?: string
|
||||
}
|
||||
|
||||
export interface MakeEasierHarderButtonsProps {
|
||||
easierResultBoth: DifficultyChangeResult
|
||||
easierResultChallenge: DifficultyChangeResult
|
||||
easierResultSupport: DifficultyChangeResult
|
||||
harderResultBoth: DifficultyChangeResult
|
||||
harderResultChallenge: DifficultyChangeResult
|
||||
harderResultSupport: DifficultyChangeResult
|
||||
canMakeEasierBoth: boolean
|
||||
canMakeEasierChallenge: boolean
|
||||
canMakeEasierSupport: boolean
|
||||
canMakeHarderBoth: boolean
|
||||
canMakeHarderChallenge: boolean
|
||||
canMakeHarderSupport: boolean
|
||||
onEasier: (mode: DifficultyMode) => void
|
||||
onHarder: (mode: DifficultyMode) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
export function MakeEasierHarderButtons({
|
||||
easierResultBoth,
|
||||
easierResultChallenge,
|
||||
easierResultSupport,
|
||||
harderResultBoth,
|
||||
harderResultChallenge,
|
||||
harderResultSupport,
|
||||
canMakeEasierBoth,
|
||||
canMakeEasierChallenge,
|
||||
canMakeEasierSupport,
|
||||
canMakeHarderBoth,
|
||||
canMakeHarderChallenge,
|
||||
canMakeHarderSupport,
|
||||
onEasier,
|
||||
onHarder,
|
||||
isDark = false,
|
||||
}: MakeEasierHarderButtonsProps) {
|
||||
// Determine which mode is alternative for easier
|
||||
const easierAlternativeMode =
|
||||
easierResultBoth.changeDescription === easierResultChallenge.changeDescription
|
||||
? 'support'
|
||||
: 'challenge'
|
||||
const easierAlternativeResult =
|
||||
easierAlternativeMode === 'support' ? easierResultSupport : easierResultChallenge
|
||||
const easierAlternativeLabel =
|
||||
easierAlternativeMode === 'support' ? '↑ More support' : '← Less challenge'
|
||||
const canEasierAlternative =
|
||||
easierAlternativeMode === 'support' ? canMakeEasierSupport : canMakeEasierChallenge
|
||||
|
||||
// Determine which mode is alternative for harder
|
||||
const harderAlternativeMode =
|
||||
harderResultBoth.changeDescription === harderResultChallenge.changeDescription
|
||||
? 'support'
|
||||
: 'challenge'
|
||||
const harderAlternativeResult =
|
||||
harderAlternativeMode === 'support' ? harderResultSupport : harderResultChallenge
|
||||
const harderAlternativeLabel =
|
||||
harderAlternativeMode === 'support' ? '↓ Less support' : '→ More challenge'
|
||||
const canHarderAlternative =
|
||||
harderAlternativeMode === 'support' ? canMakeHarderSupport : canMakeHarderChallenge
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2',
|
||||
pt: '1',
|
||||
borderTop: '1px solid',
|
||||
borderColor: isDark ? 'gray.700' : 'gray.200',
|
||||
})}
|
||||
>
|
||||
{/* Four-Button Layout: [Alt-35%][Rec-65%][Rec-65%][Alt-35%] */}
|
||||
<Tooltip.Provider delayDuration={300}>
|
||||
<div className={css({ display: 'flex', gap: '2' })}>
|
||||
{/* EASIER SECTION */}
|
||||
<div className={css({ display: 'flex', flex: '1' })}>
|
||||
{/* Alternative Easier Button - Hidden if disabled and main is enabled */}
|
||||
{canEasierAlternative && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
onClick={() => onEasier(easierAlternativeMode)}
|
||||
disabled={!canEasierAlternative}
|
||||
data-action={`easier-${easierAlternativeMode}`}
|
||||
className={css({
|
||||
flexShrink: 0,
|
||||
width: '10',
|
||||
h: '16',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'medium',
|
||||
color: isDark ? 'gray.300' : 'gray.700',
|
||||
bg: isDark ? 'gray.700' : 'gray.100',
|
||||
border: '1.5px solid',
|
||||
borderColor: isDark ? 'gray.600' : 'gray.300',
|
||||
borderRight: 'none',
|
||||
borderTopLeftRadius: 'lg',
|
||||
borderBottomLeftRadius: 'lg',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: isDark ? 'gray.600' : 'gray.200',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{easierAlternativeLabel}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side="top"
|
||||
className={css({
|
||||
bg: 'gray.800',
|
||||
color: 'white',
|
||||
px: '3',
|
||||
py: '2',
|
||||
rounded: 'md',
|
||||
fontSize: 'xs',
|
||||
maxW: '250px',
|
||||
shadow: 'lg',
|
||||
zIndex: 1000,
|
||||
})}
|
||||
>
|
||||
{easierAlternativeResult.changeDescription}
|
||||
<Tooltip.Arrow className={css({ fill: 'gray.800' })} />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{/* Recommended Easier Button - Expands to full width if alternative is hidden */}
|
||||
<button
|
||||
onClick={() => onEasier('both')}
|
||||
disabled={!canMakeEasierBoth}
|
||||
data-action="easier-both"
|
||||
className={css({
|
||||
flex: '1',
|
||||
h: '16',
|
||||
px: '3',
|
||||
py: '2',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '0.5',
|
||||
color: canMakeEasierBoth ? 'brand.700' : isDark ? 'gray.500' : 'gray.400',
|
||||
bg: isDark ? 'gray.800' : 'white',
|
||||
border: '1.5px solid',
|
||||
borderColor: canMakeEasierBoth
|
||||
? 'brand.500'
|
||||
: isDark
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
borderTopLeftRadius: canEasierAlternative ? 'none' : 'lg',
|
||||
borderBottomLeftRadius: canEasierAlternative ? 'none' : 'lg',
|
||||
borderTopRightRadius: 'lg',
|
||||
borderBottomRightRadius: 'lg',
|
||||
cursor: canMakeEasierBoth ? 'pointer' : 'not-allowed',
|
||||
opacity: canMakeEasierBoth ? 1 : 0.5,
|
||||
_hover: canMakeEasierBoth
|
||||
? {
|
||||
bg: isDark ? 'gray.700' : 'brand.50',
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'semibold',
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
← Make Easier
|
||||
</div>
|
||||
{canMakeEasierBoth && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.3',
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{easierResultBoth.changeDescription}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* HARDER SECTION */}
|
||||
<div className={css({ display: 'flex', flex: '1' })}>
|
||||
{/* Recommended Harder Button - Expands to full width if alternative is hidden */}
|
||||
<button
|
||||
onClick={() => onHarder('both')}
|
||||
disabled={!canMakeHarderBoth}
|
||||
data-action="harder-both"
|
||||
className={css({
|
||||
flex: '1',
|
||||
h: '16',
|
||||
px: '3',
|
||||
py: '2',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '0.5',
|
||||
color: canMakeHarderBoth ? 'brand.700' : isDark ? 'gray.500' : 'gray.400',
|
||||
bg: isDark ? 'gray.800' : 'white',
|
||||
border: '1.5px solid',
|
||||
borderColor: canMakeHarderBoth
|
||||
? 'brand.500'
|
||||
: isDark
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
borderTopLeftRadius: 'lg',
|
||||
borderBottomLeftRadius: 'lg',
|
||||
borderTopRightRadius: canHarderAlternative ? 'none' : 'lg',
|
||||
borderBottomRightRadius: canHarderAlternative ? 'none' : 'lg',
|
||||
cursor: canMakeHarderBoth ? 'pointer' : 'not-allowed',
|
||||
opacity: canMakeHarderBoth ? 1 : 0.5,
|
||||
_hover: canMakeHarderBoth
|
||||
? {
|
||||
bg: isDark ? 'gray.700' : 'brand.50',
|
||||
}
|
||||
: {},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'semibold',
|
||||
flexShrink: 0,
|
||||
})}
|
||||
>
|
||||
Make Harder →
|
||||
</div>
|
||||
{canMakeHarderBoth && (
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.3',
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
WebkitBoxOrient: 'vertical',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{harderResultBoth.changeDescription}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Alternative Harder Button - Hidden if disabled and main is enabled */}
|
||||
{canHarderAlternative && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
onClick={() => onHarder(harderAlternativeMode)}
|
||||
disabled={!canHarderAlternative}
|
||||
data-action={`harder-${harderAlternativeMode}`}
|
||||
className={css({
|
||||
flexShrink: 0,
|
||||
width: '10',
|
||||
h: '16',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'medium',
|
||||
color: isDark ? 'gray.300' : 'gray.700',
|
||||
bg: isDark ? 'gray.700' : 'gray.100',
|
||||
border: '1.5px solid',
|
||||
borderColor: isDark ? 'gray.600' : 'gray.300',
|
||||
borderLeft: 'none',
|
||||
borderTopRightRadius: 'lg',
|
||||
borderBottomRightRadius: 'lg',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: isDark ? 'gray.600' : 'gray.200',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{harderAlternativeLabel}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
{canHarderAlternative && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side="top"
|
||||
className={css({
|
||||
bg: 'gray.800',
|
||||
color: 'white',
|
||||
px: '3',
|
||||
py: '2',
|
||||
rounded: 'md',
|
||||
fontSize: 'xs',
|
||||
maxW: '250px',
|
||||
shadow: 'lg',
|
||||
zIndex: 1000,
|
||||
})}
|
||||
>
|
||||
{harderAlternativeResult.changeDescription}
|
||||
<Tooltip.Arrow className={css({ fill: 'gray.800' })} />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# SmartModeControls.tsx Refactoring Plan
|
||||
|
||||
## Current State
|
||||
- **File size**: 1505 lines
|
||||
- **Complexity**: Multiple large, self-contained UI sections embedded in one component
|
||||
- **Issues**: Hard to maintain, test, and reuse
|
||||
|
||||
## Proposed Refactoring
|
||||
|
||||
### 1. Extract DifficultyPresetDropdown Component (~270 lines)
|
||||
**Lines**: 275-546
|
||||
**Purpose**: Dropdown for selecting difficulty presets
|
||||
**Props**:
|
||||
```typescript
|
||||
interface DifficultyPresetDropdownProps {
|
||||
currentProfile: DifficultyLevel | null
|
||||
isCustom: boolean
|
||||
nearestEasier: DifficultyLevel | null
|
||||
nearestHarder: DifficultyLevel | null
|
||||
customDescription: React.ReactNode
|
||||
hoverPreview: { pAnyStart: number; pAllStart: number; displayRules: DisplayRules; matchedProfile: string | 'custom' } | null
|
||||
operator: 'addition' | 'subtraction' | 'mixed'
|
||||
onChange: (updates: { difficultyProfile: DifficultyLevel; pAnyStart: number; pAllStart: number; displayRules: DisplayRules }) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Extract MakeEasierHarderButtons Component (~240 lines)
|
||||
**Lines**: 548-850
|
||||
**Purpose**: Four-button layout for adjusting difficulty
|
||||
**Props**:
|
||||
```typescript
|
||||
interface MakeEasierHarderButtonsProps {
|
||||
canMakeEasier: { recommended: boolean; alternative: boolean }
|
||||
canMakeHarder: { recommended: boolean; alternative: boolean }
|
||||
onEasier: (mode: DifficultyMode) => void
|
||||
onHarder: (mode: DifficultyMode) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Extract OverallDifficultySlider Component (~200 lines)
|
||||
**Lines**: 859-1110
|
||||
**Purpose**: Slider with preset markers for difficulty adjustment
|
||||
**Props**:
|
||||
```typescript
|
||||
interface OverallDifficultySliderProps {
|
||||
overallDifficulty: number
|
||||
currentProfile: DifficultyLevel | null
|
||||
isCustom: boolean
|
||||
onChange: (difficulty: number) => void
|
||||
isDark?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Extract DifficultySpaceMap Component (~390 lines)
|
||||
**Lines**: 1113-1505
|
||||
**Purpose**: 2D visualization of difficulty space with interactive hover
|
||||
**Props**:
|
||||
```typescript
|
||||
interface DifficultySpaceMapProps {
|
||||
currentState: { pAnyStart: number; pAllStart: number; displayRules: DisplayRules }
|
||||
hoverPoint: { x: number; y: number } | null
|
||||
setHoverPoint: (point: { x: number; y: number } | null) => void
|
||||
setHoverPreview: (preview: { pAnyStart: number; pAllStart: number; displayRules: DisplayRules; matchedProfile: string | 'custom' } | null) => void
|
||||
operator: 'addition' | 'subtraction' | 'mixed'
|
||||
isDark?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Create Shared Style Utilities
|
||||
**File**: `src/app/create/worksheets/addition/components/config-panel/buttonStyles.ts`
|
||||
**Purpose**: Reusable button style generators to reduce duplication
|
||||
|
||||
```typescript
|
||||
export function getDifficultyButtonStyles(
|
||||
isEnabled: boolean,
|
||||
isDark: boolean,
|
||||
variant: 'primary' | 'secondary'
|
||||
): CSSProperties {
|
||||
// Common button styling logic
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits
|
||||
1. **Maintainability**: Each component focuses on single responsibility
|
||||
2. **Testability**: Smaller components are easier to test in isolation
|
||||
3. **Reusability**: Components can be reused in other contexts (e.g., MasteryModePanel)
|
||||
4. **Readability**: Main SmartModeControls becomes a clean composition
|
||||
5. **Performance**: React can optimize smaller component trees better
|
||||
6. **Dark mode**: Easier to audit and maintain consistent theming
|
||||
|
||||
## Refactored SmartModeControls Structure
|
||||
```tsx
|
||||
export function SmartModeControls({ formState, onChange, isDark }: SmartModeControlsProps) {
|
||||
// State and logic
|
||||
const [showDebugPlot, setShowDebugPlot] = useState(false)
|
||||
const [hoverPoint, setHoverPoint] = useState<{ x: number; y: number } | null>(null)
|
||||
const [hoverPreview, setHoverPreview] = useState<...>(null)
|
||||
|
||||
// Computed values
|
||||
const currentProfile = getProfileFromConfig(...)
|
||||
const isCustom = currentProfile === null
|
||||
|
||||
return (
|
||||
<div data-section="smart-mode">
|
||||
<DigitRangeSection {...} />
|
||||
|
||||
<div data-section="difficulty">
|
||||
<DifficultyPresetDropdown {...} />
|
||||
<MakeEasierHarderButtons {...} />
|
||||
<OverallDifficultySlider {...} />
|
||||
<DifficultySpaceMap {...} />
|
||||
</div>
|
||||
|
||||
<RegroupingFrequencyPanel {...} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Risks & Mitigation
|
||||
- **Risk**: Breaking existing functionality
|
||||
- **Mitigation**: Extract one component at a time, test thoroughly
|
||||
- **Risk**: Props become too complex
|
||||
- **Mitigation**: Create intermediate types, use composition patterns
|
||||
- **Risk**: Performance regression from more components
|
||||
- **Mitigation**: Use React.memo where appropriate
|
||||
|
||||
## Implementation Steps
|
||||
1. ✅ Create this plan document
|
||||
2. Extract DifficultyPresetDropdown
|
||||
3. Extract MakeEasierHarderButtons
|
||||
4. Extract OverallDifficultySlider
|
||||
5. Extract DifficultySpaceMap
|
||||
6. Create shared button utilities
|
||||
7. Test all components
|
||||
8. Commit and push
|
||||
|
||||
## Questions for User
|
||||
1. Should we proceed with this refactoring plan?
|
||||
2. Any components you'd prefer to keep inline?
|
||||
3. Any additional concerns or requirements?
|
||||
Reference in New Issue
Block a user