feat(worksheets): implement constrained 2D difficulty system with pedagogical zones

Implement a novel constrained 2D pedagogical space for addition worksheet difficulty:

**Core Innovation:**
- Difficulty is 2D: Challenge (regrouping) vs Support (scaffolding)
- Diagonal constraint band enforces pedagogical principles
- Valid combinations only (prevents high challenge + high support, or low challenge + low support)

**Architecture:**
- Hybrid discrete/continuous system
  - 19 regrouping levels (indices 0-18)
  - 13 scaffolding levels (indices 0-12)
  - Indices for navigation, scores for visualization
- Pedagogical constraints auto-enforced at every step
- findNearestValidState() ensures valid difficulty combinations

**Movement Modes:**
- 'both': Smart diagonal navigation (default)
- 'challenge': Horizontal - adjust problem complexity only
- 'support': Vertical - adjust scaffolding only

**UI Features:**
- Split buttons with dropdown for mode selection
- Live preview of what will change before clicking
- Clickable 2D graph for debugging (can jump to any valid difficulty)
- Automatic preset detection when navigating

**Theoretical Foundation:**
- Zone of Proximal Development (Vygotsky)
- Cognitive Load Theory (Sweller)
- Scaffolding Fading (Wood, Bruner, Ross)

Files:
- difficultyProfiles.ts: Core system with constraint functions
- ConfigPanel.tsx: Split button UI + clickable debug graph
- AdditionWorksheetClient.tsx: Form state management
- types.ts: V2 type definitions

🤖 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-07 06:25:19 -06:00
parent 50c405c317
commit c39b7f6d3a
4 changed files with 2525 additions and 1238 deletions

View File

