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:
@@ -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>`
|
||||
})()}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ export {
|
||||
calculateBeadDiffFromValues,
|
||||
validateAbacusValue,
|
||||
areStatesEqual,
|
||||
calculateAbacusDimensions,
|
||||
} from "./AbacusUtils";
|
||||
export type {
|
||||
BeadState,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user