Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c245d29fa | ||
|
|
d1b9b72cfc |
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user