/** * Fit configured swatch padding inside a fixed swatch box. * * Swatches are `box-sizing: border-box` with an inline width/height, but CSS * never shrinks padding: a 24px box with 16px side padding still paints 32px * wide, and 48px padding on every side paints 96px. The stored/draft settings * stay untouched; only the *painted* padding is scaled down so the advertised * outer dimension is authoritative. Borders are always reserved, and callers * may reserve useful content space (a label's font size) so text keeps room. * * Shared by the free SwatchGroup, the Pro ExtensionSwatchGroup and the Pro * renderer so product pages, shop archives and admin previews agree. */ export type BoxPadding = { top: number; right: number; bottom: number; left: number; }; export type FitPaddingOptions = { /** Fixed outer width. Omit when the horizontal axis is free (minWidth). */ width?: number; /** Fixed outer height. Omit when the vertical axis is free. */ height?: number; /** Total horizontal border (left + right) reserved inside the box. */ borderX?: number; /** Total vertical border (top + bottom) reserved inside the box. */ borderY?: number; /** Minimum horizontal content space reserved before padding is fitted. */ reserveX?: number; /** Minimum vertical content space reserved before padding is fitted. */ reserveY?: number; }; const toNonNegative = (value: unknown): number => { const candidate = Number(value); return Number.isFinite(candidate) && candidate > 0 ? candidate : 0; }; /** * Fit a start/end padding pair inside `size`, keeping `border` and `reserve` * free. Padding that already fits is returned unchanged; otherwise both sides * are scaled down proportionally (floored so the pair never exceeds the room). */ export const fitPaddingAxis = ( start: number, end: number, { size, border = 0, reserve = 0, }: { size?: number; border?: number; reserve?: number }, ): [number, number] => { const a = toNonNegative(start); const b = toNonNegative(end); if (size === undefined || !Number.isFinite(size)) { return [a, b]; } const room = Math.max( 0, toNonNegative(size) - toNonNegative(border) - toNonNegative(reserve), ); const total = a + b; if (total <= room) { return [a, b]; } if (room <= 0 || total <= 0) { return [0, 0]; } const scale = room / total; return [Math.floor(a * scale), Math.floor(b * scale)]; }; export const fitSwatchPadding = ( padding: BoxPadding, { width, height, borderX = 0, borderY = 0, reserveX = 0, reserveY = 0, }: FitPaddingOptions, ): BoxPadding => { const [top, bottom] = fitPaddingAxis(padding.top, padding.bottom, { size: height, border: borderY, reserve: reserveY, }); const [left, right] = fitPaddingAxis(padding.left, padding.right, { size: width, border: borderX, reserve: reserveX, }); return { top, right, bottom, left }; }; /** Parse a `T R B L` px shorthand (1..4 values, CSS expansion rules). */ export const parsePaddingShorthand = (shorthand: unknown): BoxPadding => { const parts = String(shorthand ?? "") .trim() .split(/\s+/) .filter(Boolean) .map((part) => toNonNegative(parseFloat(part))); const [a = 0, b = a, c = a, d = b] = parts; return { top: a, right: b, bottom: c, left: d }; }; export const toPaddingShorthand = (padding: BoxPadding): string => `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`; /** Shorthand-in / shorthand-out convenience for renderers holding a string. */ export const fitSwatchPaddingShorthand = ( shorthand: unknown, options: FitPaddingOptions, ): string => toPaddingShorthand( fitSwatchPadding(parsePaddingShorthand(shorthand), options), ); /** Read a `14px`-style value (or number) as a non-negative px number. */ export const parsePx = (value: unknown, fallback: number): number => { const candidate = typeof value === "number" ? value : parseFloat(String(value ?? "")); return Number.isFinite(candidate) && candidate >= 0 ? candidate : fallback; };