/** * Element Styles * * Unified types and utilities for UI element CSS styling. * Used by constructor, RuntimePresentation, and element-type-defaults admin pages. */ import type { CSSProperties } from 'react'; /** * Normalize a numeric value to include a CSS unit suffix. * @param value - The value to normalize * @param unit - The unit to append (e.g., 'px', 'vw', 'vh', 's') * @returns Normalized value with unit, or empty string if invalid */ function normalizeWithUnit( value: string | number | undefined, unit: string, ): string { if (value === null || value === undefined || value === '') return ''; const str = String(value).trim(); if (!str) return ''; // Already has a unit - return as is if (/[a-z%]+$/i.test(str)) { return str; } // Just a number - append unit const num = parseFloat(str); if (Number.isFinite(num)) { return `${num}${unit}`; } return str; } /** Normalize pixel values (border, borderRadius, fontSize for description) */ export const normalizePixelValue = (value: string | number | undefined) => normalizeWithUnit(value, 'px'); /** Normalize viewport width values (width, minWidth, maxWidth) */ export const normalizeViewportWidth = (value: string | number | undefined) => normalizeWithUnit(value, 'vw'); /** Normalize viewport height values (height, minHeight, maxHeight) */ export const normalizeViewportHeight = (value: string | number | undefined) => normalizeWithUnit(value, 'vh'); /** * CSS style properties supported by UI elements. * These properties can be set in element-type-defaults and applied at runtime. */ export interface ElementStyleProperties { width?: string; height?: string; minWidth?: string; maxWidth?: string; minHeight?: string; maxHeight?: string; margin?: string; padding?: string; gap?: string; fontSize?: string; lineHeight?: string; fontWeight?: string; border?: string; borderRadius?: string; opacity?: string; boxShadow?: string; display?: string; position?: string; justifyContent?: string; alignItems?: string; textAlign?: string; zIndex?: string; backgroundColor?: string; color?: string; } /** * Array of CSS property names for iteration. * Used when applying styles from element data to CSSProperties. */ export const ELEMENT_STYLE_PROPS = [ 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'margin', 'padding', 'gap', 'fontSize', 'lineHeight', 'fontWeight', 'border', 'borderRadius', 'boxShadow', 'display', 'position', 'justifyContent', 'alignItems', 'textAlign', 'zIndex', 'backgroundColor', 'color', ] as const; /** * Properties that need numeric conversion */ const NUMERIC_PROPS = ['opacity', 'zIndex'] as const; /** * Get trimmed CSS value from unknown input. * Returns empty string for null/undefined, but preserves '0' for explicit zero values. */ const getTrimmedValue = (value: unknown): string => { if (value === null || value === undefined) return ''; // Preserve 0 as '0' - explicit zero should be applied if (value === 0) return '0'; return String(value).trim(); }; /** * Build React CSSProperties from element style properties. * Handles type coercion for special properties like opacity and zIndex. * * @param element - Object containing style properties * @returns React CSSProperties object * * @example * const style = buildElementStyle({ * width: '100px', * opacity: '0.5', * display: 'flex', * }); */ export function buildElementStyle( element: Partial, ): CSSProperties { const style: CSSProperties = {}; const source = element as Record; // Properties that need viewport width unit (vw) const vwProps = ['width', 'minWidth', 'maxWidth']; // Properties that need viewport height unit (vh) const vhProps = ['height', 'minHeight', 'maxHeight']; // Properties that need pixel unit (px) const pxProps = ['border', 'borderRadius']; // Apply string properties with unit normalization where needed ELEMENT_STYLE_PROPS.forEach((prop) => { const rawValue = getTrimmedValue(source[prop]); if (!rawValue) return; let value = rawValue; // Apply unit normalization based on property type if (vwProps.includes(prop)) { value = normalizeViewportWidth(rawValue); } else if (vhProps.includes(prop)) { value = normalizeViewportHeight(rawValue); } else if (pxProps.includes(prop)) { value = normalizePixelValue(rawValue); } if (value) { (style as Record)[prop] = value; } }); // Handle opacity (needs numeric conversion) const opacityValue = getTrimmedValue(source.opacity); if (opacityValue) { const parsed = Number(opacityValue); if (Number.isFinite(parsed)) { style.opacity = parsed; } } // Handle zIndex (needs numeric conversion, already in style from loop but override with number) const zIndexValue = getTrimmedValue(source.zIndex); if (zIndexValue) { const parsed = Number(zIndexValue); if (Number.isFinite(parsed)) { style.zIndex = parsed; } } return style; } /** * All style property names including numeric ones. * Used for form state management in element-type-defaults admin. */ export const ALL_STYLE_PROPS = [...ELEMENT_STYLE_PROPS, 'opacity'] as const; export type StylePropName = (typeof ALL_STYLE_PROPS)[number];