feat: add responsive page button layout with dynamic dropdown
Implement fully responsive page selection that adapts to container width: **Responsive button display:** - ≤280px: 4 buttons (1, 2, 3, 4), dropdown has remaining options - ≤320px: 6 buttons (1, 2, 3, 4, 10, 25), dropdown has 50, 100 - >320px: All 8 options as buttons (1, 2, 3, 4, 10, 25, 50, 100) **Smart dropdown handling:** - Hidden when empty (all options fit as buttons) - Single remaining option rendered as button instead of dropdown - Dropdown label shows first option + "+" or selected value **Layout improvements:** - Uses ResizeObserver to track container width dynamically - Buttons spread across full width with space-between - Smart orientation switching minimizes total problem count changes - Total badge moved to layout tab button - Orientation buttons show calculated layouts for unselected orientation **Container queries:** - Replaced viewport media queries with CSS container queries - Panel responds to its own width (280px minimum), not viewport 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -42,11 +42,17 @@
|
||||
"mcp__sqlite__describe_table",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)"
|
||||
"Bash(git stash:*)",
|
||||
"Bash(npx @biomejs/biome:*)",
|
||||
"Bash(git rev-parse:*)",
|
||||
"Bash(gh run list:*)",
|
||||
"Bash(npx biome:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
},
|
||||
"enableAllProjectMcpServers": true,
|
||||
"enabledMcpjsonServers": ["sqlite"]
|
||||
"enabledMcpjsonServers": [
|
||||
"sqlite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,16 +10,12 @@
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
generateProblems,
|
||||
generateSubtractionProblems,
|
||||
} from '@/app/create/worksheets/problemGenerator'
|
||||
import type { WorksheetOperator } from '@/app/create/worksheets/types'
|
||||
import {
|
||||
generatePlaceValueColors,
|
||||
generateProblemStackFunction,
|
||||
generateSubtractionProblemStackFunction,
|
||||
generateTypstHelpers,
|
||||
generatePlaceValueColors,
|
||||
} from '@/app/create/worksheets/typstHelpers'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@@ -64,7 +60,7 @@ function generateExampleTypst(config: ExampleRequest): string {
|
||||
const showBorrowingHints = config.showBorrowingHints ?? false
|
||||
|
||||
if (operator === 'addition') {
|
||||
// Use custom addends if provided, otherwise generate a problem
|
||||
// Use custom addends if provided, otherwise use a 3-digit problem with multiple regroups
|
||||
let a: number
|
||||
let b: number
|
||||
|
||||
@@ -72,11 +68,10 @@ function generateExampleTypst(config: ExampleRequest): string {
|
||||
a = config.addend1
|
||||
b = config.addend2
|
||||
} else {
|
||||
// Generate a simple 2-digit + 2-digit problem with carries
|
||||
const problems = generateProblems(1, 0.8, 0.5, false, 12345)
|
||||
const problem = problems[0]
|
||||
a = problem.a
|
||||
b = problem.b
|
||||
// Use a 3-digit problem with multiple regroups to demonstrate all scaffolding features
|
||||
// 456 + 789 = 1245 (3 digits, 3 regroups: ones, tens, hundreds)
|
||||
a = 456
|
||||
b = 789
|
||||
}
|
||||
|
||||
return String.raw`
|
||||
@@ -113,12 +108,10 @@ ${generateProblemStackFunction(cellSize, 3)}
|
||||
minuend = config.minuend
|
||||
subtrahend = config.subtrahend
|
||||
} else {
|
||||
// Generate a simple 2-digit - 2-digit problem with borrows
|
||||
const digitRange = { min: 2, max: 2 }
|
||||
const problems = generateSubtractionProblems(1, digitRange, 0.8, 0.5, false, 12345)
|
||||
const problem = problems[0]
|
||||
minuend = problem.minuend
|
||||
subtrahend = problem.subtrahend
|
||||
// Use a 3-digit problem with multiple borrows to demonstrate all scaffolding features
|
||||
// 832 - 456 = 376 (3 digits, 2 borrows: ones and tens)
|
||||
minuend = 832
|
||||
subtrahend = 456
|
||||
}
|
||||
|
||||
return String.raw`
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('Ten-frames rendering', () => {
|
||||
it('should pass showTenFrames: true to Typst template for regrouping problems', () => {
|
||||
const config: WorksheetConfig = {
|
||||
version: 4,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
problemsPerPage: 4,
|
||||
cols: 2,
|
||||
pages: 1,
|
||||
@@ -149,7 +149,7 @@ describe('Ten-frames rendering', () => {
|
||||
it('should include ten-frames rendering code when showTenFrames: true', () => {
|
||||
const config: WorksheetConfig = {
|
||||
version: 4,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
problemsPerPage: 2,
|
||||
cols: 1,
|
||||
pages: 1,
|
||||
|
||||
@@ -267,7 +267,7 @@ export const SmartModeEarlyLearner: Story = {
|
||||
render: () => (
|
||||
<FullWorksheetGenerator
|
||||
initialState={{
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
difficultyProfile: 'earlyLearner',
|
||||
pages: 3,
|
||||
}}
|
||||
@@ -279,7 +279,7 @@ export const SmartModeAdvanced: Story = {
|
||||
render: () => (
|
||||
<FullWorksheetGenerator
|
||||
initialState={{
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
difficultyProfile: 'advanced',
|
||||
pages: 5,
|
||||
}}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DifficultyMethodSelector } from './DifficultyMethodSelector'
|
||||
import { StudentNameInput } from './config-panel/StudentNameInput'
|
||||
import { OperatorSection } from './config-panel/OperatorSection'
|
||||
import { ProgressiveDifficultyToggle } from './config-panel/ProgressiveDifficultyToggle'
|
||||
import { SmartModeControls } from './config-panel/SmartModeControls'
|
||||
import { CustomModeControls } from './config-panel/CustomModeControls'
|
||||
import { MasteryModePanel } from './config-panel/MasteryModePanel'
|
||||
import { DisplayControlsPanel } from './DisplayControlsPanel'
|
||||
import { validateProblemSpace } from '@/app/create/worksheets/utils/validateProblemSpace'
|
||||
@@ -56,8 +56,8 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
|
||||
])
|
||||
|
||||
// Handler for difficulty method switching (smart vs mastery)
|
||||
const handleMethodChange = (newMethod: 'smart' | 'mastery') => {
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
|
||||
const handleMethodChange = (newMethod: 'custom' | 'mastery') => {
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'custom'
|
||||
if (currentMethod === newMethod) {
|
||||
return // No change needed
|
||||
}
|
||||
@@ -65,9 +65,9 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
|
||||
// Preserve displayRules when switching
|
||||
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
|
||||
|
||||
if (newMethod === 'smart') {
|
||||
if (newMethod === 'custom') {
|
||||
onChange({
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
displayRules,
|
||||
difficultyProfile: 'earlyLearner',
|
||||
} as unknown as Partial<WorksheetFormState>)
|
||||
@@ -80,7 +80,7 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
|
||||
}
|
||||
|
||||
// Determine current method for selector
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'custom'
|
||||
|
||||
return (
|
||||
<WorksheetConfigProvider formState={formState} updateFormState={onChange}>
|
||||
@@ -158,8 +158,8 @@ export function ConfigPanel({ formState, onChange, isDark = false }: ConfigPanel
|
||||
/>
|
||||
|
||||
{/* Method-specific preset controls */}
|
||||
{currentMethod === 'smart' && (
|
||||
<SmartModeControls formState={formState} onChange={onChange} />
|
||||
{currentMethod === 'custom' && (
|
||||
<CustomModeControls formState={formState} onChange={onChange} />
|
||||
)}
|
||||
|
||||
{currentMethod === 'mastery' && (
|
||||
|
||||
@@ -103,7 +103,7 @@ export const SmartMode: Story = {
|
||||
render: () => (
|
||||
<SidebarWrapper
|
||||
initialState={{
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
difficultyProfile: 'earlyLearner',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { css } from '@styled/css'
|
||||
|
||||
interface DifficultyMethodSelectorProps {
|
||||
currentMethod: 'smart' | 'mastery'
|
||||
onChange: (method: 'smart' | 'mastery') => void
|
||||
currentMethod: 'custom' | 'mastery'
|
||||
onChange: (method: 'custom' | 'mastery') => void
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ export function DifficultyMethodSelector({
|
||||
borderColor: isDark ? 'gray.700' : 'gray.200',
|
||||
})}
|
||||
>
|
||||
{/* Smart Difficulty Tab */}
|
||||
{/* Custom Difficulty Tab */}
|
||||
<button
|
||||
type="button"
|
||||
data-action="select-smart"
|
||||
onClick={() => onChange('smart')}
|
||||
onClick={() => onChange('custom')}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -37,16 +37,16 @@ export function DifficultyMethodSelector({
|
||||
px: '5',
|
||||
py: '3',
|
||||
flex: '1',
|
||||
bg: currentMethod === 'smart' ? (isDark ? 'gray.800' : 'white') : 'transparent',
|
||||
bg: currentMethod === 'custom' ? (isDark ? 'gray.800' : 'white') : 'transparent',
|
||||
color:
|
||||
currentMethod === 'smart'
|
||||
currentMethod === 'custom'
|
||||
? isDark
|
||||
? 'brand.300'
|
||||
: 'brand.600'
|
||||
: isDark
|
||||
? 'gray.500'
|
||||
: 'gray.500',
|
||||
fontWeight: currentMethod === 'smart' ? 'bold' : 'medium',
|
||||
fontWeight: currentMethod === 'custom' ? 'bold' : 'medium',
|
||||
fontSize: 'sm',
|
||||
borderTopLeftRadius: 'lg',
|
||||
borderTopRightRadius: 'lg',
|
||||
@@ -54,11 +54,11 @@ export function DifficultyMethodSelector({
|
||||
transition: 'all 0.2s',
|
||||
borderBottom: '3px solid',
|
||||
borderColor:
|
||||
currentMethod === 'smart' ? (isDark ? 'brand.500' : 'brand.500') : 'transparent',
|
||||
currentMethod === 'custom' ? (isDark ? 'brand.500' : 'brand.500') : 'transparent',
|
||||
mb: '-2px',
|
||||
_hover: {
|
||||
color:
|
||||
currentMethod === 'smart'
|
||||
currentMethod === 'custom'
|
||||
? isDark
|
||||
? 'brand.200'
|
||||
: 'brand.700'
|
||||
@@ -66,7 +66,7 @@ export function DifficultyMethodSelector({
|
||||
? 'gray.400'
|
||||
: 'gray.600',
|
||||
bg:
|
||||
currentMethod === 'smart'
|
||||
currentMethod === 'custom'
|
||||
? isDark
|
||||
? 'gray.800'
|
||||
: 'white'
|
||||
@@ -77,7 +77,7 @@ export function DifficultyMethodSelector({
|
||||
})}
|
||||
>
|
||||
<span>🎯</span>
|
||||
<span>Smart Difficulty</span>
|
||||
<span>Custom Difficulty</span>
|
||||
</button>
|
||||
|
||||
{/* Mastery Progression Tab */}
|
||||
@@ -147,7 +147,7 @@ export function DifficultyMethodSelector({
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{currentMethod === 'smart'
|
||||
{currentMethod === 'custom'
|
||||
? 'Choose a difficulty preset, then customize display options below'
|
||||
: 'Follow a structured skill progression with recommended scaffolding'}
|
||||
</div>
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
import { css } from '@styled/css'
|
||||
|
||||
interface ModeSelectorProps {
|
||||
currentMode: 'smart' | 'manual' | 'mastery'
|
||||
onChange: (mode: 'smart' | 'manual' | 'mastery') => void
|
||||
currentMode: 'custom' | 'manual' | 'mastery'
|
||||
onChange: (mode: 'custom' | 'manual' | 'mastery') => void
|
||||
isDark?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode selector tabs for worksheet generation
|
||||
* Large, prominent tabs that switch between Smart Difficulty, Manual Control, and Mastery Progression modes
|
||||
* Large, prominent tabs that switch between Custom Difficulty, Manual Control, and Mastery Progression modes
|
||||
*/
|
||||
export function ModeSelector({ currentMode, onChange, isDark = false }: ModeSelectorProps) {
|
||||
const modes = [
|
||||
{
|
||||
id: 'smart' as const,
|
||||
id: 'custom' as const,
|
||||
emoji: '🎯',
|
||||
label: 'Smart Difficulty',
|
||||
label: 'Custom Difficulty',
|
||||
description: 'Research-backed progressive difficulty with adaptive scaffolding per problem',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import { css } from '@styled/css'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { LayoutPreview } from './config-sidebar/LayoutPreview'
|
||||
import { validateProblemSpace } from '../utils/validateProblemSpace'
|
||||
import type { ProblemSpaceValidation } from '../utils/validateProblemSpace'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getDefaultColsForProblemsPerPage } from '../utils/layoutCalculations'
|
||||
import type { ProblemSpaceValidation } from '../utils/validateProblemSpace'
|
||||
import { validateProblemSpace } from '../utils/validateProblemSpace'
|
||||
import { LayoutPreview } from './config-sidebar/LayoutPreview'
|
||||
|
||||
interface OrientationPanelProps {
|
||||
orientation: 'portrait' | 'landscape'
|
||||
@@ -56,8 +56,27 @@ export function OrientationPanel({
|
||||
onProblemNumbersChange,
|
||||
onCellBordersChange,
|
||||
}: OrientationPanelProps) {
|
||||
// Calculate best problems per page for an orientation to minimize total change
|
||||
const getBestProblemsPerPage = (targetOrientation: 'portrait' | 'landscape') => {
|
||||
const currentTotal = problemsPerPage * pages
|
||||
const options = targetOrientation === 'portrait' ? [6, 8, 10, 12, 15] : [8, 10, 12, 15, 16, 20]
|
||||
|
||||
let bestOption = options[options.length - 1] // default to largest
|
||||
let smallestDiff = Math.abs(bestOption * pages - currentTotal)
|
||||
|
||||
for (const option of options) {
|
||||
const diff = Math.abs(option * pages - currentTotal)
|
||||
if (diff < smallestDiff) {
|
||||
smallestDiff = diff
|
||||
bestOption = option
|
||||
}
|
||||
}
|
||||
|
||||
return bestOption
|
||||
}
|
||||
|
||||
const handleOrientationChange = (newOrientation: 'portrait' | 'landscape') => {
|
||||
const newProblemsPerPage = newOrientation === 'portrait' ? 15 : 20
|
||||
const newProblemsPerPage = getBestProblemsPerPage(newOrientation)
|
||||
const newCols = getDefaultColsForProblemsPerPage(newProblemsPerPage, newOrientation)
|
||||
onOrientationChange(newOrientation, newProblemsPerPage, newCols)
|
||||
}
|
||||
@@ -68,8 +87,39 @@ export function OrientationPanel({
|
||||
}
|
||||
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Track container width to determine which buttons are visible
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry) {
|
||||
setContainerWidth(entry.contentRect.width)
|
||||
}
|
||||
})
|
||||
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
// All possible page options
|
||||
const allPageOptions = [1, 2, 3, 4, 10, 25, 50, 100]
|
||||
|
||||
// Determine which buttons are visible based on container width
|
||||
const getVisibleButtonCount = (): number => {
|
||||
if (containerWidth <= 280) return 4
|
||||
if (containerWidth <= 320) return 6
|
||||
return 8 // Show all options as buttons
|
||||
}
|
||||
|
||||
const visibleButtonCount = getVisibleButtonCount()
|
||||
const visibleButtons = allPageOptions.slice(0, visibleButtonCount)
|
||||
const dropdownOptions = allPageOptions.slice(visibleButtonCount)
|
||||
|
||||
const total = problemsPerPage * pages
|
||||
const problemsForOrientation =
|
||||
orientation === 'portrait' ? [6, 8, 10, 12, 15] : [8, 10, 12, 15, 16, 20]
|
||||
|
||||
@@ -110,15 +160,14 @@ export function OrientationPanel({
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mildest (most severe) warning among dropdown items (4, 10, 25, 50, 100)
|
||||
* Get the mildest (most severe) warning among dropdown items
|
||||
* Returns 'none' if no warnings, 'caution' if any caution, 'danger' if any danger
|
||||
*/
|
||||
const getDropdownMildestWarning = (): 'none' | 'caution' | 'danger' => {
|
||||
const dropdownPageCounts = [4, 10, 25, 50, 100]
|
||||
let hasCaution = false
|
||||
let hasDanger = false
|
||||
|
||||
for (const pageCount of dropdownPageCounts) {
|
||||
for (const pageCount of dropdownOptions) {
|
||||
const risk = getRiskLevel(getValidationForPageCount(pageCount))
|
||||
if (risk === 'danger') hasDanger = true
|
||||
if (risk === 'caution') hasCaution = true
|
||||
@@ -139,35 +188,35 @@ export function OrientationPanel({
|
||||
p: '4',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
'@media (max-width: 400px)': {
|
||||
containerType: 'inline-size',
|
||||
'@container (max-width: 400px)': {
|
||||
p: '3',
|
||||
},
|
||||
'@media (max-width: 300px)': {
|
||||
'@container (max-width: 300px)': {
|
||||
p: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '3' })}>
|
||||
{/* Row 1: Orientation + Pages */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '3',
|
||||
'@container (min-width: 400px)': {
|
||||
gap: '4',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Orientation + Pages - Always stacked for better visual hierarchy */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexDirection: 'column',
|
||||
gap: '3',
|
||||
alignItems: 'end',
|
||||
'@media (max-width: 444px)': {
|
||||
flexDirection: 'column',
|
||||
gap: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Orientation */}
|
||||
<div
|
||||
className={css({
|
||||
flex: '1',
|
||||
minWidth: 0,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
@@ -176,6 +225,9 @@ export function OrientationPanel({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
mb: '1.5',
|
||||
'@container (min-width: 400px)': {
|
||||
mb: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Orientation
|
||||
@@ -183,54 +235,76 @@ export function OrientationPanel({
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1.5',
|
||||
'@media (max-width: 400px)': {
|
||||
gap: '1',
|
||||
gap: '2',
|
||||
width: '100%',
|
||||
'@container (min-width: 500px)': {
|
||||
gap: '3',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<LayoutPreview
|
||||
orientation="portrait"
|
||||
cols={
|
||||
orientation === 'portrait'
|
||||
? cols
|
||||
: getDefaultColsForProblemsPerPage(15, 'portrait')
|
||||
}
|
||||
rows={
|
||||
orientation === 'portrait'
|
||||
? Math.ceil(problemsPerPage / cols)
|
||||
: Math.ceil(15 / getDefaultColsForProblemsPerPage(15, 'portrait'))
|
||||
}
|
||||
onClick={() => handleOrientationChange('portrait')}
|
||||
isSelected={orientation === 'portrait'}
|
||||
/>
|
||||
<LayoutPreview
|
||||
orientation="landscape"
|
||||
cols={
|
||||
orientation === 'landscape'
|
||||
? cols
|
||||
: getDefaultColsForProblemsPerPage(20, 'landscape')
|
||||
}
|
||||
rows={
|
||||
orientation === 'landscape'
|
||||
? Math.ceil(problemsPerPage / cols)
|
||||
: Math.ceil(20 / getDefaultColsForProblemsPerPage(20, 'landscape'))
|
||||
}
|
||||
onClick={() => handleOrientationChange('landscape')}
|
||||
isSelected={orientation === 'landscape'}
|
||||
/>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
{(() => {
|
||||
const portraitProblemsPerPage =
|
||||
orientation === 'portrait'
|
||||
? problemsPerPage
|
||||
: getBestProblemsPerPage('portrait')
|
||||
const portraitCols = getDefaultColsForProblemsPerPage(
|
||||
portraitProblemsPerPage,
|
||||
'portrait'
|
||||
)
|
||||
return (
|
||||
<LayoutPreview
|
||||
orientation="portrait"
|
||||
cols={portraitCols}
|
||||
rows={Math.ceil(portraitProblemsPerPage / portraitCols)}
|
||||
onClick={() => handleOrientationChange('portrait')}
|
||||
isSelected={orientation === 'portrait'}
|
||||
maxSize={80}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
{(() => {
|
||||
const landscapeProblemsPerPage =
|
||||
orientation === 'landscape'
|
||||
? problemsPerPage
|
||||
: getBestProblemsPerPage('landscape')
|
||||
const landscapeCols = getDefaultColsForProblemsPerPage(
|
||||
landscapeProblemsPerPage,
|
||||
'landscape'
|
||||
)
|
||||
return (
|
||||
<LayoutPreview
|
||||
orientation="landscape"
|
||||
cols={landscapeCols}
|
||||
rows={Math.ceil(landscapeProblemsPerPage / landscapeCols)}
|
||||
onClick={() => handleOrientationChange('landscape')}
|
||||
isSelected={orientation === 'landscape'}
|
||||
maxSize={80}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pages */}
|
||||
<div
|
||||
className={css({
|
||||
flexShrink: 0,
|
||||
'@media (max-width: 444px)': {
|
||||
width: '100%',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
@@ -239,23 +313,26 @@ export function OrientationPanel({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
mb: '1.5',
|
||||
'@container (min-width: 400px)': {
|
||||
mb: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Pages
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
gap: '1',
|
||||
alignItems: 'center',
|
||||
'@media (max-width: 444px)': {
|
||||
width: '100%',
|
||||
gap: '0.5',
|
||||
},
|
||||
width: '100%',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
})}
|
||||
>
|
||||
{/* Quick select buttons for 1-3 pages */}
|
||||
{[1, 2, 3].map((pageCount) => {
|
||||
{/* Quick select buttons - dynamically shown based on container width */}
|
||||
{visibleButtons.map((pageCount) => {
|
||||
const isSelected = pages === pageCount
|
||||
const validation = getValidationForPageCount(pageCount)
|
||||
const risk = getRiskLevel(validation)
|
||||
@@ -336,17 +413,11 @@ export function OrientationPanel({
|
||||
? 'yellow.400'
|
||||
: 'brand.400',
|
||||
},
|
||||
'@media (max-width: 444px)': {
|
||||
w: '6',
|
||||
h: '6',
|
||||
'@container (max-width: 280px)': {
|
||||
w: '7',
|
||||
h: '7',
|
||||
fontSize: '2xs',
|
||||
},
|
||||
'@media (max-width: 300px)': {
|
||||
w: '5',
|
||||
h: '5',
|
||||
fontSize: '2xs',
|
||||
borderWidth: '1px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{pageCount}
|
||||
@@ -408,15 +479,165 @@ export function OrientationPanel({
|
||||
return button
|
||||
})}
|
||||
|
||||
{/* Dropdown for 4+ pages */}
|
||||
<DropdownMenu.Root open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
{/* Dropdown for remaining options (only if more than one option) */}
|
||||
{dropdownOptions.length === 1 ? (
|
||||
// Render single dropdown option as a button
|
||||
(() => {
|
||||
const pageCount = dropdownOptions[0]
|
||||
const isSelected = pages === pageCount
|
||||
const validation = getValidationForPageCount(pageCount)
|
||||
const risk = getRiskLevel(validation)
|
||||
const tooltipMessage = getTooltipMessage(validation)
|
||||
|
||||
const button = (
|
||||
<button
|
||||
key={pageCount}
|
||||
type="button"
|
||||
data-action={`select-pages-${pageCount}`}
|
||||
onClick={() => onPagesChange(pageCount)}
|
||||
className={css({
|
||||
w: '8',
|
||||
h: '8',
|
||||
border: '2px solid',
|
||||
borderColor: isSelected
|
||||
? risk === 'danger'
|
||||
? 'red.500'
|
||||
: risk === 'caution'
|
||||
? 'yellow.500'
|
||||
: 'brand.500'
|
||||
: risk === 'danger'
|
||||
? isDark
|
||||
? 'red.700'
|
||||
: 'red.300'
|
||||
: risk === 'caution'
|
||||
? isDark
|
||||
? 'yellow.700'
|
||||
: 'yellow.300'
|
||||
: isDark
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
bg: isSelected
|
||||
? isDark
|
||||
? risk === 'danger'
|
||||
? 'red.900'
|
||||
: risk === 'caution'
|
||||
? 'yellow.900'
|
||||
: 'brand.900'
|
||||
: risk === 'danger'
|
||||
? 'red.50'
|
||||
: risk === 'caution'
|
||||
? 'yellow.50'
|
||||
: 'brand.50'
|
||||
: isDark
|
||||
? 'gray.700'
|
||||
: 'white',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'bold',
|
||||
color: isSelected
|
||||
? isDark
|
||||
? risk === 'danger'
|
||||
? 'red.200'
|
||||
: risk === 'caution'
|
||||
? 'yellow.200'
|
||||
: 'brand.200'
|
||||
: risk === 'danger'
|
||||
? 'red.700'
|
||||
: risk === 'caution'
|
||||
? 'yellow.700'
|
||||
: 'brand.700'
|
||||
: isDark
|
||||
? 'gray.300'
|
||||
: 'gray.600',
|
||||
transition: 'all 0.15s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
_hover: {
|
||||
borderColor:
|
||||
risk === 'danger'
|
||||
? 'red.400'
|
||||
: risk === 'caution'
|
||||
? 'yellow.400'
|
||||
: 'brand.400',
|
||||
},
|
||||
'@container (max-width: 280px)': {
|
||||
w: '7',
|
||||
h: '7',
|
||||
fontSize: '2xs',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{pageCount}
|
||||
{/* Warning indicator dot */}
|
||||
{risk !== 'none' && (
|
||||
<span
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '-1',
|
||||
right: '-1',
|
||||
w: '2',
|
||||
h: '2',
|
||||
bg: risk === 'danger' ? 'red.500' : 'yellow.500',
|
||||
rounded: 'full',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
// Wrap in tooltip if there's a warning message
|
||||
if (tooltipMessage) {
|
||||
return (
|
||||
<Tooltip.Provider key={pageCount}>
|
||||
<Tooltip.Root delayDuration={0} disableHoverableContent={true}>
|
||||
<Tooltip.Trigger asChild>{button}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className={css({
|
||||
bg: isDark ? 'gray.800' : 'white',
|
||||
color: isDark ? 'gray.100' : 'gray.900',
|
||||
px: '3',
|
||||
py: '2',
|
||||
rounded: 'lg',
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'gray.600' : 'gray.200',
|
||||
maxW: '64',
|
||||
fontSize: 'xs',
|
||||
lineHeight: '1.5',
|
||||
whiteSpace: 'pre-wrap',
|
||||
zIndex: 10000,
|
||||
})}
|
||||
sideOffset={5}
|
||||
>
|
||||
{tooltipMessage}
|
||||
<Tooltip.Arrow
|
||||
className={css({
|
||||
fill: isDark ? 'gray.800' : 'white',
|
||||
})}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return button
|
||||
})()
|
||||
) : dropdownOptions.length > 1 ? (
|
||||
<DropdownMenu.Root open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root
|
||||
delayDuration={0}
|
||||
disableHoverableContent={true}
|
||||
open={
|
||||
!dropdownOpen &&
|
||||
pages > 3 &&
|
||||
dropdownOptions.includes(pages) &&
|
||||
getTooltipMessage(getValidationForPageCount(pages)) !== null
|
||||
? undefined
|
||||
: false
|
||||
@@ -432,27 +653,29 @@ export function OrientationPanel({
|
||||
h: '8',
|
||||
px: '2',
|
||||
border: '2px solid',
|
||||
borderColor: pages > 3 ? 'brand.500' : isDark ? 'gray.600' : 'gray.300',
|
||||
bg:
|
||||
pages > 3
|
||||
? isDark
|
||||
? 'brand.900'
|
||||
: 'brand.50'
|
||||
: isDark
|
||||
? 'gray.700'
|
||||
: 'white',
|
||||
borderColor: dropdownOptions.includes(pages)
|
||||
? 'brand.500'
|
||||
: isDark
|
||||
? 'gray.600'
|
||||
: 'gray.300',
|
||||
bg: dropdownOptions.includes(pages)
|
||||
? isDark
|
||||
? 'brand.900'
|
||||
: 'brand.50'
|
||||
: isDark
|
||||
? 'gray.700'
|
||||
: 'white',
|
||||
rounded: 'lg',
|
||||
cursor: 'pointer',
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
pages > 3
|
||||
? isDark
|
||||
? 'brand.200'
|
||||
: 'brand.700'
|
||||
: isDark
|
||||
? 'gray.300'
|
||||
: 'gray.600',
|
||||
color: dropdownOptions.includes(pages)
|
||||
? isDark
|
||||
? 'brand.200'
|
||||
: 'brand.700'
|
||||
: isDark
|
||||
? 'gray.300'
|
||||
: 'gray.600',
|
||||
transition: 'all 0.15s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -463,20 +686,18 @@ export function OrientationPanel({
|
||||
_hover: {
|
||||
borderColor: 'brand.400',
|
||||
},
|
||||
'@media (max-width: 444px)': {
|
||||
minW: '6',
|
||||
h: '6',
|
||||
'@container (max-width: 280px)': {
|
||||
minW: '7',
|
||||
h: '7',
|
||||
fontSize: '2xs',
|
||||
},
|
||||
'@media (max-width: 300px)': {
|
||||
minW: '5',
|
||||
h: '5',
|
||||
fontSize: '2xs',
|
||||
borderWidth: '1px',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{pages > 3 ? pages : '4+'}
|
||||
{dropdownOptions.includes(pages)
|
||||
? pages
|
||||
: dropdownOptions.length > 0
|
||||
? `${dropdownOptions[0]}+`
|
||||
: '•••'}
|
||||
<span className={css({ fontSize: '2xs', opacity: 0.7 })}>▼</span>
|
||||
{/* Warning indicator dot - shows mildest warning from all dropdown items */}
|
||||
{getDropdownMildestWarning() !== 'none' && (
|
||||
@@ -498,7 +719,8 @@ export function OrientationPanel({
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
</Tooltip.Trigger>
|
||||
{pages > 3 && getTooltipMessage(getValidationForPageCount(pages)) && (
|
||||
{dropdownOptions.includes(pages) &&
|
||||
getTooltipMessage(getValidationForPageCount(pages)) && (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className={css({
|
||||
@@ -544,7 +766,7 @@ export function OrientationPanel({
|
||||
})}
|
||||
sideOffset={5}
|
||||
>
|
||||
{[4, 10, 25, 50, 100].map((pageCount) => {
|
||||
{dropdownOptions.map((pageCount) => {
|
||||
const isSelected = pages === pageCount
|
||||
const validation = getValidationForPageCount(pageCount)
|
||||
const risk = getRiskLevel(validation)
|
||||
@@ -668,42 +890,30 @@ export function OrientationPanel({
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Problems per page dropdown + Total badge */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '3',
|
||||
alignItems: 'center',
|
||||
'@media (max-width: 444px)': {
|
||||
flexDirection: 'column',
|
||||
gap: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Problems per page dropdown */}
|
||||
<div>
|
||||
<div
|
||||
className={css({
|
||||
flex: '1',
|
||||
minWidth: 0,
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
color: isDark ? 'gray.400' : 'gray.500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
display: 'block',
|
||||
mb: '1.5',
|
||||
'@container (min-width: 400px)': {
|
||||
mb: '2',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
color: isDark ? 'gray.400' : 'gray.500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
display: 'block',
|
||||
mb: '1.5',
|
||||
})}
|
||||
>
|
||||
Problems per Page
|
||||
</div>
|
||||
Problems per Page
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
@@ -730,10 +940,6 @@ export function OrientationPanel({
|
||||
_hover: {
|
||||
borderColor: 'brand.400',
|
||||
},
|
||||
'@media (max-width: 200px)': {
|
||||
px: '1',
|
||||
fontSize: '2xs',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span
|
||||
@@ -744,26 +950,8 @@ export function OrientationPanel({
|
||||
minWidth: 0,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
'@media (max-width: 250px)': {
|
||||
display: 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{problemsPerPage} problems ({cols} cols × {Math.ceil(problemsPerPage / cols)}{' '}
|
||||
rows)
|
||||
</span>
|
||||
<span
|
||||
className={css({
|
||||
display: 'none',
|
||||
'@media (max-width: 250px)': {
|
||||
display: 'inline',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{problemsPerPage} ({cols}×{Math.ceil(problemsPerPage / cols)})
|
||||
</span>
|
||||
{problemsPerPage} problems ({cols} cols × {Math.ceil(problemsPerPage / cols)}{' '}
|
||||
rows)
|
||||
</span>
|
||||
<span
|
||||
className={css({
|
||||
@@ -915,56 +1103,17 @@ export function OrientationPanel({
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
|
||||
{/* Total problems badge */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
flexShrink: 0,
|
||||
'@media (max-width: 444px)': {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
color: isDark ? 'gray.400' : 'gray.500',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
})}
|
||||
>
|
||||
Total
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.100',
|
||||
rounded: 'full',
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.700',
|
||||
})}
|
||||
>
|
||||
{total}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Layout Options */}
|
||||
{/* Layout Options */}
|
||||
<div
|
||||
className={css({
|
||||
borderTop: '1px solid',
|
||||
borderColor: isDark ? 'gray.700' : 'gray.200',
|
||||
pt: '3',
|
||||
mt: '1',
|
||||
'@container (min-width: 400px)': {
|
||||
pt: '4',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@@ -975,12 +1124,24 @@ export function OrientationPanel({
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 'wider',
|
||||
mb: '2',
|
||||
'@container (min-width: 400px)': {
|
||||
mb: '3',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Layout Options
|
||||
</div>
|
||||
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '2' })}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '2',
|
||||
'@container (min-width: 400px)': {
|
||||
gap: '3',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Problem Numbers Toggle */}
|
||||
<label
|
||||
className={css({
|
||||
|
||||
@@ -143,8 +143,8 @@ function PreviewContent({
|
||||
formState.operator,
|
||||
// V4: Mode and conditional display settings
|
||||
formState.mode,
|
||||
formState.displayRules, // Smart mode: conditional scaffolding
|
||||
formState.difficultyProfile, // Smart mode: difficulty preset
|
||||
formState.displayRules, // Custom mode: conditional scaffolding
|
||||
formState.difficultyProfile, // Custom mode: difficulty preset
|
||||
formState.manualPreset, // Manual mode: manual preset
|
||||
// Mastery mode: skill IDs (CRITICAL for mastery+mixed mode)
|
||||
formState.currentAdditionSkillId,
|
||||
|
||||
@@ -32,12 +32,12 @@ import { DifficultyPresetDropdown } from './DifficultyPresetDropdown'
|
||||
import { MakeEasierHarderButtons } from './MakeEasierHarderButtons'
|
||||
import { OverallDifficultySlider } from './OverallDifficultySlider'
|
||||
|
||||
export interface SmartModeControlsProps {
|
||||
export interface CustomModeControlsProps {
|
||||
formState: WorksheetFormState
|
||||
onChange: (updates: Partial<WorksheetFormState>) => void
|
||||
}
|
||||
|
||||
export function SmartModeControls({ formState, onChange }: SmartModeControlsProps) {
|
||||
export function CustomModeControls({ formState, onChange }: CustomModeControlsProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
const [showDebugPlot, setShowDebugPlot] = useState(false)
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
import * as Slider from '@radix-ui/react-slider'
|
||||
import { css } from '@styled/css'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import {
|
||||
DIFFICULTY_PROFILES,
|
||||
DIFFICULTY_PROGRESSION,
|
||||
calculateOverallDifficulty,
|
||||
calculateRegroupingIntensity,
|
||||
calculateScaffoldingLevel,
|
||||
REGROUPING_PROGRESSION,
|
||||
SCAFFOLDING_PROGRESSION,
|
||||
DIFFICULTY_PROFILES,
|
||||
DIFFICULTY_PROGRESSION,
|
||||
type DifficultyLevel,
|
||||
findNearestValidState,
|
||||
getProfileFromConfig,
|
||||
type DifficultyLevel,
|
||||
REGROUPING_PROGRESSION,
|
||||
SCAFFOLDING_PROGRESSION,
|
||||
} from '../../difficultyProfiles'
|
||||
import type { DisplayRules } from '../../displayRules'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
|
||||
export interface OverallDifficultySliderProps {
|
||||
currentDifficulty: number
|
||||
@@ -162,13 +162,14 @@ export function OverallDifficultySlider({
|
||||
mb: '1.5',
|
||||
})}
|
||||
>
|
||||
Overall Difficulty: {currentDifficulty.toFixed(1)} / 10
|
||||
Overall Difficulty: {Number.isNaN(currentDifficulty) ? '0.0' : currentDifficulty.toFixed(1)}{' '}
|
||||
/ 10
|
||||
</div>
|
||||
|
||||
{/* Difficulty Slider */}
|
||||
<div className={css({ position: 'relative', px: '2' })}>
|
||||
<Slider.Root
|
||||
value={[currentDifficulty * 10]}
|
||||
value={[Number.isNaN(currentDifficulty) ? 0 : currentDifficulty * 10]}
|
||||
max={100}
|
||||
step={1}
|
||||
onValueChange={handleValueChange}
|
||||
|
||||
@@ -22,7 +22,7 @@ const RULE_OPTIONS: Array<{ value: RuleMode; label: string; short: string }> = [
|
||||
{ value: 'auto', label: 'Auto (Use Mastery Progression)', short: 'Auto' },
|
||||
{ value: 'always', label: 'Always', short: 'Always' },
|
||||
{ value: 'whenRegrouping', label: 'When Regrouping', short: 'Regroup' },
|
||||
{ value: 'whenMultipleRegroups', label: 'Multiple Regroups', short: '2+' },
|
||||
{ value: 'whenMultipleRegroups', label: 'Multiple Regroups', short: '2+ reg' },
|
||||
{ value: 'when3PlusDigits', label: '3+ Digits', short: '3+ dig' },
|
||||
{ value: 'never', label: 'Never', short: 'Never' },
|
||||
]
|
||||
|
||||
@@ -17,13 +17,13 @@ export function ContentTab() {
|
||||
onChange={(operator) => {
|
||||
// If switching to 'mixed' while in mastery mode without both skill IDs,
|
||||
// automatically switch to smart mode to prevent errors
|
||||
const mode = formState.mode ?? 'smart'
|
||||
const mode = formState.mode ?? 'custom'
|
||||
if (
|
||||
operator === 'mixed' &&
|
||||
mode === 'mastery' &&
|
||||
(!formState.currentAdditionSkillId || !formState.currentSubtractionSkillId)
|
||||
) {
|
||||
onChange({ operator, mode: 'smart' })
|
||||
onChange({ operator, mode: 'custom' })
|
||||
} else {
|
||||
onChange({ operator })
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from '@/contexts/ThemeContext'
|
||||
import type { WorksheetFormState } from '../../types'
|
||||
import { MasteryModePanel } from '../config-panel/MasteryModePanel'
|
||||
import { ProgressiveDifficultyToggle } from '../config-panel/ProgressiveDifficultyToggle'
|
||||
import { SmartModeControls } from '../config-panel/SmartModeControls'
|
||||
import { CustomModeControls } from '../config-panel/CustomModeControls'
|
||||
import { DifficultyMethodSelector } from '../DifficultyMethodSelector'
|
||||
import { useWorksheetConfig } from '../WorksheetConfigContext'
|
||||
|
||||
@@ -15,19 +15,19 @@ export function DifficultyTab() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'smart'
|
||||
const currentMethod = formState.mode === 'mastery' ? 'mastery' : 'custom'
|
||||
|
||||
// Handler for difficulty method switching (smart vs mastery)
|
||||
const handleMethodChange = (newMethod: 'smart' | 'mastery') => {
|
||||
const handleMethodChange = (newMethod: 'custom' | 'mastery') => {
|
||||
if (currentMethod === newMethod) {
|
||||
return
|
||||
}
|
||||
|
||||
const displayRules = formState.displayRules ?? defaultAdditionConfig.displayRules
|
||||
|
||||
if (newMethod === 'smart') {
|
||||
if (newMethod === 'custom') {
|
||||
onChange({
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
displayRules,
|
||||
difficultyProfile: 'earlyLearner',
|
||||
} as unknown as Partial<WorksheetFormState>)
|
||||
@@ -56,7 +56,9 @@ export function DifficultyTab() {
|
||||
/>
|
||||
|
||||
{/* Method-specific controls */}
|
||||
{currentMethod === 'smart' && <SmartModeControls formState={formState} onChange={onChange} />}
|
||||
{currentMethod === 'custom' && (
|
||||
<CustomModeControls formState={formState} onChange={onChange} />
|
||||
)}
|
||||
|
||||
{currentMethod === 'mastery' && (
|
||||
<MasteryModePanel formState={formState} onChange={onChange} isDark={isDark} />
|
||||
|
||||
@@ -10,6 +10,7 @@ interface LayoutPreviewProps {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
isSelected?: boolean
|
||||
maxSize?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ export function LayoutPreview({
|
||||
className,
|
||||
onClick,
|
||||
isSelected = false,
|
||||
maxSize = 48,
|
||||
}: LayoutPreviewProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
@@ -31,7 +33,6 @@ export function LayoutPreview({
|
||||
const pageAspect = orientation === 'portrait' ? 8.5 / 11 : 11 / 8.5
|
||||
|
||||
// Scale to fit in button (max dimensions)
|
||||
const maxSize = 48
|
||||
let pageWidth: number
|
||||
let pageHeight: number
|
||||
|
||||
@@ -51,15 +52,16 @@ export function LayoutPreview({
|
||||
|
||||
const svgContent = (
|
||||
<svg
|
||||
width={pageWidth}
|
||||
height={pageHeight}
|
||||
viewBox={`0 0 ${pageWidth} ${pageHeight}`}
|
||||
className={css({
|
||||
rounded: 'sm',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})}
|
||||
style={{
|
||||
backgroundColor: isDark ? '#1f2937' : 'white',
|
||||
}}
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
{/* Problem grid */}
|
||||
{Array.from({ length: rows }).map((_, rowIdx) =>
|
||||
@@ -80,6 +82,8 @@ export function LayoutPreview({
|
||||
|
||||
// If used as a button, wrap in button element with styling
|
||||
if (onClick) {
|
||||
const aspectRatio = orientation === 'portrait' ? 8.5 / 11 : 11 / 8.5
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -95,10 +99,15 @@ export function LayoutPreview({
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s',
|
||||
p: '2',
|
||||
width: '100%',
|
||||
maxWidth: '32',
|
||||
_hover: {
|
||||
borderColor: 'brand.400',
|
||||
},
|
||||
})}
|
||||
style={{
|
||||
aspectRatio: aspectRatio.toString(),
|
||||
}}
|
||||
>
|
||||
{svgContent}
|
||||
</button>
|
||||
@@ -106,14 +115,23 @@ export function LayoutPreview({
|
||||
}
|
||||
|
||||
// Otherwise just return the SVG with border
|
||||
const aspectRatio = orientation === 'portrait' ? 8.5 / 11 : 11 / 8.5
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css({
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'gray.600' : 'gray.300',
|
||||
rounded: 'sm',
|
||||
display: 'inline-block',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
p: '1',
|
||||
})}
|
||||
style={{
|
||||
width: `${maxSize}px`,
|
||||
aspectRatio: aspectRatio.toString(),
|
||||
}}
|
||||
>
|
||||
{svgContent}
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function LayoutTab() {
|
||||
digitRange={formState.digitRange || { min: 2, max: 2 }}
|
||||
pAnyStart={formState.pAnyStart ?? 0}
|
||||
operator={formState.operator || 'addition'}
|
||||
mode={formState.mode || 'smart'}
|
||||
mode={formState.mode || 'custom'}
|
||||
problemNumbers={
|
||||
((formState.displayRules ?? defaultAdditionConfig.displayRules).problemNumbers as
|
||||
| 'always'
|
||||
|
||||
@@ -247,6 +247,7 @@ export function TabNavigation({
|
||||
orientation={orientation!}
|
||||
cols={cols!}
|
||||
rows={Math.ceil(problemsPerPage! / cols!)}
|
||||
maxSize={32}
|
||||
/>
|
||||
) : showScaffoldingPreview ? (
|
||||
<ProblemPreview
|
||||
@@ -299,6 +300,27 @@ export function TabNavigation({
|
||||
{subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
{/* Total badge for layout tab */}
|
||||
{tab.id === 'layout' && problemsPerPage && pages ? (
|
||||
<span
|
||||
className={css({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
px: '2',
|
||||
py: '0.5',
|
||||
bg: activeTab === tab.id ? 'brand.500' : isDark ? 'gray.600' : 'gray.400',
|
||||
color: 'white',
|
||||
rounded: 'full',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
mt: '0.5',
|
||||
})}
|
||||
>
|
||||
<span>Total:</span>
|
||||
<span>{problemsPerPage * pages}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -44,7 +44,7 @@ export function WorksheetPreviewProvider({ formState, children }: WorksheetPrevi
|
||||
const problemsPerPage = formState.problemsPerPage ?? 20
|
||||
const pages = formState.pages ?? 1
|
||||
const operator = formState.operator ?? 'addition'
|
||||
const mode = formState.mode ?? 'smart'
|
||||
const mode = formState.mode ?? 'custom'
|
||||
|
||||
// Reset dismissed state when config changes
|
||||
setIsDismissed(false)
|
||||
|
||||
@@ -83,7 +83,7 @@ export type AdditionConfigV1 = z.infer<typeof additionConfigV1Schema>
|
||||
|
||||
/**
|
||||
* Addition worksheet config - Version 2
|
||||
* Smart difficulty system with conditional display rules
|
||||
* Custom difficulty system with conditional display rules
|
||||
*/
|
||||
export const additionConfigV2Schema = z.object({
|
||||
version: z.literal(2),
|
||||
@@ -176,7 +176,7 @@ export type AdditionConfigV2 = z.infer<typeof additionConfigV2Schema>
|
||||
|
||||
/**
|
||||
* Addition worksheet config - Version 3
|
||||
* Two-mode system: Smart Difficulty vs Manual Control
|
||||
* Two-mode system: Custom Difficulty vs Manual Control
|
||||
*/
|
||||
|
||||
// Shared base fields for both modes
|
||||
@@ -205,9 +205,9 @@ const additionConfigV3BaseSchema = z.object({
|
||||
prngAlgorithm: z.string().optional(),
|
||||
})
|
||||
|
||||
// Smart Difficulty Mode
|
||||
const additionConfigV3SmartSchema = additionConfigV3BaseSchema.extend({
|
||||
mode: z.literal('smart'),
|
||||
// Custom Difficulty Mode
|
||||
const additionConfigV3CustomSchema = additionConfigV3BaseSchema.extend({
|
||||
mode: z.literal('custom'),
|
||||
|
||||
// Conditional display rules
|
||||
displayRules: z.object({
|
||||
@@ -269,10 +269,10 @@ const additionConfigV3SmartSchema = additionConfigV3BaseSchema.extend({
|
||||
]),
|
||||
}),
|
||||
|
||||
// Optional: Which smart difficulty profile is selected
|
||||
// Optional: Which custom difficulty profile is selected
|
||||
difficultyProfile: z.string().optional(),
|
||||
|
||||
// showTenFramesForAll is deprecated in V3 smart mode
|
||||
// showTenFramesForAll is deprecated in V3 custom mode
|
||||
// (controlled by displayRules.tenFrames)
|
||||
})
|
||||
|
||||
@@ -295,12 +295,12 @@ const additionConfigV3ManualSchema = additionConfigV3BaseSchema.extend({
|
||||
|
||||
// V3 uses discriminated union on 'mode'
|
||||
export const additionConfigV3Schema = z.discriminatedUnion('mode', [
|
||||
additionConfigV3SmartSchema,
|
||||
additionConfigV3CustomSchema,
|
||||
additionConfigV3ManualSchema,
|
||||
])
|
||||
|
||||
export type AdditionConfigV3 = z.infer<typeof additionConfigV3Schema>
|
||||
export type AdditionConfigV3Smart = z.infer<typeof additionConfigV3SmartSchema>
|
||||
export type AdditionConfigV3Custom = z.infer<typeof additionConfigV3CustomSchema>
|
||||
export type AdditionConfigV3Manual = z.infer<typeof additionConfigV3ManualSchema>
|
||||
|
||||
/**
|
||||
@@ -355,11 +355,11 @@ const additionConfigV4BaseSchema = z.object({
|
||||
prngAlgorithm: z.string().optional(),
|
||||
})
|
||||
|
||||
// Smart Difficulty Mode for V4
|
||||
const additionConfigV4SmartSchema = additionConfigV4BaseSchema.extend({
|
||||
mode: z.literal('smart'),
|
||||
// Custom Difficulty Mode for V4
|
||||
const additionConfigV4CustomSchema = additionConfigV4BaseSchema.extend({
|
||||
mode: z.literal('custom'),
|
||||
|
||||
// Conditional display rules (with 'auto' for deferring to smart difficulty)
|
||||
// Conditional display rules (with 'auto' for deferring to custom difficulty)
|
||||
displayRules: z.object({
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
answerBoxes: z.enum(displayRuleValues),
|
||||
@@ -371,16 +371,16 @@ const additionConfigV4SmartSchema = additionConfigV4BaseSchema.extend({
|
||||
borrowingHints: z.enum(displayRuleValues),
|
||||
}),
|
||||
|
||||
// Optional: Which smart difficulty profile is selected
|
||||
// Optional: Which custom difficulty profile is selected
|
||||
difficultyProfile: z.string().optional(),
|
||||
})
|
||||
|
||||
// Manual Control Mode for V4
|
||||
// Now uses displayRules like Smart/Mastery modes for 1:1 correspondence
|
||||
// Now uses displayRules like Custom/Mastery modes for 1:1 correspondence
|
||||
const additionConfigV4ManualSchema = additionConfigV4BaseSchema.extend({
|
||||
mode: z.literal('manual'),
|
||||
|
||||
// Manual mode now uses conditional display rules (same as Smart/Mastery)
|
||||
// Manual mode now uses conditional display rules (same as Custom/Mastery)
|
||||
// 'auto' is available but typically not used in manual mode
|
||||
displayRules: z.object({
|
||||
carryBoxes: z.enum(displayRuleValues),
|
||||
@@ -452,13 +452,13 @@ const additionConfigV4MasterySchema = additionConfigV4BaseSchema.extend({
|
||||
|
||||
// V4 uses discriminated union on 'mode'
|
||||
export const additionConfigV4Schema = z.discriminatedUnion('mode', [
|
||||
additionConfigV4SmartSchema,
|
||||
additionConfigV4CustomSchema,
|
||||
additionConfigV4ManualSchema,
|
||||
additionConfigV4MasterySchema,
|
||||
])
|
||||
|
||||
export type AdditionConfigV4 = z.infer<typeof additionConfigV4Schema>
|
||||
export type AdditionConfigV4Smart = z.infer<typeof additionConfigV4SmartSchema>
|
||||
export type AdditionConfigV4Custom = z.infer<typeof additionConfigV4CustomSchema>
|
||||
export type AdditionConfigV4Manual = z.infer<typeof additionConfigV4ManualSchema>
|
||||
export type AdditionConfigV4Mastery = z.infer<typeof additionConfigV4MasterySchema>
|
||||
|
||||
@@ -473,11 +473,11 @@ export const additionConfigSchema = z.discriminatedUnion('version', [
|
||||
export type AdditionConfig = z.infer<typeof additionConfigSchema>
|
||||
|
||||
/**
|
||||
* Default addition config (always latest version - V4 Smart Mode)
|
||||
* Default addition config (always latest version - V4 Custom Mode)
|
||||
*/
|
||||
export const defaultAdditionConfig: AdditionConfigV4Smart = {
|
||||
export const defaultAdditionConfig: AdditionConfigV4Custom = {
|
||||
version: 4,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
problemsPerPage: 20,
|
||||
cols: 5,
|
||||
pages: 1,
|
||||
@@ -545,11 +545,11 @@ function migrateAdditionV1toV2(v1: AdditionConfigV1): AdditionConfigV2 {
|
||||
* Determines mode based on whether difficultyProfile is set
|
||||
*/
|
||||
function migrateAdditionV2toV3(v2: AdditionConfigV2): AdditionConfigV3 {
|
||||
// If user has a difficultyProfile set, they're using smart mode
|
||||
// If user has a difficultyProfile set, they're using custom mode
|
||||
if (v2.difficultyProfile) {
|
||||
return {
|
||||
version: 3,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
problemsPerPage: v2.problemsPerPage,
|
||||
cols: v2.cols,
|
||||
pages: v2.pages,
|
||||
@@ -618,10 +618,10 @@ function migrateAdditionV3toV4(v3: AdditionConfigV3): AdditionConfigV4 {
|
||||
prngAlgorithm: v3.prngAlgorithm,
|
||||
}
|
||||
|
||||
if (v3.mode === 'smart') {
|
||||
if (v3.mode === 'custom') {
|
||||
return {
|
||||
...baseFields,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
displayRules: v3.displayRules,
|
||||
difficultyProfile: v3.difficultyProfile,
|
||||
}
|
||||
|
||||
@@ -550,6 +550,7 @@ export function calculateScaffoldingLevel(
|
||||
regroupingIntensity?: number
|
||||
): number {
|
||||
const ruleScores: Record<string, number> = {
|
||||
auto: 8, // Default to 'whenRegrouping' level for auto mode
|
||||
always: 10, // Maximum scaffolding (easiest)
|
||||
whenRegrouping: 8,
|
||||
whenMultipleRegroups: 5,
|
||||
@@ -632,8 +633,9 @@ export function calculateOverallDifficulty(
|
||||
result: progress * 10,
|
||||
})
|
||||
|
||||
// Scale to 0-10 range
|
||||
return progress * 10
|
||||
// Scale to 0-10 range and ensure no NaN
|
||||
const result = progress * 10
|
||||
return Number.isNaN(result) ? 0 : result
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,7 +77,7 @@ export function generateWorksheetPreview(
|
||||
}
|
||||
|
||||
// Generate all problems for full preview based on operator
|
||||
const mode = config.mode ?? 'smart'
|
||||
const mode = config.mode ?? 'custom'
|
||||
|
||||
console.log(
|
||||
`[PREVIEW] Step 2: Generating ${validatedConfig.total} problems (mode: ${mode}, operator: ${operator})...`
|
||||
@@ -275,7 +275,7 @@ export function generateSinglePage(
|
||||
// Generate all problems (need full set to know which problems go on which page)
|
||||
// This is unavoidable because problems are distributed across pages
|
||||
const operator = validatedConfig.operator ?? 'addition'
|
||||
const mode = config.mode ?? 'smart'
|
||||
const mode = config.mode ?? 'custom'
|
||||
|
||||
let problems
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ export function calculateMasteryMix(
|
||||
*
|
||||
* This is the bridge between mastery mode and smart mode.
|
||||
* Each skill's configuration (digitRange, regrouping, scaffolding) maps directly
|
||||
* to a Smart Mode configuration.
|
||||
* to a Custom Mode configuration.
|
||||
*
|
||||
* @param skill - Skill definition
|
||||
* @param problemCount - Number of problems to generate for this skill
|
||||
@@ -172,7 +172,7 @@ export function skillToConfig(
|
||||
): Partial<WorksheetConfig> {
|
||||
return {
|
||||
version: 4,
|
||||
mode: 'smart',
|
||||
mode: 'custom',
|
||||
|
||||
// Digit range from skill
|
||||
digitRange: skill.digitRange,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type {
|
||||
AdditionConfigV4,
|
||||
AdditionConfigV4Smart,
|
||||
AdditionConfigV4Custom,
|
||||
AdditionConfigV4Manual,
|
||||
AdditionConfigV4Mastery,
|
||||
} from '@/app/create/worksheets/config-schemas'
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
* Extends V4 config with additional derived fields needed for rendering
|
||||
*
|
||||
* V4 uses discriminated union on 'mode':
|
||||
* - Smart mode: Uses displayRules for conditional per-problem scaffolding
|
||||
* - Custom mode: Uses displayRules for conditional per-problem scaffolding
|
||||
* - Manual mode: Uses boolean flags for uniform display across all problems
|
||||
*
|
||||
* V4 adds digitRange field to support 1-5 digit problems
|
||||
@@ -47,12 +47,12 @@ export type WorksheetConfig = AdditionConfigV4 & {
|
||||
* Based on V4 config with additional derived state
|
||||
*
|
||||
* V4 supports three modes via discriminated union:
|
||||
* - Smart mode: Has displayRules and optional difficultyProfile
|
||||
* - Custom mode: Has displayRules and optional difficultyProfile
|
||||
* - Mastery mode: Has displayRules and optional currentStepId
|
||||
* - Manual mode: Has boolean display flags and optional manualPreset
|
||||
*
|
||||
* During editing, mode field may be present to indicate which mode is active.
|
||||
* If mode is absent, defaults to 'smart' mode.
|
||||
* If mode is absent, defaults to 'custom' mode.
|
||||
*
|
||||
* This type is intentionally permissive during form editing to allow fields from
|
||||
* all modes to exist temporarily. Validation will enforce mode consistency.
|
||||
@@ -72,7 +72,7 @@ export type WorksheetConfig = AdditionConfigV4 & {
|
||||
*
|
||||
* See `.claude/WORKSHEET_CONFIG_PERSISTENCE.md` for full architecture.
|
||||
*/
|
||||
export type WorksheetFormState = Partial<Omit<AdditionConfigV4Smart, 'version'>> &
|
||||
export type WorksheetFormState = Partial<Omit<AdditionConfigV4Custom, 'version'>> &
|
||||
Partial<Omit<AdditionConfigV4Manual, 'version'>> &
|
||||
Partial<Omit<AdditionConfigV4Mastery, 'version'>> & {
|
||||
// ========================================
|
||||
|
||||
@@ -58,8 +58,8 @@ function generatePageTypst(
|
||||
|
||||
// Enrich problems with display options based on mode
|
||||
const enrichedProblems = pageProblems.map((p, index) => {
|
||||
if (config.mode === 'smart' || config.mode === 'mastery') {
|
||||
// Smart & Mastery modes: Per-problem conditional display based on problem complexity
|
||||
if (config.mode === 'custom' || config.mode === 'mastery') {
|
||||
// Custom & Mastery modes: Per-problem conditional display based on problem complexity
|
||||
// Both modes use displayRules for conditional scaffolding
|
||||
const meta =
|
||||
p.operator === 'add'
|
||||
@@ -103,7 +103,7 @@ function generatePageTypst(
|
||||
...displayOptions, // Now includes showBorrowNotation and showBorrowingHints from resolved rules
|
||||
}
|
||||
} else {
|
||||
// Manual mode: Per-problem conditional display using displayRules (same as Smart/Mastery)
|
||||
// Manual mode: Per-problem conditional display using displayRules (same as Custom/Mastery)
|
||||
const meta =
|
||||
p.operator === 'add'
|
||||
? analyzeProblem(p.a, p.b)
|
||||
|
||||
@@ -71,11 +71,12 @@ export function generateBorrowBoxesRow(cellDimensions: CellDimensions): string {
|
||||
]
|
||||
)
|
||||
// Draw curved line using Typst bezier with control point
|
||||
// Note: path() is deprecated but curve() has different API - needs investigation
|
||||
#place(
|
||||
top + left,
|
||||
dx: ${arrowStartDx}in,
|
||||
dy: ${arrowStartDy}in,
|
||||
curve(
|
||||
path(
|
||||
stroke: (paint: gray.darken(30%), thickness: ${TYPST_CONSTANTS.ARROW_STROKE_WIDTH}pt),
|
||||
// Start vertex (near the "1" in borrow box)
|
||||
(0pt, 0pt),
|
||||
|
||||
@@ -85,13 +85,13 @@ export function generateSettingsSummary(config: Partial<WorksheetFormState>): {
|
||||
if (config.mode) {
|
||||
const diffIcon = SETTING_ICONS.difficulty[config.mode as keyof typeof SETTING_ICONS.difficulty]
|
||||
const modeName =
|
||||
config.mode === 'smart'
|
||||
? 'Smart difficulty'
|
||||
config.mode === 'custom'
|
||||
? 'Custom difficulty'
|
||||
: config.mode === 'mastery'
|
||||
? 'Mastery mode'
|
||||
: 'Manual mode'
|
||||
const pStart =
|
||||
config.mode === 'smart' && config.pAnyStart != null
|
||||
config.mode === 'custom' && config.pAnyStart != null
|
||||
? ` • ${Math.round(config.pAnyStart * 100)}% starts`
|
||||
: ''
|
||||
lines.push(`${diffIcon} ${modeName}${pStart}`)
|
||||
|
||||
@@ -184,8 +184,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
|
||||
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape')
|
||||
|
||||
// Determine mode (default to 'smart' if not specified)
|
||||
const mode: 'smart' | 'manual' | 'mastery' = formState.mode ?? 'smart'
|
||||
// Determine mode (default to 'custom' if not specified)
|
||||
const mode: 'custom' | 'manual' | 'mastery' = formState.mode ?? 'custom'
|
||||
|
||||
// Shared fields for both modes
|
||||
const sharedFields = {
|
||||
@@ -204,7 +204,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
date: formState.date?.trim() || getDefaultDate(),
|
||||
pAnyStart,
|
||||
pAllStart,
|
||||
// Default interpolate based on mode: true for smart/manual, false for mastery
|
||||
// Default interpolate based on mode: true for custom/manual, false for mastery
|
||||
interpolate:
|
||||
formState.interpolate !== undefined
|
||||
? formState.interpolate
|
||||
@@ -238,8 +238,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
// Build mode-specific config
|
||||
let config: WorksheetConfig
|
||||
|
||||
if (mode === 'smart' || mode === 'mastery') {
|
||||
// Smart & Mastery modes: Use displayRules for conditional scaffolding
|
||||
if (mode === 'custom' || mode === 'mastery') {
|
||||
// Custom & Mastery modes: Use displayRules for conditional scaffolding
|
||||
|
||||
// Default display rules
|
||||
let baseDisplayRules: DisplayRules = {
|
||||
@@ -295,7 +295,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
? mergeDisplayRulesWithAuto(baseDisplayRules, userDisplayRules)
|
||||
: {
|
||||
...baseDisplayRules,
|
||||
...userDisplayRules, // Smart mode: direct override (no "auto" resolution)
|
||||
...userDisplayRules, // Custom mode: direct override (no "auto" resolution)
|
||||
}
|
||||
|
||||
console.log('[MASTERY MODE] Display rules resolved:', {
|
||||
@@ -309,7 +309,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
const operator = formState.operator ?? 'addition'
|
||||
const baseConfig = {
|
||||
version: 4,
|
||||
mode: mode as 'smart' | 'mastery', // Preserve the actual mode
|
||||
mode: mode as 'custom' | 'mastery', // Preserve the actual mode
|
||||
displayRules,
|
||||
difficultyProfile: formState.difficultyProfile,
|
||||
currentStepId: formState.currentStepId, // Mastery progression tracking
|
||||
@@ -376,7 +376,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
config = baseConfig as any
|
||||
}
|
||||
} else {
|
||||
// Manual mode: Use displayRules (same as Smart/Mastery)
|
||||
// Manual mode: Use displayRules (same as Custom/Mastery)
|
||||
const displayRules: DisplayRules = formState.displayRules ?? {
|
||||
carryBoxes: 'always',
|
||||
answerBoxes: 'always',
|
||||
|
||||
Reference in New Issue
Block a user