feat(abacus-react): add shared dimension calculator for consistent sizing

- Add calculateAbacusDimensions utility to AbacusUtils
- Refactor AbacusStatic to use shared dimension calculator
- Update calendar composite generator to use shared utility
- Export calculateAbacusDimensions from index and static entry points
- Ensures consistent sizing between preview and PDF generation

🤖 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-04 10:42:45 -06:00
parent 293390ae35
commit e5ba772fde
5 changed files with 72 additions and 23 deletions

View File

@ -9,7 +9,7 @@
*/
import React from 'react'
import { AbacusStatic } from '@soroban/abacus-react/static'
import { AbacusStatic, calculateAbacusDimensions } from '@soroban/abacus-react/static'
interface CalendarCompositeOptions {
month: number
@ -45,9 +45,30 @@ const MARGIN = 50
const CONTENT_WIDTH = WIDTH - MARGIN * 2
const CONTENT_HEIGHT = HEIGHT - MARGIN * 2
// Header
const HEADER_HEIGHT = 60
// Abacus natural size is 120x230 at scale=1
const ABACUS_NATURAL_WIDTH = 120
const ABACUS_NATURAL_HEIGHT = 230
// Calculate how many columns needed for year
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
// Year abacus dimensions (calculate first to determine header height)
// Use the shared dimension calculator so we stay in sync with AbacusStatic
const { width: yearAbacusActualWidth, height: yearAbacusActualHeight } = calculateAbacusDimensions({
columns: yearColumns,
showNumbers: false,
columnLabels: [],
})
const yearAbacusDisplayWidth = WIDTH * 0.15 // Display size on page
const yearAbacusDisplayHeight = (yearAbacusActualHeight / yearAbacusActualWidth) * yearAbacusDisplayWidth
// Header - sized to fit month name + year abacus
const MONTH_NAME_HEIGHT = 40
const HEADER_HEIGHT = MONTH_NAME_HEIGHT + yearAbacusDisplayHeight + 20 // 20px spacing
const TITLE_Y = MARGIN + 35
const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2
const yearAbacusY = TITLE_Y + 10
// Calendar grid
const GRID_START_Y = MARGIN + HEADER_HEIGHT
@ -59,10 +80,7 @@ const DAY_GRID_HEIGHT = GRID_HEIGHT - WEEKDAY_ROW_HEIGHT
const CELL_WIDTH = CONTENT_WIDTH / 7
const DAY_CELL_HEIGHT = DAY_GRID_HEIGHT / 6
// Abacus natural size is 120x230 at scale=1
// We need to fit in cell with padding
const ABACUS_NATURAL_WIDTH = 120
const ABACUS_NATURAL_HEIGHT = 230
// Day abacus sizing - fit in cell with padding
const CELL_PADDING = 5
// Calculate max scale to fit in cell
@ -82,9 +100,6 @@ for (let day = 1; day <= daysInMonth; day++) {
calendarCells.push(day)
}
// Calculate how many columns needed for year
const yearColumns = Math.max(1, Math.ceil(Math.log10(year + 1)))
// Render individual abacus SVGs as complete SVG elements
function renderAbacusSVG(value: number, columns: number, scale: number): string {
return renderToString(
@ -105,7 +120,7 @@ const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" h
<rect width="${WIDTH}" height="${HEIGHT}" fill="white"/>
<!-- Title: Month Name -->
<text x="${WIDTH / 2}" y="${TITLE_Y - 20}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a">
<text x="${WIDTH / 2}" y="${TITLE_Y}" text-anchor="middle" font-family="Arial" font-size="32" font-weight="bold" fill="#1a1a1a">
${monthName}
</text>
@ -113,13 +128,8 @@ const compositeSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" h
${(() => {
const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1)
const yearAbacusContent = yearAbacusSVG.replace(/<svg[^>]*>/, '').replace(/<\/svg>$/, '')
// Scale year abacus to be smaller (about 15% of width)
const yearAbacusDisplayWidth = WIDTH * 0.15
const yearAbacusDisplayHeight = (ABACUS_NATURAL_HEIGHT / ABACUS_NATURAL_WIDTH) * yearAbacusDisplayWidth
const yearAbacusX = (WIDTH - yearAbacusDisplayWidth) / 2
const yearAbacusY = TITLE_Y - 10
return `<svg x="${yearAbacusX}" y="${yearAbacusY}" width="${yearAbacusDisplayWidth}" height="${yearAbacusDisplayHeight}"
viewBox="0 0 ${ABACUS_NATURAL_WIDTH} ${ABACUS_NATURAL_HEIGHT}">
viewBox="0 0 ${yearAbacusActualWidth} ${yearAbacusActualHeight}">
${yearAbacusContent}
</svg>`
})()}

View File

@ -6,7 +6,7 @@
* Different: No hooks, no animations, no interactions, simplified rendering
*/
import { numberToAbacusState } from './AbacusUtils'
import { numberToAbacusState, calculateAbacusDimensions } from './AbacusUtils'
import { AbacusStaticBead } from './AbacusStaticBead'
import type {
AbacusCustomStyles,
@ -175,7 +175,14 @@ export function AbacusStatic({
beadConfigs.push(beads)
}
// Calculate dimensions (matching AbacusReact)
// Calculate dimensions using shared utility
const { width, height } = calculateAbacusDimensions({
columns: effectiveColumns,
showNumbers: !!showNumbers,
columnLabels,
})
// Layout constants (must match calculateAbacusDimensions)
const beadSize = 20
const rodSpacing = 40
const heavenHeight = 60
@ -185,9 +192,6 @@ export function AbacusStatic({
const numberHeightCalc = showNumbers ? 30 : 0
const labelHeight = columnLabels.length > 0 ? 30 : 0
const width = effectiveColumns * rodSpacing + padding * 2
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
const dimensions = {
width,
height,

View File

@ -356,3 +356,37 @@ function getPlaceName(place: number): string {
return `place ${place} column`
}
}
/**
* Calculate the natural dimensions of an abacus SVG
* This uses the same logic as AbacusStatic to ensure consistency
*
* @param columns - Number of columns in the abacus
* @param showNumbers - Whether numbers are shown below columns
* @param columnLabels - Array of column labels (if any)
* @returns Object with width and height in pixels (at scale=1)
*/
export function calculateAbacusDimensions({
columns,
showNumbers = true,
columnLabels = [],
}: {
columns: number
showNumbers?: boolean
columnLabels?: string[]
}): { width: number; height: number } {
// Constants matching AbacusStatic
const beadSize = 20
const rodSpacing = 40
const heavenHeight = 60
const earthHeight = 120
const barHeight = 10
const padding = 20
const numberHeightCalc = showNumbers ? 30 : 0
const labelHeight = columnLabels.length > 0 ? 30 : 0
const width = columns * rodSpacing + padding * 2
const height = heavenHeight + earthHeight + barHeight + padding * 2 + numberHeightCalc + labelHeight
return { width, height }
}

View File

@ -47,6 +47,7 @@ export {
calculateBeadDiffFromValues,
validateAbacusValue,
areStatesEqual,
calculateAbacusDimensions,
} from "./AbacusUtils";
export type {
BeadState,

View File

@ -9,7 +9,7 @@ export { AbacusStaticBead } from './AbacusStaticBead'
export type { StaticBeadProps } from './AbacusStaticBead'
// Re-export shared utilities that are safe for server components
export { numberToAbacusState } from './AbacusUtils'
export { numberToAbacusState, calculateAbacusDimensions } from './AbacusUtils'
export type {
AbacusCustomStyles,
BeadConfig,