39948-vm/frontend/src/lib/elementStyles.ts
2026-03-30 21:28:00 +04:00

147 lines
3.5 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';
/**
* 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;
fontFamily?: string;
fontStretch?: 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',
'fontFamily',
'fontStretch',
] 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>;
// Apply string properties
ELEMENT_STYLE_PROPS.forEach((prop) => {
const value = getTrimmedValue(source[prop]);
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];