feat(worksheets): Add borrow notation scaffolding for subtraction

Implements empty scratch boxes to guide students through borrowing work
without doing the arithmetic for them.

Scaffolding design:
1. **Source digit box** (above) - Student crosses out original and writes
   reduced value (e.g., cross out 5, write 4)
2. **Destination digit box** (left side) - Student writes modified value
   after adding 10 (e.g., write "12" next to 2)

Both boxes:
- Dotted borders to indicate workspace
- ~35-50% size of main digit cells
- Positioned close to the digits they modify
- Only appear where borrowing is needed

Implementation:
- Add `showBorrowNotation` boolean to V4 manual schema
- Update Typst rendering with conditional notation rows
- Add UI toggle (only shows for subtraction/mixed modes)
- Include in validation and auto-save persistence
- Update typstGenerator and example routes

Pedagogical approach: Shows WHERE and provides space for WHAT,
but student must determine and write the actual values.

🤖 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 22:58:31 -06:00
parent 13dfb128a2
commit ff161d4e30
7 changed files with 81 additions and 12 deletions

View File

@ -134,7 +134,7 @@ ${generateSubtractionProblemStackFunction(cellSize, 3)}
#let subtrahend = ${subtrahend}
#align(center + horizon)[
#subtraction-problem-stack(minuend, subtrahend, if show-numbers { 0 } else { none }, show-borrows, show-answers, show-colors, show-ten-frames, show-numbers)
#subtraction-problem-stack(minuend, subtrahend, if show-numbers { 0 } else { none }, show-borrows, show-answers, show-colors, show-ten-frames, show-numbers, false)
]
`
}

View File

@ -2285,6 +2285,15 @@ export function ConfigPanel({ formState, onChange }: ConfigPanelProps) {
description="Organize problems visually for easier focus"
/>
{formState.operator === 'subtraction' || formState.operator === 'mixed' ? (
<ToggleOption
checked={formState.showBorrowNotation ?? false}
onChange={(checked) => onChange({ showBorrowNotation: checked })}
label="Borrow Notation Boxes"
description="Empty scratch boxes for students to write borrowing work (cross out source, write modified values)"
/>
) : null}
<ToggleOption
checked={formState.showTenFrames ?? false}
onChange={(checked) => {

View File

@ -63,6 +63,7 @@ export function useWorksheetAutoSave(
showCellBorder,
showTenFrames,
showTenFramesForAll,
showBorrowNotation,
fontSize,
mode,
difficultyProfile,
@ -93,6 +94,7 @@ export function useWorksheetAutoSave(
showCellBorder,
showTenFrames,
showTenFramesForAll,
showBorrowNotation,
fontSize,
mode,
difficultyProfile,

View File

@ -108,6 +108,7 @@ function generatePageTypst(
showTenFrames: config.showTenFrames,
showProblemNumbers: config.showProblemNumbers,
showCellBorder: config.showCellBorder,
showBorrowNotation: 'showBorrowNotation' in config ? config.showBorrowNotation : false,
}
}
})
@ -116,9 +117,9 @@ function generatePageTypst(
const problemsTypst = enrichedProblems
.map((p) => {
if (p.operator === '+') {
return ` (operator: "+", a: ${p.a}, b: ${p.b}, showCarryBoxes: ${p.showCarryBoxes}, showAnswerBoxes: ${p.showAnswerBoxes}, showPlaceValueColors: ${p.showPlaceValueColors}, showTenFrames: ${p.showTenFrames}, showProblemNumbers: ${p.showProblemNumbers}, showCellBorder: ${p.showCellBorder}),`
return ` (operator: "+", a: ${p.a}, b: ${p.b}, showCarryBoxes: ${p.showCarryBoxes}, showAnswerBoxes: ${p.showAnswerBoxes}, showPlaceValueColors: ${p.showPlaceValueColors}, showTenFrames: ${p.showTenFrames}, showProblemNumbers: ${p.showProblemNumbers}, showCellBorder: ${p.showCellBorder}, showBorrowNotation: ${p.showBorrowNotation}),`
} else {
return ` (operator: "", minuend: ${p.minuend}, subtrahend: ${p.subtrahend}, showCarryBoxes: ${p.showCarryBoxes}, showAnswerBoxes: ${p.showAnswerBoxes}, showPlaceValueColors: ${p.showPlaceValueColors}, showTenFrames: ${p.showTenFrames}, showProblemNumbers: ${p.showProblemNumbers}, showCellBorder: ${p.showCellBorder}),`
return ` (operator: "", minuend: ${p.minuend}, subtrahend: ${p.subtrahend}, showCarryBoxes: ${p.showCarryBoxes}, showAnswerBoxes: ${p.showAnswerBoxes}, showPlaceValueColors: ${p.showPlaceValueColors}, showTenFrames: ${p.showTenFrames}, showProblemNumbers: ${p.showProblemNumbers}, showCellBorder: ${p.showCellBorder}, showBorrowNotation: ${p.showBorrowNotation}),`
}
})
.join('\n')
@ -214,7 +215,8 @@ ${generateSubtractionProblemStackFunction(cellSize, maxDigits)}
problem.showAnswerBoxes,
problem.showPlaceValueColors,
problem.showTenFrames,
problem.showProblemNumbers
problem.showProblemNumbers,
problem.showBorrowNotation
)
}
]

View File

@ -355,8 +355,8 @@ export function generateSubtractionProblemStackFunction(
return String.raw`
// Subtraction problem rendering function (supports 1-${maxDigits} digit problems)
// Returns the stack/grid structure for rendering a single subtraction problem
// Per-problem display flags: show-borrows, show-answers, show-colors, show-ten-frames, show-numbers
#let subtraction-problem-stack(minuend, subtrahend, index-or-none, show-borrows, show-answers, show-colors, show-ten-frames, show-numbers) = {
// Per-problem display flags: show-borrows, show-answers, show-colors, show-ten-frames, show-numbers, show-borrow-notation
#let subtraction-problem-stack(minuend, subtrahend, index-or-none, show-borrows, show-answers, show-colors, show-ten-frames, show-numbers, show-borrow-notation) = {
// Place value colors array for dynamic lookup
let place-colors = (${placeColors.join(', ')})
@ -450,20 +450,74 @@ export function generateSubtractionProblemStackFunction(
}
},
// Minuend row (top number)
// Borrow notation row (scratch boxes for student work)
..if show-borrow-notation {
(
[], // Empty cell for operator column
..for i in range(0, grid-digits).rev() {
// Check if this place needs borrowing FROM (source)
// We borrow FROM position i when position i-1 needs to borrow
let is-source = i > 0 and (m-digits.at(i - 1) < s-digits.at(i - 1))
if is-source and i <= m-highest {
// Small dotted box above this digit for student to write reduced value
(box(width: ${cellSizeIn}, height: ${cellSizeIn} * 0.4)[
#align(center + bottom)[
#box(
width: ${cellSizeIn} * 0.5,
height: ${cellSizeIn} * 0.35,
stroke: (dash: "dotted", thickness: 0.5pt, paint: gray)
)[]
]
],)
} else {
// No notation needed
(v(${cellSizeIn} * 0.4),)
}
},
)
} else {
()
},
// Minuend row (top number with optional borrow notation)
[], // Empty cell for operator column
..for i in range(0, grid-digits).rev() {
let digit = m-digits.at(i)
let place-color = place-colors.at(i)
let fill-color = if show-colors { place-color } else { color-none }
// Check if this place needs to borrow (destination)
let needs-borrow = i < grid-digits and (m-digits.at(i) < s-digits.at(i))
// Show digit if within minuend's actual range
if i <= m-highest {
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: fill-color)[
#align(center + horizon)[
#text(size: ${cellSizePt.toFixed(1)}pt, font: "New Computer Modern Math")[#str(digit)]
]
],)
if show-borrow-notation and needs-borrow {
// Show digit with small scratch box to the left for modified value (e.g., "12")
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: fill-color)[
#stack(
dir: ltr,
spacing: 2pt,
// Small dotted box for student to write modified digit
box(
width: ${cellSizeIn} * 0.35,
height: ${cellSizeIn} * 0.35,
stroke: (dash: "dotted", thickness: 0.5pt, paint: gray)
)[],
// Original digit
align(center + horizon)[
#text(size: ${cellSizePt.toFixed(1)}pt, font: "New Computer Modern Math")[#str(digit)]
]
)
],)
} else {
// Normal digit display
(box(width: ${cellSizeIn}, height: ${cellSizeIn}, fill: fill-color)[
#align(center + horizon)[
#text(size: ${cellSizePt.toFixed(1)}pt, font: "New Computer Modern Math")[#str(digit)]
]
],)
}
} else {
// Leading zero position - don't show
(box(width: ${cellSizeIn}, height: ${cellSizeIn})[

View File

@ -168,6 +168,7 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
showProblemNumbers: formState.showProblemNumbers ?? true,
showCellBorder: formState.showCellBorder ?? true,
showTenFramesForAll: formState.showTenFramesForAll ?? false,
showBorrowNotation: formState.showBorrowNotation ?? false,
manualPreset: formState.manualPreset,
...sharedFields,
}

View File

@ -329,6 +329,7 @@ const additionConfigV4ManualSchema = additionConfigV4BaseSchema.extend({
showProblemNumbers: z.boolean(),
showCellBorder: z.boolean(),
showTenFramesForAll: z.boolean(),
showBorrowNotation: z.boolean(), // Scratch boxes for borrowing work
// Optional: Which manual preset is selected
manualPreset: z.string().optional(),