fixed css units inconsistence

This commit is contained in:
Dmitri 2026-04-03 10:00:50 +04:00
parent 8ef30576b1
commit 4e431eab9b
7 changed files with 160 additions and 45 deletions

File diff suppressed because one or more lines are too long

View File

@ -286,10 +286,42 @@ export const toOptionalTrimmed = (value: string): string | undefined => {
return normalized ? normalized : undefined;
};
/**
* Convert a value to a CSS value with unit.
* Handles edge cases:
* - Complex values with spaces (e.g., "10px 20px") preserved as-is
* - Values with existing units (e.g., "100vw", "16px") preserved as-is
* - Plain numbers (e.g., "24", 100) append unit
* - Zero values ("0", 0) return "0" (no unit needed)
* - CSS functions (e.g., "calc(...)") preserved as-is
*/
export const toUnitValue = (
value: string,
unit: 'vw' | 'vh' | 'px',
unit: 'vw' | 'vh' | 'px' | 'rem',
): string | undefined => {
const normalized = normalizeNumberString(value);
return normalized ? `${normalized}${unit}` : undefined;
const trimmed = String(value ?? '').trim();
if (!trimmed) return undefined;
// Zero doesn't need a unit
if (trimmed === '0') return '0';
// Complex values (contain spaces) - return as-is
if (trimmed.includes(' ')) return trimmed;
// CSS functions (calc, var, etc.) - return as-is
if (/^(calc|var|min|max|clamp)\(/i.test(trimmed)) return trimmed;
// Already has a unit (letters or %) at the end - return as-is
if (/[a-z%]+$/i.test(trimmed)) return trimmed;
// Handle numbers (including negative and decimals like ".5")
const num = parseFloat(trimmed);
if (Number.isFinite(num)) {
// Zero after parsing
if (num === 0) return '0';
return `${num}${unit}`;
}
// Fallback: return as-is if non-empty
return trimmed || undefined;
};

View File

@ -650,22 +650,31 @@ export function useElementSettingsForm(options: UseElementSettingsFormOptions) {
}
// Description type settings
// Note: Color/fontSize/fontWeight cascade from General Element Styles via CSS inheritance
// Only set section-specific values if explicitly configured (allows inheritance)
if (isDescriptionType) {
settings.iconUrl = state.iconUrl.trim();
settings.descriptionTitle = state.descriptionTitle.trim();
settings.descriptionText = state.descriptionText;
settings.descriptionTitleFontSize =
state.descriptionTitleFontSize.trim() || '48px';
settings.descriptionTextFontSize =
state.descriptionTextFontSize.trim() || '36px';
settings.descriptionTitleFontFamily =
state.descriptionTitleFontFamily.trim() || 'inherit';
settings.descriptionTextFontFamily =
state.descriptionTextFontFamily.trim() || 'inherit';
settings.descriptionTitleColor =
state.descriptionTitleColor.trim() || '#000000';
settings.descriptionTextColor =
state.descriptionTextColor.trim() || '#4B5563';
// Only include if explicitly set - allows CSS inheritance from wrapper
if (state.descriptionTitleFontSize.trim()) {
settings.descriptionTitleFontSize = state.descriptionTitleFontSize.trim();
}
if (state.descriptionTextFontSize.trim()) {
settings.descriptionTextFontSize = state.descriptionTextFontSize.trim();
}
if (state.descriptionTitleFontFamily.trim()) {
settings.descriptionTitleFontFamily = state.descriptionTitleFontFamily.trim();
}
if (state.descriptionTextFontFamily.trim()) {
settings.descriptionTextFontFamily = state.descriptionTextFontFamily.trim();
}
if (state.descriptionTitleColor.trim()) {
settings.descriptionTitleColor = state.descriptionTitleColor.trim();
}
if (state.descriptionTextColor.trim()) {
settings.descriptionTextColor = state.descriptionTextColor.trim();
}
}
// Gallery type settings

View File

@ -56,9 +56,10 @@ const NavigationElement: React.FC<NavigationElementProps> = ({
}
// Without icon: render text label
// fontSize/fontWeight/color inherit from General Element Styles via CSS cascade
return (
<div className={className} style={style}>
<span className='px-4 py-2 text-sm' style={labelFontStyle}>
<span className='px-4 py-2' style={labelFontStyle}>
{element.navLabel ||
(element.type === 'navigation_next' ? 'Next' : 'Back')}
</span>

View File

@ -21,9 +21,10 @@ const PopupElement: React.FC<PopupElementProps> = ({
className,
style,
}) => {
// fontSize/fontWeight/color inherit from General Element Styles via CSS cascade
return (
<div className={className} style={style}>
<span className='px-4 py-2 text-sm'>{element.label || 'Popup'}</span>
<span className='px-4 py-2'>{element.label || 'Popup'}</span>
</div>
);
};

View File

@ -9,6 +9,15 @@ import type { CSSProperties } from 'react';
/**
* Normalize a numeric value to include a CSS unit suffix.
* Handles edge cases:
* - Complex values with spaces (e.g., "10px 20px") preserved as-is
* - Values with existing units (e.g., "100vw", "16px") preserved as-is
* - Plain numbers (e.g., "24", 100) append unit
* - Zero values ("0", 0) return "0" (no unit needed)
* - Negative values (e.g., "-10") append unit
* - Decimal without leading zero (e.g., ".5") append unit
* - CSS functions (e.g., "calc(...)") preserved as-is
*
* @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
@ -22,17 +31,27 @@ function normalizeWithUnit(
const str = String(value).trim();
if (!str) return '';
// Already has a unit - return as is
if (/[a-z%]+$/i.test(str)) {
return str;
}
// Zero doesn't need a unit
if (str === '0') return '0';
// Just a number - append unit
// Complex values (contain spaces) - return as-is
if (str.includes(' ')) return str;
// CSS functions (calc, var, etc.) - return as-is
if (/^(calc|var|min|max|clamp)\(/i.test(str)) return str;
// Already has a unit (letters or %) at the end - return as-is
if (/[a-z%]+$/i.test(str)) return str;
// Handle numbers (including negative and decimals like ".5")
const num = parseFloat(str);
if (Number.isFinite(num)) {
// Zero after parsing
if (num === 0) return '0';
return `${num}${unit}`;
}
// Fallback: return as-is
return str;
}
@ -150,7 +169,8 @@ export function buildElementStyle(
// Properties that need viewport height unit (vh)
const vhProps = ['height', 'minHeight', 'maxHeight'];
// Properties that need pixel unit (px)
const pxProps = ['border', 'borderRadius'];
// Note: 'border' is NOT included - it's a complex value like "1px solid #ccc"
const pxProps = ['borderRadius'];
// Apply string properties with unit normalization where needed
ELEMENT_STYLE_PROPS.forEach((prop) => {

View File

@ -61,15 +61,65 @@ const getTrimmedValue = (value: unknown): string => {
};
/**
* Apply value with default fallback
* Normalize a numeric value to include a CSS unit suffix.
* Handles edge cases:
* - Complex values with spaces (e.g., "0.5rem 0.75rem") preserved as-is
* - Values with existing units (e.g., "1.5rem", "16px") preserved as-is
* - Plain numbers (e.g., "0.875", 0.5) append unit
* - Zero values ("0", 0) return "0" (no unit needed)
* - Negative values (e.g., "-0.5") append unit
* - Decimal without leading zero (e.g., ".5") append unit
* - CSS functions (e.g., "calc(...)") preserved as-is
*
* @param value - The value to normalize
* @param unit - The unit to append (e.g., 'rem', 'px')
* @returns Normalized value with unit, or empty string if invalid
*/
const normalizeWithUnit = (value: unknown, unit: string): string => {
const trimmed = getTrimmedValue(value);
if (!trimmed) return '';
// Zero doesn't need a unit
if (trimmed === '0') return '0';
// Complex values (contain spaces) - return as-is (e.g., "0.5rem 0.75rem")
if (trimmed.includes(' ')) return trimmed;
// CSS functions (calc, var, etc.) - return as-is
if (/^(calc|var|min|max|clamp)\(/i.test(trimmed)) return trimmed;
// Already has a unit (letters or %) at the end - return as-is
if (/[a-z%]+$/i.test(trimmed)) return trimmed;
// Handle numbers (including negative and decimals like ".5")
const num = parseFloat(trimmed);
if (Number.isFinite(num)) {
// Zero after parsing
if (num === 0) return '0';
return `${num}${unit}`;
}
// Fallback: return as-is (shouldn't normally reach here)
return trimmed;
};
/** Normalize rem values (fontSize, padding, borderRadius, gap) */
const normalizeRemValue = (value: unknown): string => normalizeWithUnit(value, 'rem');
/** Normalize pixel values (for properties that use px) */
const normalizePxValue = (value: unknown): string => normalizeWithUnit(value, 'px');
/**
* Apply value with default fallback and optional unit normalization
*/
const applyWithDefault = (
style: CSSProperties,
prop: keyof CSSProperties,
value: unknown,
defaultValue: unknown,
normalize?: (v: unknown) => string,
): void => {
const trimmed = getTrimmedValue(value);
const trimmed = normalize ? normalize(value) : getTrimmedValue(value);
if (trimmed) {
(style as Record<string, unknown>)[prop] = trimmed;
} else if (defaultValue !== undefined) {
@ -79,13 +129,15 @@ const applyWithDefault = (
/**
* Apply value only if explicitly set (no default - allows CSS inheritance)
* with optional unit normalization
*/
const applyIfSet = (
style: CSSProperties,
prop: keyof CSSProperties,
value: unknown,
normalize?: (v: unknown) => string,
): void => {
const trimmed = getTrimmedValue(value);
const trimmed = normalize ? normalize(value) : getTrimmedValue(value);
if (trimmed) {
(style as Record<string, unknown>)[prop] = trimmed;
}
@ -103,12 +155,12 @@ export function buildGalleryHeaderStyle(
applyIfSet(style, 'backgroundColor', element.galleryHeaderBackgroundColor);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.galleryHeaderColor);
applyIfSet(style, 'fontSize', element.galleryHeaderFontSize);
applyIfSet(style, 'fontSize', element.galleryHeaderFontSize, normalizeRemValue);
applyIfSet(style, 'fontWeight', element.galleryHeaderFontWeight);
// Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding);
applyIfSet(style, 'borderRadius', element.galleryHeaderBorderRadius);
applyIfSet(style, 'border', element.galleryHeaderBorder);
applyWithDefault(style, 'padding', element.galleryHeaderPadding, defaults.padding, normalizeRemValue);
applyIfSet(style, 'borderRadius', element.galleryHeaderBorderRadius, normalizeRemValue);
applyIfSet(style, 'border', element.galleryHeaderBorder); // Complex value, no normalization
// Apply font family with font library resolution
const fontKey = element.galleryHeaderFontFamily;
@ -136,12 +188,12 @@ export function buildGalleryTitleStyle(
applyWithDefault(style, 'backgroundColor', element.galleryTitleBackgroundColor, defaults.backgroundColor);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.galleryTitleColor);
applyIfSet(style, 'fontSize', element.galleryTitleFontSize);
applyIfSet(style, 'fontSize', element.galleryTitleFontSize, normalizeRemValue);
applyIfSet(style, 'fontWeight', element.galleryTitleFontWeight);
// Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.galleryTitlePadding, defaults.padding);
applyWithDefault(style, 'borderRadius', element.galleryTitleBorderRadius, defaults.borderRadius);
applyIfSet(style, 'border', element.galleryTitleBorder);
applyWithDefault(style, 'padding', element.galleryTitlePadding, defaults.padding, normalizeRemValue);
applyWithDefault(style, 'borderRadius', element.galleryTitleBorderRadius, defaults.borderRadius, normalizeRemValue);
applyIfSet(style, 'border', element.galleryTitleBorder); // Complex value, no normalization
// Apply font family with font library resolution
const fontKey = element.galleryTitleFontFamily;
@ -169,12 +221,12 @@ export function buildGallerySpanStyle(
applyWithDefault(style, 'backgroundColor', element.gallerySpanBackgroundColor, defaults.backgroundColor);
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
applyIfSet(style, 'color', element.gallerySpanColor);
applyIfSet(style, 'fontSize', element.gallerySpanFontSize);
applyIfSet(style, 'fontSize', element.gallerySpanFontSize, normalizeRemValue);
applyIfSet(style, 'fontWeight', element.gallerySpanFontWeight);
// Non-inheritable properties: use defaults
applyWithDefault(style, 'padding', element.gallerySpanPadding, defaults.padding);
applyWithDefault(style, 'borderRadius', element.gallerySpanBorderRadius, defaults.borderRadius);
applyIfSet(style, 'border', element.gallerySpanBorder);
applyWithDefault(style, 'padding', element.gallerySpanPadding, defaults.padding, normalizeRemValue);
applyWithDefault(style, 'borderRadius', element.gallerySpanBorderRadius, defaults.borderRadius, normalizeRemValue);
applyIfSet(style, 'border', element.gallerySpanBorder); // Complex value, no normalization
// Apply font family with font library resolution (fallback to galleryTextFontFamily for legacy support)
const fontKey = element.gallerySpanFontFamily || element.galleryTextFontFamily;
@ -197,7 +249,7 @@ export function buildGallerySpanGridStyle(
element: Partial<CanvasElement>,
): CSSProperties {
const columns = getGalleryGridColumns(element, 'span');
const gap = getTrimmedValue(element.gallerySpanGap) || '0.5rem';
const gap = normalizeRemValue(element.gallerySpanGap) || '0.5rem';
return {
display: 'grid',
@ -216,8 +268,8 @@ export function buildGalleryCardStyle(
const style: CSSProperties = {};
applyWithDefault(style, 'backgroundColor', element.galleryCardBackgroundColor, undefined);
applyWithDefault(style, 'borderRadius', element.galleryCardBorderRadius, defaults.borderRadius);
applyWithDefault(style, 'border', element.galleryCardBorder, undefined);
applyWithDefault(style, 'borderRadius', element.galleryCardBorderRadius, defaults.borderRadius, normalizeRemValue);
applyIfSet(style, 'border', element.galleryCardBorder); // Complex value, no normalization
return style;
}
@ -233,14 +285,14 @@ export function buildGalleryCardTitleStyle(
// Inheritable properties: only apply if explicitly set (allows CSS inheritance from wrapper)
// Note: card titles typically need white text for visibility over images
applyIfSet(style, 'color', element.galleryCardTitleColor);
applyIfSet(style, 'fontSize', element.galleryCardTitleFontSize);
applyIfSet(style, 'fontSize', element.galleryCardTitleFontSize, normalizeRemValue);
applyIfSet(style, 'fontWeight', element.galleryCardTitleFontWeight);
if (element.galleryCardTitleBackgroundColor) {
style.backgroundColor = element.galleryCardTitleBackgroundColor;
}
// Handle text shadow
// Handle text shadow - complex value, no normalization
const shadow = element.galleryCardTitleShadow;
if (shadow) {
style.textShadow = shadow;
@ -270,7 +322,7 @@ export function buildGalleryCardGridStyle(
element: Partial<CanvasElement>,
): CSSProperties {
const columns = getGalleryGridColumns(element, 'card');
const gap = getTrimmedValue(element.galleryCardGap) || '0.5rem';
const gap = normalizeRemValue(element.galleryCardGap) || '0.5rem';
return {
display: 'grid',