Files
soroban-abacus-flashcards/apps/web/scripts/generateDayIcon.tsx
Thomas Hallock b36df3a40c fix(worksheets): ten-frames not rendering in mastery mode
Fixed two critical bugs preventing ten-frames from rendering:

1. **Mastery mode not handled** (typstGenerator.ts:61)
   - Code only checked for 'smart' | 'manual' modes
   - Mastery mode fell into manual path, tried to use boolean flags that don't exist
   - Resulted in all display options being `undefined`
   - Fix: Check for both 'smart' OR 'mastery' modes (both use displayRules)

2. **Typst array membership syntax** (already fixed in previous commit)
   - Used `(i in array)` which doesn't work in Typst
   - Changed to `array.contains(i)`

Added comprehensive unit tests (tenFrames.test.ts):
- Problem analysis tests (regrouping detection)
- Display rule evaluation tests
- Full Typst template generation tests
- Mastery mode specific tests
- All 14 tests now passing

Added debug logging to trace display rules resolution:
- displayRules.ts: Shows rule evaluation per problem
- typstGenerator.ts: Shows enriched problems and Typst data
- Helps diagnose future issues

The issue was that mastery mode (which uses displayRules like smart mode)
was being treated as manual mode (which uses boolean flags), resulting in
undefined display options.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 10:06:27 -06:00

119 lines
3.6 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Generate a single day-of-month favicon
* Usage: npx tsx scripts/generateDayIcon.tsx <day>
* Example: npx tsx scripts/generateDayIcon.tsx 15
*/
import React from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { AbacusStatic } from '@soroban/abacus-react'
// Extract just the SVG element from rendered output
function extractSvgElement(markup: string): string {
const svgMatch = markup.match(/<svg[^>]*>[\s\S]*?<\/svg>/)
if (!svgMatch) {
throw new Error('No SVG element found in rendered output')
}
return svgMatch[0]
}
// Get day from command line argument
const day = parseInt(process.argv[2], 10)
if (!day || day < 1 || day > 31) {
console.error('Usage: npx tsx scripts/generateDayIcon.tsx <day>')
console.error('Example: npx tsx scripts/generateDayIcon.tsx 15')
process.exit(1)
}
// Render 2-column abacus showing day of month
// Using AbacusStatic for server-side rendering
const abacusMarkup = renderToStaticMarkup(
<AbacusStatic
value={day}
columns={2}
scaleFactor={1.8}
showNumbers={false}
hideInactiveBeads={true}
frameVisible={true}
cropToActiveBeads={{
padding: {
top: 8,
bottom: 2,
left: 5,
right: 5,
},
}}
customStyles={{
columnPosts: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 2,
},
reckoningBar: {
fill: '#1c1917',
stroke: '#0c0a09',
strokeWidth: 3,
},
columns: {
0: {
// Ones place - Gold (royal theme)
heavenBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
earthBeads: { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 2 },
},
1: {
// Tens place - Purple (royal theme)
heavenBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
earthBeads: { fill: '#a855f7', stroke: '#7e22ce', strokeWidth: 2 },
},
},
}}
/>
)
// Extract the cropped SVG
let croppedSvg = extractSvgElement(abacusMarkup)
// Remove !important from CSS (production code policy)
croppedSvg = croppedSvg.replace(/\s*!important/g, '')
// Parse width and height from the cropped SVG
const widthMatch = croppedSvg.match(/width="([^"]+)"/)
const heightMatch = croppedSvg.match(/height="([^"]+)"/)
if (!widthMatch || !heightMatch) {
throw new Error('Could not parse dimensions from cropped SVG')
}
const croppedWidth = parseFloat(widthMatch[1])
const croppedHeight = parseFloat(heightMatch[1])
// Calculate scale to fit cropped region into 96x96 (leaving room for border)
const targetSize = 96
const scale = Math.min(targetSize / croppedWidth, targetSize / croppedHeight)
// Center in 100x100 canvas
const scaledWidth = croppedWidth * scale
const scaledHeight = croppedHeight * scale
const offsetX = (100 - scaledWidth) / 2
const offsetY = (100 - scaledHeight) / 2
// Wrap in 100x100 SVG canvas for favicon
// Extract viewBox from cropped SVG to preserve it
const viewBoxMatch = croppedSvg.match(/viewBox="([^"]+)"/)
const viewBox = viewBoxMatch ? viewBoxMatch[1] : `0 0 ${croppedWidth} ${croppedHeight}`
const svg = `<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Abacus showing day ${day.toString().padStart(2, '0')} (US Central Time) - cropped to active beads -->
<svg x="${offsetX}" y="${offsetY}" width="${scaledWidth}" height="${scaledHeight}"
viewBox="${viewBox}">
${croppedSvg.match(/<svg[^>]*>([\s\S]*?)<\/svg>/)?.[1] || ''}
</svg>
</svg>
`
// Output to stdout so parent process can capture it
process.stdout.write(svg)