204 lines
5.3 KiB
TypeScript
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];
|