diff --git a/apps/web/scripts/generateCalendarComposite.tsx b/apps/web/scripts/generateCalendarComposite.tsx index 413b852d..ef18ddb8 100644 --- a/apps/web/scripts/generateCalendarComposite.tsx +++ b/apps/web/scripts/generateCalendarComposite.tsx @@ -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 = ` - + ${monthName} @@ -113,13 +128,8 @@ const compositeSVG = ` { const yearAbacusSVG = renderAbacusSVG(year, yearColumns, 1) const yearAbacusContent = yearAbacusSVG.replace(/]*>/, '').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 ` + viewBox="0 0 ${yearAbacusActualWidth} ${yearAbacusActualHeight}"> ${yearAbacusContent} ` })()} diff --git a/packages/abacus-react/src/AbacusStatic.tsx b/packages/abacus-react/src/AbacusStatic.tsx index ed52b711..98873190 100644 --- a/packages/abacus-react/src/AbacusStatic.tsx +++ b/packages/abacus-react/src/AbacusStatic.tsx @@ -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, diff --git a/packages/abacus-react/src/AbacusUtils.ts b/packages/abacus-react/src/AbacusUtils.ts index 28727ce9..d4527fab 100644 --- a/packages/abacus-react/src/AbacusUtils.ts +++ b/packages/abacus-react/src/AbacusUtils.ts @@ -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 } +} diff --git a/packages/abacus-react/src/index.ts b/packages/abacus-react/src/index.ts index e6b07680..bf2716fe 100644 --- a/packages/abacus-react/src/index.ts +++ b/packages/abacus-react/src/index.ts @@ -47,6 +47,7 @@ export { calculateBeadDiffFromValues, validateAbacusValue, areStatesEqual, + calculateAbacusDimensions, } from "./AbacusUtils"; export type { BeadState, diff --git a/packages/abacus-react/src/static.ts b/packages/abacus-react/src/static.ts index b43feb11..0a555ae7 100644 --- a/packages/abacus-react/src/static.ts +++ b/packages/abacus-react/src/static.ts @@ -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,