@@ -1,59 +1,65 @@
'use client'
"use client";
import { useTranslations } from 'next-intl'
import React, { useState, useEffect } from 'react'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../../../../styled-system/css'
import { container, grid, hstack, stack } from '../../../../../../styled-system/patterns'
import { ConfigPanel } from './ConfigPanel'
import { WorksheetPreview } from './WorksheetPreview'
import type { WorksheetFormState } from '../types'
import { validateWorksheetConfig } from '../validation'
import type { DisplayExamples } from '../generateExamples'
import { useTranslations } from "next-intl";
import React, { useState, useEffect } from "react";
import { PageWithNav } from "@/components/PageWithNav";
import { css } from "../../../../../../styled-system/css";
import {
container,
grid,
hstack,
stack,
} from "../../../../../../styled-system/patterns";
import { ConfigPanel } from "./ConfigPanel";
import { WorksheetPreview } from "./WorksheetPreview";
import type { WorksheetFormState } from "../types";
import { validateWorksheetConfig } from "../validation";
type GenerationStatus = 'idle' | 'generating' | 'error'
type GenerationStatus = "idle" | "generating" | "error";
/**
* Get current date formatted as "Month Day, Year"
*/
function getDefaultDate(): string {
const now = new Date()
return now.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
})
const now = new Date();
return now.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
});
}
interface AdditionWorksheetClientProps {
initialSettings: Omit<WorksheetFormState, 'date' | 'rows' | 'total'>
initialPreview?: string[]
displayExamples?: DisplayExamples
initialSettings: Omit<WorksheetFormState, "date" | "rows" | "total">;
initialPreview?: string[];
}
export function AdditionWorksheetClient({
initialSettings,
initialPreview,
displayExamples,
}: AdditionWorksheetClientProps) {
console.log('[Worksheet Client] Component render, initialSettings:', {
console.log("[Worksheet Client] Component render, initialSettings:", {
problemsPerPage: initialSettings.problemsPerPage,
cols: initialSettings.cols,
pages: initialSettings.pages,
seed: initialSettings.seed,
})
});
const t = useTranslations('create.worksheets.addition')
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
const [error, setError] = useState<string | null>(null)
const [lastSaved, setLastSaved] = useState<Date | null>(null)
const [isSaving, setIsSaving] = useState(false)
const t = useTranslations("create.worksheets.addition");
const [generationStatus, setGenerationStatus] =
useState<GenerationStatus>("idle");
const [error, setError] = useState<string | null>(null);
const [lastSaved, setLastSaved] = useState<Date | null>(null);
const [isSaving, setIsSaving] = useState(false);
// Calculate derived state from initial settings
const rows = Math.ceil(
(initialSettings.problemsPerPage * initialSettings.pages) / initialSettings.cols
)
const total = initialSettings.problemsPerPage * initialSettings.pages
// Use defaults for required fields (server should always provide these, but TypeScript needs guarantees)
const problemsPerPage = initialSettings.problemsPerPage ?? 20;
const pages = initialSettings.pages ?? 1;
const cols = initialSettings.cols ?? 5;
const rows = Math.ceil((problemsPerPage * pages) / cols);
const total = problemsPerPage * pages;
// Immediate form state (for controls - updates instantly)
const [formState, setFormState] = useState<WorksheetFormState>(() => {
@@ -61,74 +67,91 @@ export function AdditionWorksheetClient({
...initialSettings,
rows,
total,
date: '', // Will be set at generation time
date: "", // Will be set at generation time
// seed comes from initialSettings (server-generated, stable across StrictMode remounts)
}
console.log('[Worksheet Client] Initial formState:', { seed: initial.seed })
return initial
})
};
console.log("[Worksheet Client] Initial formState:", {
seed: initial.seed,
});
return initial;
});
// Debounced form state (for preview - updates after delay)
const [debouncedFormState, setDebouncedFormState] = useState<WorksheetFormState>(() => {
console.log('[Worksheet Client] Initial debouncedFormState (same as formState)')
return formState
})
const [debouncedFormState, setDebouncedFormState] =
useState<WorksheetFormState>(() => {
console.log(
"[Worksheet Client] Initial debouncedFormState (same as formState)",
);
return formState;
});
// Store the previous formState to detect real changes
const prevFormStateRef = React.useRef(formState)
const prevFormStateRef = React.useRef(formState);
// Log whenever debouncedFormState changes (this triggers preview re-fetch)
useEffect(() => {
console.log('[Worksheet Client] debouncedFormState changed - preview will re-fetch:', {
seed: debouncedFormState.seed,
problemsPerPage: debouncedFormState.problemsPerPage,
})
}, [debouncedFormState])
console.log(
"[Worksheet Client] debouncedFormState changed - preview will re-fetch:",
{
seed: debouncedFormState.seed,
problemsPerPage: debouncedFormState.problemsPerPage,
},
);
}, [debouncedFormState]);
// Debounce preview updates (500ms delay) - only when formState actually changes
useEffect(() => {
console.log('[Debounce Effect] Triggered')
console.log('[Debounce Effect] Current formState seed:', formState.seed)
console.log('[Debounce Effect] Previous formState seed:', prevFormStateRef.current.seed)
console.log("[Debounce Effect] Triggered");
console.log("[Debounce Effect] Current formState seed:", formState.seed);
console.log(
"[Debounce Effect] Previous formState seed:",
prevFormStateRef.current.seed,
);
// Skip if formState hasn't actually changed (handles StrictMode double-render)
if (formState === prevFormStateRef.current) {
console.log('[Debounce Effect] Skipping - formState reference unchanged')
return
console.log("[Debounce Effect] Skipping - formState reference unchanged");
return;
}
prevFormStateRef.current = formState
prevFormStateRef.current = formState;
console.log('[Debounce Effect] Setting timer to update debouncedFormState in 500ms')
console.log(
"[Debounce Effect] Setting timer to update debouncedFormState in 500ms",
);
const timer = setTimeout(() => {
console.log('[Debounce Effect] Timer fired - updating debouncedFormState')
setDebouncedFormState(formState)
}, 500)
console.log(
"[Debounce Effect] Timer fired - updating debouncedFormState",
);
setDebouncedFormState(formState);
}, 500);
return () => {
console.log('[Debounce Effect] Cleanup - clearing timer')
clearTimeout(timer)
}
}, [formState])
console.log("[Debounce Effect] Cleanup - clearing timer");
clearTimeout(timer);
};
}, [formState]);
// Store the previous formState for auto-save to detect real changes
const prevAutoSaveFormStateRef = React.useRef(formState)
const prevAutoSaveFormStateRef = React.useRef(formState);
// Auto-save settings when they change (debounced) - skip on initial mount
useEffect(() => {
// Skip auto-save if formState hasn't actually changed (handles StrictMode double-render)
if (formState === prevAutoSaveFormStateRef.current) {
console.log('[Worksheet Settings] Skipping auto-save - formState reference unchanged')
return
console.log(
"[Worksheet Settings] Skipping auto-save - formState reference unchanged",
);
return;
}
prevAutoSaveFormStateRef.current = formState
prevAutoSaveFormStateRef.current = formState;
console.log('[Worksheet Settings] Settings changed, will save in 1s...')
console.log("[Worksheet Settings] Settings changed, will save in 1s...");
const timer = setTimeout(async () => {
console.log('[Worksheet Settings] Attempting to save settings...')
setIsSaving(true)
console.log("[Worksheet Settings] Attempting to save settings...");
setIsSaving(true);
try {
// Extract only the fields we want to persist (exclude date, seed, derived state)
const {
@@ -148,13 +171,13 @@ export function AdditionWorksheetClient({
showTenFrames,
showTenFramesForAll,
fontSize,
} = formState
} = formState;
const response = await fetch('/api/worksheets/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
const response = await fetch("/api/worksheets/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: 'addition',
type: "addition",
config: {
problemsPerPage,
cols,
@@ -174,34 +197,37 @@ export function AdditionWorksheetClient({
fontSize,
},
}),
})
});
if (response.ok) {
const data = await response.json()
console.log('[Worksheet Settings] Save response:', data)
const data = await response.json();
console.log("[Worksheet Settings] Save response:", data);
if (data.success) {
console.log('[Worksheet Settings] ✓ Settings saved successfully')
setLastSaved(new Date())
console.log("[Worksheet Settings] ✓ Settings saved successfully");
setLastSaved(new Date());
} else {
console.log('[Worksheet Settings] Save skipped')
console.log("[Worksheet Settings] Save skipped");
}
} else {
console.error('[Worksheet Settings] Save failed with status:', response.status)
console.error(
"[Worksheet Settings] Save failed with status:",
response.status,
);
}
} catch (error) {
// Silently fail - settings persistence is not critical
console.error('[Worksheet Settings] Settings save error:', error)
console.error("[Worksheet Settings] Settings save error:", error);
} finally {
setIsSaving(false)
setIsSaving(false);
}
}, 1000) // 1 second debounce for auto-save
}, 1000); // 1 second debounce for auto-save
return () => clearTimeout(timer)
}, [formState])
return () => clearTimeout(timer);
}, [formState]);
const handleFormChange = (updates: Partial<WorksheetFormState>) => {
setFormState((prev) => {
const newState = { ...prev, ...updates }
const newState = { ...prev, ...updates };
// Generate new seed when problem settings change
const affectsProblems =
@@ -211,103 +237,105 @@ export function AdditionWorksheetClient({
updates.orientation !== undefined ||
updates.pAnyStart !== undefined ||
updates.pAllStart !== undefined ||
updates.interpolate !== undefined
updates.interpolate !== undefined;
if (affectsProblems) {
newState.seed = Date.now() % 2147483647
newState.seed = Date.now() % 2147483647;
}
return newState
})
}
return newState;
});
};
const handleGenerate = async () => {
setGenerationStatus('generating')
setError(null)
setGenerationStatus("generating");
setError(null);
try {
// Set current date at generation time
const configWithDate = {
...formState,
date: getDefaultDate(),
}
};
// Validate configuration
const validation = validateWorksheetConfig(configWithDate)
const validation = validateWorksheetConfig(configWithDate);
if (!validation.isValid || !validation.config) {
throw new Error(validation.errors?.join(', ') || 'Invalid configuration')
throw new Error(
validation.errors?.join(", ") || "Invalid configuration",
);
}
const response = await fetch('/api/create/worksheets/addition', {
method: 'POST',
const response = await fetch("/api/create/worksheets/addition", {
method: "POST",
headers: {
'Content-Type': 'application/json',
"Content-Type": "application/json",
},
body: JSON.stringify(configWithDate),
})
});
if (!response.ok) {
const errorResult = await response.json()
const errorResult = await response.json();
const errorMsg = errorResult.details
? `${errorResult.error}\n\n${errorResult.details}`
: errorResult.error || 'Generation failed'
throw new Error(errorMsg)
: errorResult.error || "Generation failed";
throw new Error(errorMsg);
}
// Success - response is binary PDF data, trigger download
const blob = await response.blob()
const filename = `addition-worksheet-${formState.name || 'student'}-${Date.now()}.pdf`
const blob = await response.blob();
const filename = `addition-worksheet-${formState.name || "student"}-${Date.now()}.pdf`;
// Create download link and trigger download
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
setGenerationStatus('idle')
setGenerationStatus("idle");
} catch (err) {
console.error('Generation error:', err)
setError(err instanceof Error ? err.message : 'Unknown error occurred')
setGenerationStatus('error')
console.error("Generation error:", err);
setError(err instanceof Error ? err.message : "Unknown error occurred");
setGenerationStatus("error");
}
}
};
const handleNewGeneration = () => {
setGenerationStatus('idle')
setError(null)
}
setGenerationStatus("idle");
setError(null);
};
return (
<PageWithNav navTitle={t('navTitle')} navEmoji="📝">
<PageWithNav navTitle={t("navTitle")} navEmoji="📝">
<div
data-component="addition-worksheet-page"
className={css({ minHeight: '100vh', bg: 'gray.50' })}
className={css({ minHeight: "100vh", bg: "gray.50" })}
>
{/* Main Content */}
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
<div className={stack({ gap: '6', mb: '8' })}>
<div className={stack({ gap: '2', textAlign: 'center' })}>
<div className={container({ maxW: "7xl", px: "4", py: "8" })}>
<div className={stack({ gap: "6", mb: "8" })}>
<div className={stack({ gap: "2", textAlign: "center" })}>
<h1
className={css({
fontSize: '3xl',
fontWeight: 'bold',
color: 'gray.900',
fontSize: "3xl",
fontWeight: "bold",
color: "gray.900",
})}
>
{t('pageTitle')}
{t("pageTitle")}
</h1>
<p
className={css({
fontSize: 'lg',
color: 'gray.600',
fontSize: "lg",
color: "gray.600",
})}
>
{t('pageSubtitle')}
{t("pageSubtitle")}
</p>
</div>
</div>
@@ -316,25 +344,24 @@ export function AdditionWorksheetClient({
<div
className={grid({
columns: { base: 1, lg: 2 },
gap: '8',
alignItems: 'start',
gap: "8",
alignItems: "start",
})}
>
{/* Configuration Panel */}
<div className={stack({ gap: '3' })}>
<div className={stack({ gap: "3" })}>
<div
data-section="config-panel"
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '8',
bg: "white",
rounded: "2xl",
shadow: "card",
p: "8",
})}
>
<ConfigPanel
formState={formState}
onChange={handleFormChange}
displayExamples={displayExamples}
/>
</div>
@@ -342,16 +369,18 @@ export function AdditionWorksheetClient({
<div
data-element="settings-status"
className={css({
fontSize: 'sm',
color: 'gray.600',
textAlign: 'center',
py: '2',
fontSize: "sm",
color: "gray.600",
textAlign: "center",
py: "2",
})}
>
{isSaving ? (
<span className={css({ color: 'gray.500' })}>Saving settings...</span>
<span className={css({ color: "gray.500" })}>
Saving settings...
</span>
) : lastSaved ? (
<span className={css({ color: 'green.600' })}>
<span className={css({ color: "green.600" })}>
Settings saved at {lastSaved.toLocaleTimeString()}
</span>
) : null}
@@ -359,77 +388,83 @@ export function AdditionWorksheetClient({
</div>
{/* Preview & Generate Panel */}
<div className={stack({ gap: '8' })}>
<div className={stack({ gap: "8" })}>
{/* Preview */}
<div
data-section="preview-panel"
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
bg: "white",
rounded: "2xl",
shadow: "card",
p: "6",
})}
>
<WorksheetPreview formState={debouncedFormState} initialData={initialPreview} />
<WorksheetPreview
formState={debouncedFormState}
initialData={initialPreview}
/>
</div>
{/* Generate Button */}
<div
data-section="generate-panel"
className={css({
bg: 'white',
rounded: '2xl',
shadow: 'card',
p: '6',
bg: "white",
rounded: "2xl",
shadow: "card",
p: "6",
})}
>
<button
data-action="generate-worksheet"
onClick={handleGenerate}
disabled={generationStatus === 'generating'}
disabled={generationStatus === "generating"}
className={css({
w: 'full',
px: '6',
py: '4',
bg: 'brand.600',
color: 'white',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
cursor: generationStatus === 'generating' ? 'not-allowed' : 'pointer',
opacity: generationStatus === 'generating' ? '0.7' : '1',
w: "full",
px: "6",
py: "4",
bg: "brand.600",
color: "white",
fontSize: "lg",
fontWeight: "semibold",
rounded: "xl",
shadow: "card",
transition: "all",
cursor:
generationStatus === "generating"
? "not-allowed"
: "pointer",
opacity: generationStatus === "generating" ? "0.7" : "1",
_hover:
generationStatus === 'generating'
generationStatus === "generating"
? {}
: {
bg: 'brand.700',
transform: 'translateY(-1px)',
shadow: 'modal',
bg: "brand.700",
transform: "translateY(-1px)",
shadow: "modal",
},
})}
>
<span className={hstack({ gap: '3', justify: 'center' })}>
{generationStatus === 'generating' ? (
<span className={hstack({ gap: "3", justify: "center" })}>
{generationStatus === "generating" ? (
<>
<div
className={css({
w: '5',
h: '5',
border: '2px solid',
borderColor: 'white',
borderTopColor: 'transparent',
rounded: 'full',
animation: 'spin 1s linear infinite',
w: "5",
h: "5",
border: "2px solid",
borderColor: "white",
borderTopColor: "transparent",
rounded: "full",
animation: "spin 1s linear infinite",
})}
/>
{t('generate.generating')}
{t("generate.generating")}
</>
) : (
<>
<div className={css({ fontSize: 'xl' })}>📝</div>
{t('generate.button')}
<div className={css({ fontSize: "xl" })}>📝</div>
{t("generate.button")}
</>
)}
</span>
@@ -439,39 +474,39 @@ export function AdditionWorksheetClient({
</div>
{/* Error Display */}
{generationStatus === 'error' && error && (
{generationStatus === "error" && error && (
<div
data-status="error"
className={css({
bg: 'red.50',
border: '1px solid',
borderColor: 'red.200',
rounded: '2xl',
p: '8',
mt: '8',
bg: "red.50",
border: "1px solid",
borderColor: "red.200",
rounded: "2xl",
p: "8",
mt: "8",
})}
>
<div className={stack({ gap: '4' })}>
<div className={hstack({ gap: '3', alignItems: 'center' })}>
<div className={css({ fontSize: '2xl' })}></div>
<div className={stack({ gap: "4" })}>
<div className={hstack({ gap: "3", alignItems: "center" })}>
<div className={css({ fontSize: "2xl" })}></div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'semibold',
color: 'red.800',
fontSize: "xl",
fontWeight: "semibold",
color: "red.800",
})}
>
{t('error.title')}
{t("error.title")}
</h3>
</div>
<pre
className={css({
color: 'red.700',
lineHeight: 'relaxed',
whiteSpace: 'pre-wrap',
fontFamily: 'mono',
fontSize: 'sm',
overflowX: 'auto',
color: "red.700",
lineHeight: "relaxed",
whiteSpace: "pre-wrap",
fontFamily: "mono",
fontSize: "sm",
overflowX: "auto",
})}
>
{error}
@@ -480,18 +515,18 @@ export function AdditionWorksheetClient({
data-action="try-again"
onClick={handleNewGeneration}
className={css({
alignSelf: 'start',
px: '4',
py: '2',
bg: 'red.600',
color: 'white',
fontWeight: 'medium',
rounded: 'lg',
transition: 'all',
_hover: { bg: 'red.700' },
alignSelf: "start",
px: "4",
py: "2",
bg: "red.600",
color: "white",
fontWeight: "medium",
rounded: "lg",
transition: "all",
_hover: { bg: "red.700" },
})}
>
{t('error.tryAgain')}
{t("error.tryAgain")}
</button>
</div>
</div>
@@ -499,5 +534,5 @@ export function AdditionWorksheetClient({
</div>
</div>
</PageWithNav>
)
);
}

View File

@@ -1,103 +1,87 @@
// Type definitions for double-digit addition worksheet creator
import type { AdditionConfigV2 } from "../config-schemas";
/**
* Complete, validated configuration for worksheet generation
* All fields have concrete values (no undefined/null)
* Extends V2 config with additional derived fields needed for rendering
*
* Note: Includes V1 compatibility fields during migration period
*/
export interface WorksheetConfig {
// Problem set - PRIMARY state
problemsPerPage: number // Number of problems per page (6, 8, 10, 12, 15, 16, 20)
cols: number // Column count
pages: number // Number of pages
export type WorksheetConfig = AdditionConfigV2 & {
// Problem set - DERIVED state
total: number // total = problemsPerPage * pages
rows: number // rows = (problemsPerPage / cols) * pages
total: number; // total = problemsPerPage * pages
rows: number; // rows = (problemsPerPage / cols) * pages
// Personalization
name: string
date: string
// Difficulty controls
pAnyStart: number // Share of problems requiring any regrouping at start (0-1)
pAllStart: number // Share requiring both ones and tens regrouping at start (0-1)
interpolate: boolean // Whether to linearly decay difficulty across sheet
date: string;
seed: number;
// Layout
page: {
wIn: number
hIn: number
}
wIn: number;
hIn: number;
};
margins: {
left: number
right: number
top: number
bottom: number
}
left: number;
right: number;
top: number;
bottom: number;
};
// Display options
showCarryBoxes: boolean
showAnswerBoxes: boolean
showPlaceValueColors: boolean
showProblemNumbers: boolean
showCellBorder: boolean
showTenFrames: boolean // Show empty ten-frames
showTenFramesForAll: boolean // Show ten-frames for all place values (not just regrouping)
fontSize: number
seed: number
}
// V1 compatibility: Include individual boolean flags during migration
// These will be derived from displayRules during validation
showCarryBoxes: boolean;
showAnswerBoxes: boolean;
showPlaceValueColors: boolean;
showProblemNumbers: boolean;
showCellBorder: boolean;
showTenFrames: boolean;
};
/**
* Partial form state - user may be editing, fields optional
* PRIMARY state: problemsPerPage, cols, pages (what user controls)
* DERIVED state: rows, total (calculated from primary)
* Based on V2 config with additional derived state
*
* Note: For backwards compatibility during migration, this type accepts either:
* - V2 displayRules (preferred)
* - V1 individual boolean flags (will be converted to displayRules)
*/
export interface WorksheetFormState {
// PRIMARY state (what user selects in UI)
problemsPerPage?: number // 6, 8, 10, 12, 15, 16, 20
cols?: number // 2, 3, 4, 5 - column count for layout
pages?: number // 1, 2, 3, 4
orientation?: 'portrait' | 'landscape'
export type WorksheetFormState = Partial<Omit<AdditionConfigV2, "version">> & {
// DERIVED state (calculated from primary state)
rows?: number;
total?: number;
date?: string;
seed?: number;
// DERIVED state (calculated: rows = (problemsPerPage / cols) * pages, total = problemsPerPage * pages)
rows?: number
total?: number
// Other settings
name?: string
date?: string
pAnyStart?: number
pAllStart?: number
interpolate?: boolean
showCarryBoxes?: boolean
showAnswerBoxes?: boolean
showPlaceValueColors?: boolean
showProblemNumbers?: boolean
showCellBorder?: boolean
showTenFrames?: boolean
showTenFramesForAll?: boolean
fontSize?: number
seed?: number
}
// V1 compatibility: Accept individual boolean flags
// These will be converted to displayRules internally
showCarryBoxes?: boolean;
showAnswerBoxes?: boolean;
showPlaceValueColors?: boolean;
showProblemNumbers?: boolean;
showCellBorder?: boolean;
showTenFrames?: boolean;
};
/**
* A single addition problem
*/
export interface AdditionProblem {
a: number
b: number
a: number;
b: number;
}
/**
* Validation result
*/
export interface ValidationResult {
isValid: boolean
config?: WorksheetConfig
errors?: string[]
isValid: boolean;
config?: WorksheetConfig;
errors?: string[];
}
/**
* Problem category for difficulty control
*/
export type ProblemCategory = 'non' | 'onesOnly' | 'both'
export type ProblemCategory = "non" | "onesOnly" | "both";