39948-vm/frontend/src/lib/elementStyles.ts

204 lines
5.3 KiB
TypeScript

/**
* 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<ElementStyleProperties>,
): CSSProperties {
const style: CSSProperties = {};
const source = element as Record<string, unknown>;
// 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<string, unknown>)[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];