feat(worksheets): add double-digit addition worksheet creator
- Problem generator with tunable difficulty (regrouping percentages) - Progressive difficulty option (easy → hard) - Mulberry32 PRNG for reproducible problem sets - Validation and type definitions - Main page component with layout configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d8b4951d63
commit
1a75213df0
|
|
@ -0,0 +1,337 @@
|
|||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { 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 './components/ConfigPanel'
|
||||
import { WorksheetPreview } from './components/WorksheetPreview'
|
||||
import type { WorksheetFormState } from './types'
|
||||
import { validateWorksheetConfig } from './validation'
|
||||
|
||||
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',
|
||||
})
|
||||
}
|
||||
|
||||
export default function AdditionWorksheetPage() {
|
||||
const t = useTranslations('create.worksheets.addition')
|
||||
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Immediate form state (for controls - updates instantly)
|
||||
const [formState, setFormState] = useState<WorksheetFormState>({
|
||||
total: 20,
|
||||
cols: 5,
|
||||
rows: 4,
|
||||
name: '',
|
||||
date: '', // Will be set at generation time
|
||||
pAnyStart: 0.75,
|
||||
pAllStart: 0.25,
|
||||
interpolate: true,
|
||||
showCarryBoxes: true,
|
||||
showCellBorder: true,
|
||||
fontSize: 16,
|
||||
seed: Date.now() % 2147483647,
|
||||
orientation: 'landscape',
|
||||
})
|
||||
|
||||
// Debounced form state (for preview - updates after delay)
|
||||
const [debouncedFormState, setDebouncedFormState] = useState<WorksheetFormState>(formState)
|
||||
|
||||
// Debounce preview updates (500ms delay)
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedFormState(formState)
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [formState])
|
||||
|
||||
const handleFormChange = (updates: Partial<WorksheetFormState>) => {
|
||||
setFormState((prev) => {
|
||||
const newState = { ...prev, ...updates }
|
||||
|
||||
// Generate new seed when problem settings change
|
||||
const affectsProblems =
|
||||
updates.total !== undefined ||
|
||||
updates.cols !== undefined ||
|
||||
updates.rows !== undefined ||
|
||||
updates.orientation !== undefined ||
|
||||
updates.pAnyStart !== undefined ||
|
||||
updates.pAllStart !== undefined ||
|
||||
updates.interpolate !== undefined
|
||||
|
||||
if (affectsProblems) {
|
||||
newState.seed = Date.now() % 2147483647
|
||||
}
|
||||
|
||||
return newState
|
||||
})
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerationStatus('generating')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Set current date at generation time
|
||||
const configWithDate = {
|
||||
...formState,
|
||||
date: getDefaultDate(),
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
const validation = validateWorksheetConfig(configWithDate)
|
||||
if (!validation.isValid || !validation.config) {
|
||||
throw new Error(validation.errors?.join(', ') || 'Invalid configuration')
|
||||
}
|
||||
|
||||
const response = await fetch('/api/create/worksheets/addition', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(configWithDate),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorResult = await response.json()
|
||||
const errorMsg = errorResult.details
|
||||
? `${errorResult.error}\n\n${errorResult.details}`
|
||||
: 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`
|
||||
|
||||
// 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)
|
||||
|
||||
setGenerationStatus('idle')
|
||||
} catch (err) {
|
||||
console.error('Generation error:', err)
|
||||
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
||||
setGenerationStatus('error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewGeneration = () => {
|
||||
setGenerationStatus('idle')
|
||||
setError(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle={t('navTitle')} navEmoji="📝">
|
||||
<div
|
||||
data-component="addition-worksheet-page"
|
||||
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' })}>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.900',
|
||||
})}
|
||||
>
|
||||
{t('pageTitle')}
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
color: 'gray.600',
|
||||
})}
|
||||
>
|
||||
{t('pageSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration Interface */}
|
||||
<div
|
||||
className={grid({
|
||||
columns: { base: 1, lg: 2 },
|
||||
gap: '8',
|
||||
alignItems: 'start',
|
||||
})}
|
||||
>
|
||||
{/* Configuration Panel */}
|
||||
<div
|
||||
data-section="config-panel"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '8',
|
||||
})}
|
||||
>
|
||||
<ConfigPanel formState={formState} onChange={handleFormChange} />
|
||||
</div>
|
||||
|
||||
{/* Preview & Generate Panel */}
|
||||
<div className={stack({ gap: '8' })}>
|
||||
{/* Preview */}
|
||||
<div
|
||||
data-section="preview-panel"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<WorksheetPreview formState={debouncedFormState} />
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<div
|
||||
data-section="generate-panel"
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: '2xl',
|
||||
shadow: 'card',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
data-action="generate-worksheet"
|
||||
onClick={handleGenerate}
|
||||
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',
|
||||
_hover:
|
||||
generationStatus === 'generating'
|
||||
? {}
|
||||
: {
|
||||
bg: 'brand.700',
|
||||
transform: 'translateY(-1px)',
|
||||
shadow: 'modal',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<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',
|
||||
})}
|
||||
/>
|
||||
{t('generate.generating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={css({ fontSize: 'xl' })}>📝</div>
|
||||
{t('generate.button')}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{generationStatus === 'error' && error && (
|
||||
<div
|
||||
data-status="error"
|
||||
className={css({
|
||||
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>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'semibold',
|
||||
color: 'red.800',
|
||||
})}
|
||||
>
|
||||
{t('error.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<pre
|
||||
className={css({
|
||||
color: 'red.700',
|
||||
lineHeight: 'relaxed',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'mono',
|
||||
fontSize: 'sm',
|
||||
overflowX: 'auto',
|
||||
})}
|
||||
>
|
||||
{error}
|
||||
</pre>
|
||||
<button
|
||||
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' },
|
||||
})}
|
||||
>
|
||||
{t('error.tryAgain')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
// Problem generation logic for double-digit addition worksheets
|
||||
|
||||
import type { AdditionProblem, ProblemCategory } from './types'
|
||||
|
||||
/**
|
||||
* Mulberry32 PRNG for reproducible random number generation
|
||||
*/
|
||||
export function createPRNG(seed: number) {
|
||||
let state = seed
|
||||
return function rand(): number {
|
||||
let t = (state += 0x6d2b79f5)
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1)
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random element from an array
|
||||
*/
|
||||
function pick<T>(arr: T[], rand: () => number): T {
|
||||
return arr[Math.floor(rand() * arr.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random integer between min and max (inclusive)
|
||||
*/
|
||||
function randint(min: number, max: number, rand: () => number): number {
|
||||
return Math.floor(rand() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random two-digit number (10-99)
|
||||
*/
|
||||
function twoDigit(rand: () => number): number {
|
||||
const tens = randint(1, 9, rand)
|
||||
const ones = randint(0, 9, rand)
|
||||
return tens * 10 + ones
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with NO regrouping
|
||||
* (ones sum < 10 AND tens sum < 10)
|
||||
*/
|
||||
export function generateNonRegroup(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO < 10 && aT + bT < 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [12, 34]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with regrouping in ONES only
|
||||
* (ones sum >= 10 AND tens sum + carry < 10)
|
||||
*/
|
||||
export function generateOnesOnly(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO >= 10 && aT + bT + 1 < 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [58, 31]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a problem with regrouping in BOTH ones and tens
|
||||
* (ones sum >= 10 AND tens sum + carry >= 10)
|
||||
*/
|
||||
export function generateBoth(rand: () => number): [number, number] {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
const aT = Math.floor((a % 100) / 10)
|
||||
const aO = a % 10
|
||||
const bT = Math.floor((b % 100) / 10)
|
||||
const bO = b % 10
|
||||
|
||||
if (aO + bO >= 10 && aT + bT + 1 >= 10) {
|
||||
return [a, b]
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
return [68, 47]
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to add a unique problem to the list
|
||||
* Returns true if added, false if duplicate
|
||||
*/
|
||||
function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set<string>): boolean {
|
||||
const key = [Math.min(a, b), Math.max(a, b)].join('+')
|
||||
if (seen.has(key) || a === b) {
|
||||
return false
|
||||
}
|
||||
seen.add(key)
|
||||
list.push({ a, b })
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a complete set of problems based on difficulty parameters
|
||||
*/
|
||||
export function generateProblems(
|
||||
total: number,
|
||||
pAnyStart: number,
|
||||
pAllStart: number,
|
||||
interpolate: boolean,
|
||||
seed: number
|
||||
): AdditionProblem[] {
|
||||
const rand = createPRNG(seed)
|
||||
const problems: AdditionProblem[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
// Calculate position from start (0) to end (1)
|
||||
const frac = total <= 1 ? 0 : i / (total - 1)
|
||||
// Progressive difficulty: start easy, end hard
|
||||
const difficultyMultiplier = interpolate ? frac : 1.0
|
||||
|
||||
// Effective probabilities at this position
|
||||
const pAll = Math.max(0, Math.min(1, pAllStart * difficultyMultiplier))
|
||||
const pAny = Math.max(0, Math.min(1, pAnyStart * difficultyMultiplier))
|
||||
const pOnesOnly = Math.max(0, pAny - pAll)
|
||||
const pNon = Math.max(0, 1 - pAny)
|
||||
|
||||
// Sample category based on probabilities
|
||||
const r = rand()
|
||||
let picked: ProblemCategory
|
||||
if (r < pAll) {
|
||||
picked = 'both'
|
||||
} else if (r < pAll + pOnesOnly) {
|
||||
picked = 'onesOnly'
|
||||
} else {
|
||||
picked = 'non'
|
||||
}
|
||||
|
||||
// Generate problem with retries for uniqueness
|
||||
let tries = 0
|
||||
let ok = false
|
||||
while (tries++ < 3000 && !ok) {
|
||||
let a: number, b: number
|
||||
if (picked === 'both') {
|
||||
;[a, b] = generateBoth(rand)
|
||||
} else if (picked === 'onesOnly') {
|
||||
;[a, b] = generateOnesOnly(rand)
|
||||
} else {
|
||||
;[a, b] = generateNonRegroup(rand)
|
||||
}
|
||||
ok = uniquePush(problems, a, b, seen)
|
||||
|
||||
// If stuck, try a different category
|
||||
if (!ok && tries % 50 === 0) {
|
||||
picked = pick(['both', 'onesOnly', 'non'], rand)
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: add any valid two-digit problem
|
||||
if (!ok) {
|
||||
const a = twoDigit(rand)
|
||||
const b = twoDigit(rand)
|
||||
uniquePush(problems, a, b, seen)
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Type definitions for double-digit addition worksheet creator
|
||||
|
||||
/**
|
||||
* Complete, validated configuration for worksheet generation
|
||||
* All fields have concrete values (no undefined/null)
|
||||
*/
|
||||
export interface WorksheetConfig {
|
||||
// Problem set
|
||||
total: number
|
||||
cols: number
|
||||
rows: number
|
||||
|
||||
// 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
|
||||
|
||||
// Layout
|
||||
page: {
|
||||
wIn: number
|
||||
hIn: number
|
||||
}
|
||||
margins: {
|
||||
left: number
|
||||
right: number
|
||||
top: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
// Display options
|
||||
showCarryBoxes: boolean
|
||||
showCellBorder: boolean
|
||||
fontSize: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial form state - user may be editing, fields optional
|
||||
*/
|
||||
export interface WorksheetFormState {
|
||||
total?: number
|
||||
cols?: number
|
||||
rows?: number
|
||||
name?: string
|
||||
date?: string
|
||||
pAnyStart?: number
|
||||
pAllStart?: number
|
||||
interpolate?: boolean
|
||||
showCarryBoxes?: boolean
|
||||
showCellBorder?: boolean
|
||||
fontSize?: number
|
||||
seed?: number
|
||||
orientation?: 'portrait' | 'landscape'
|
||||
}
|
||||
|
||||
/**
|
||||
* A single addition problem
|
||||
*/
|
||||
export interface AdditionProblem {
|
||||
a: number
|
||||
b: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation result
|
||||
*/
|
||||
export interface ValidationResult {
|
||||
isValid: boolean
|
||||
config?: WorksheetConfig
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Problem category for difficulty control
|
||||
*/
|
||||
export type ProblemCategory = 'non' | 'onesOnly' | 'both'
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// Validation logic for worksheet configuration
|
||||
|
||||
import type { WorksheetFormState, WorksheetConfig, ValidationResult } from './types'
|
||||
|
||||
/**
|
||||
* 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',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and create complete config from partial form state
|
||||
*/
|
||||
export function validateWorksheetConfig(formState: WorksheetFormState): ValidationResult {
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate total (must be positive, reasonable limit)
|
||||
const total = formState.total ?? 20
|
||||
if (total < 1 || total > 100) {
|
||||
errors.push('Total problems must be between 1 and 100')
|
||||
}
|
||||
|
||||
// Validate cols and auto-calculate rows
|
||||
const cols = formState.cols ?? 4
|
||||
if (cols < 1 || cols > 10) {
|
||||
errors.push('Columns must be between 1 and 10')
|
||||
}
|
||||
|
||||
// Auto-calculate rows to fit all problems
|
||||
const rows = Math.ceil(total / cols)
|
||||
|
||||
// Validate probabilities (0-1 range)
|
||||
const pAnyStart = formState.pAnyStart ?? 0.75
|
||||
const pAllStart = formState.pAllStart ?? 0.25
|
||||
if (pAnyStart < 0 || pAnyStart > 1) {
|
||||
errors.push('pAnyStart must be between 0 and 1')
|
||||
}
|
||||
if (pAllStart < 0 || pAllStart > 1) {
|
||||
errors.push('pAllStart must be between 0 and 1')
|
||||
}
|
||||
if (pAllStart > pAnyStart) {
|
||||
errors.push('pAllStart cannot be greater than pAnyStart')
|
||||
}
|
||||
|
||||
// Validate fontSize
|
||||
const fontSize = formState.fontSize ?? 16
|
||||
if (fontSize < 8 || fontSize > 32) {
|
||||
errors.push('Font size must be between 8 and 32')
|
||||
}
|
||||
|
||||
// Validate seed (must be positive integer)
|
||||
const seed = formState.seed ?? Date.now() % 2147483647
|
||||
if (!Number.isInteger(seed) || seed < 0) {
|
||||
errors.push('Seed must be a non-negative integer')
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { isValid: false, errors }
|
||||
}
|
||||
|
||||
// Determine orientation based on columns (portrait = 2-3 cols, landscape = 4-5 cols)
|
||||
const orientation = formState.orientation || (cols <= 3 ? 'portrait' : 'landscape')
|
||||
|
||||
// Build complete config with defaults
|
||||
const config: WorksheetConfig = {
|
||||
total,
|
||||
cols,
|
||||
rows,
|
||||
name: formState.name?.trim() || 'Student',
|
||||
date: formState.date?.trim() || getDefaultDate(),
|
||||
pAnyStart,
|
||||
pAllStart,
|
||||
interpolate: formState.interpolate ?? true,
|
||||
page: {
|
||||
wIn: orientation === 'portrait' ? 8.5 : 11,
|
||||
hIn: orientation === 'portrait' ? 11 : 8.5,
|
||||
},
|
||||
margins: {
|
||||
left: 0.6,
|
||||
right: 0.6,
|
||||
top: 1.1,
|
||||
bottom: 0.7,
|
||||
},
|
||||
showCarryBoxes: formState.showCarryBoxes ?? true,
|
||||
showCellBorder: formState.showCellBorder ?? true,
|
||||
fontSize,
|
||||
seed,
|
||||
}
|
||||
|
||||
return { isValid: true, config }
|
||||
}
|
||||
Loading…
Reference in New Issue