Compare commits

...

2 Commits

Author SHA1 Message Date
semantic-release-bot
3c245d29fa chore(release): 2.2.1 [skip ci]
## [2.2.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.2.0...v2.2.1) (2025-10-07)

### Bug Fixes

* remove remaining typst-dependent files ([d1b9b72](d1b9b72cfc))
2025-10-07 15:47:29 +00:00
Thomas Hallock
d1b9b72cfc fix: remove remaining typst-dependent files
Remove preview API route and template-demo page that still
referenced the deleted typst-soroban library.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 10:46:46 -05:00
4 changed files with 8 additions and 623 deletions

View File

@@ -1,3 +1,10 @@
## [2.2.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.2.0...v2.2.1) (2025-10-07)
### Bug Fixes
* remove remaining typst-dependent files ([d1b9b72](https://github.com/antialias/soroban-abacus-flashcards/commit/d1b9b72cfc2f2ba36c40d7ae54bc6fdfcc5f34da))
## [2.2.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.1.3...v2.2.0) (2025-10-07)

View File

@@ -1,164 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { generateSorobanSVG } from '@/lib/typst-soroban'
export async function POST(request: NextRequest) {
try {
const config = await request.json()
// Debug: log the received config
console.log('🔍 Preview config:', JSON.stringify(config, null, 2))
// Ensure range is set with a default
if (!config.range) {
config.range = '0-9'
}
// For preview, limit to a few numbers and use SVG format for fast rendering
const previewConfig = {
...config,
range: getPreviewRange(config.range),
format: 'svg', // Use SVG format for preview
cardsPerPage: 6 // Standard card layout
}
console.log('🔍 Processed preview config:', JSON.stringify(previewConfig, null, 2))
// Generate real SVG preview using typst.ts
console.log('🚀 Generating soroban SVG preview via typst.ts')
try {
// Parse the numbers from the range for individual cards
const numbers = parseNumbersFromRange(getPreviewRange(config.range))
console.log('🔍 Generating individual SVGs for numbers:', numbers)
// Generate individual SVGs for each number using typst.ts
const samples = []
for (const number of numbers) {
try {
const typstConfig = {
number: number,
beadShape: previewConfig.beadShape || 'diamond',
colorScheme: previewConfig.colorScheme || 'place-value',
hideInactiveBeads: previewConfig.hideInactiveBeads || false,
scaleFactor: previewConfig.scaleFactor || 1.0,
width: '200pt',
height: '250pt'
}
console.log(`🔍 Generating typst.ts SVG for number ${number}`)
const svg = await generateSorobanSVG(typstConfig)
console.log(`✅ Generated typst.ts SVG for ${number}, length: ${svg.length}`)
samples.push({
number,
front: svg,
back: number.toString()
})
} catch (error) {
console.error(`❌ Failed to generate SVG for number ${number}:`, error instanceof Error ? error.message : error)
samples.push({
number,
front: `<svg width="200" height="300" viewBox="0 0 200 300" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="180" height="280" fill="none" stroke="#ccc" stroke-width="2"/>
<line x1="20" y1="150" x2="180" y2="150" stroke="#ccc" stroke-width="2"/>
<text x="100" y="160" text-anchor="middle" font-size="24" fill="#666">SVG Error</text>
<text x="100" y="180" text-anchor="middle" font-size="16" fill="#999">${number}</text>
</svg>`,
back: number.toString()
})
}
}
return NextResponse.json({
count: numbers.length,
samples,
note: 'Real individual SVGs generated by typst.ts'
})
} catch (error) {
console.error('⚠️ Typst.ts SVG generation failed, using fallback preview:', error instanceof Error ? error.message : error)
return NextResponse.json(getMockPreviewData(config))
}
} catch (error) {
console.error('❌ Preview generation failed:', error)
// Always fall back to mock data for preview
const config = await request.json().catch(() => ({ range: '0-9' }))
return NextResponse.json(getMockPreviewData(config))
}
}
// Helper function to parse numbers from range string
function parseNumbersFromRange(range: string): number[] {
if (!range) return [0, 1, 2]
if (range.includes('-')) {
const [start] = range.split('-')
const startNum = parseInt(start) || 0
return [startNum, startNum + 1, startNum + 2]
}
if (range.includes(',')) {
return range.split(',').slice(0, 3).map(n => parseInt(n.trim()) || 0)
}
const num = parseInt(range) || 0
return [num, num + 1, num + 2]
}
// Helper function to limit range for preview
function getPreviewRange(range: string): string {
if (!range) return '0,1,2'
if (range.includes('-')) {
const [start] = range.split('-')
const startNum = parseInt(start) || 0
return `${startNum},${startNum + 1},${startNum + 2}`
}
if (range.includes(',')) {
const numbers = range.split(',').slice(0, 3)
return numbers.join(',')
}
return range
}
// Mock preview data for development and fallback
function getMockPreviewData(config: any) {
const range = config.range || '0-9'
let numbers: number[]
if (range.includes('-')) {
const [start] = range.split('-')
const startNum = parseInt(start) || 0
numbers = [startNum, startNum + 1, startNum + 2]
} else if (range.includes(',')) {
numbers = range.split(',').slice(0, 3).map((n: string) => parseInt(n.trim()) || 0)
} else {
const num = parseInt(range) || 0
numbers = [num, num + 1, num + 2]
}
return {
count: numbers.length,
samples: numbers.map(number => ({
number,
front: `<svg width="200" height="300" viewBox="0 0 200 300" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="180" height="280" fill="none" stroke="#ccc" stroke-width="2"/>
<line x1="20" y1="150" x2="180" y2="150" stroke="#ccc" stroke-width="2"/>
<text x="100" y="160" text-anchor="middle" font-size="24" fill="#666">Preview Error</text>
<text x="100" y="180" text-anchor="middle" font-size="16" fill="#999">${number}</text>
</svg>`,
back: number.toString()
}))
}
}
// Health check endpoint
export async function GET() {
return NextResponse.json({
status: 'healthy',
endpoint: 'preview',
message: 'Preview API is running'
})
}

View File

@@ -1,458 +0,0 @@
'use client'
import React, { useState } from 'react'
import { css } from '../../../styled-system/css'
import { TypstSoroban } from '../../components/TypstSoroban'
export default function TemplateDemoPage() {
const [selectedNumber, setSelectedNumber] = useState(42)
const demoNumbers = [0, 1, 5, 12, 42, 123, 567, 1234]
const beadShapes = ['diamond', 'circle', 'square'] as const
const colorSchemes = ['monochrome', 'place-value', 'heaven-earth', 'alternating'] as const
const colorPalettes = ['default', 'colorblind', 'mnemonic', 'grayscale', 'nature'] as const
return (
<div className={css({
p: 8,
maxW: '1200px',
mx: 'auto',
fontFamily: 'system-ui'
})}>
<h1 className={css({
fontSize: '2xl',
fontWeight: 'bold',
mb: 6,
textAlign: 'center'
})}>
🎨 Soroban Template Package Demo
</h1>
<p className={css({
mb: 8,
textAlign: 'center',
color: 'gray.600'
})}>
Showcasing browser-side SVG generation with viewport cropping and bead annotations
</p>
{/* Number Selector */}
<div className={css({
mb: 8,
p: 4,
bg: 'gray.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'gray.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 3 })}>
Select Number to Display
</h2>
<div className={css({ display: 'flex', gap: 2, flexWrap: 'wrap' })}>
{demoNumbers.map(num => (
<button
key={num}
onClick={() => setSelectedNumber(num)}
className={css({
px: 3,
py: 2,
borderRadius: 'md',
border: '1px solid',
borderColor: selectedNumber === num ? 'blue.500' : 'gray.300',
bg: selectedNumber === num ? 'blue.500' : 'white',
color: selectedNumber === num ? 'white' : 'gray.700',
cursor: 'pointer',
_hover: {
bg: selectedNumber === num ? 'blue.600' : 'gray.100'
}
})}
>
{num}
</button>
))}
</div>
<div className={css({ mt: 3 })}>
<input
type="number"
value={selectedNumber}
onChange={(e) => setSelectedNumber(parseInt(e.target.value) || 0)}
className={css({
px: 3,
py: 2,
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
width: '120px'
})}
placeholder="Custom number"
/>
</div>
</div>
{/* Bead Shapes Demo */}
<div className={css({
mb: 8,
p: 4,
bg: 'blue.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'blue.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 4 })}>
🔸 Bead Shapes
</h2>
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 4
})}>
{beadShapes.map(shape => (
<div key={shape} className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2, textTransform: 'capitalize' })}>
{shape}
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
/>
</div>
</div>
))}
</div>
</div>
{/* Color Schemes Demo */}
<div className={css({
mb: 8,
p: 4,
bg: 'green.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'green.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 4 })}>
🎨 Color Schemes
</h2>
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 4
})}>
{colorSchemes.map(scheme => (
<div key={scheme} className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2, textTransform: 'capitalize' })}>
{scheme.replace('-', ' ')}
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
/>
</div>
</div>
))}
</div>
</div>
{/* Color Palettes Demo */}
<div className={css({
mb: 8,
p: 4,
bg: 'purple.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'purple.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 4 })}>
🌈 Color Palettes
</h2>
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 4
})}>
{colorPalettes.map(palette => (
<div key={palette} className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2, textTransform: 'capitalize' })}>
{palette}
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
/>
</div>
</div>
))}
</div>
</div>
{/* Configuration Options Demo */}
<div className={css({
mb: 8,
p: 4,
bg: 'orange.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'orange.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 4 })}>
Configuration Options
</h2>
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: 4
})}>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Show Empty Columns
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="180pt"
height="200pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Hide Inactive Beads
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Transparent Background
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'repeating-conic-gradient(from 0deg, #eee 0deg 90deg, #ddd 90deg 180deg)',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
transparent={true}
width="150pt"
height="180pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Scale Factor 1.5x
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
enableServerFallback={true}
/>
</div>
</div>
</div>
</div>
{/* Size Variations Demo */}
<div className={css({
mb: 8,
p: 4,
bg: 'pink.50',
borderRadius: 'md',
border: '1px solid',
borderColor: 'pink.200'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 4 })}>
📏 Size Variations (Viewport Cropping Test)
</h2>
<p className={css({ mb: 4, fontSize: 'sm', color: 'gray.600' })}>
All these should show optimally cropped SVGs regardless of initial canvas size
</p>
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 4,
alignItems: 'start'
})}>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Small Canvas (100x120pt)
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="100pt"
height="120pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Medium Canvas (150x180pt)
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="150pt"
height="180pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Large Canvas (300x400pt)
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="300pt"
height="400pt"
enableServerFallback={true}
/>
</div>
</div>
<div className={css({ textAlign: 'center' })}>
<h3 className={css({ fontWeight: 'medium', mb: 2 })}>
Huge Canvas (500x600pt)
</h3>
<div className={css({
border: '1px solid',
borderColor: 'gray.300',
borderRadius: 'md',
p: 2,
bg: 'white',
display: 'inline-block'
})}>
<TypstSoroban
number={selectedNumber}
width="500pt"
height="600pt"
enableServerFallback={true}
/>
</div>
</div>
</div>
</div>
{/* Technical Info */}
<div className={css({
p: 4,
bg: 'gray.100',
borderRadius: 'md',
border: '1px solid',
borderColor: 'gray.300'
})}>
<h2 className={css({ fontSize: 'lg', fontWeight: 'semibold', mb: 3 })}>
🔧 Technical Details
</h2>
<div className={css({ fontSize: 'sm', color: 'gray.700', lineHeight: '1.6' })}>
<p className={css({ mb: 2 })}>
<strong> Browser-side generation:</strong> All SVGs generated using Typst.ts WASM in your browser
</p>
<p className={css({ mb: 2 })}>
<strong> Viewport cropping:</strong> SVG processor detects crop marks and optimizes viewBox (67-81% size reduction)
</p>
<p className={css({ mb: 2 })}>
<strong> Bead annotations:</strong> Interactive data attributes added to each bead for hover/click functionality
</p>
<p className={css({ mb: 2 })}>
<strong> Template package integration:</strong> Official @soroban/templates package provides Typst templates and SVG processing
</p>
<p>
<strong>🎯 Key insight:</strong> If all size variations above look identical despite different canvas sizes,
the viewport cropping is working perfectly!
</p>
</div>
</div>
</div>
)
}

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "2.2.0",
"version": "2.2.1",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [