diff --git a/frontend/src/components/RuntimePresentation.tsx b/frontend/src/components/RuntimePresentation.tsx index 9bfe60a..d1ae29f 100644 --- a/frontend/src/components/RuntimePresentation.tsx +++ b/frontend/src/components/RuntimePresentation.tsx @@ -460,16 +460,20 @@ export default function RuntimePresentation({ element.type === 'navigation_prev' ) { if (element.iconUrl) { + // Use img tag with flexible sizing - auto for dimensions not provided + const imgStyle: React.CSSProperties = { + width: element.width || 'auto', + height: element.height || 'auto', + objectFit: 'contain', + }; + // eslint-disable-next-line @next/next/no-img-element return ( -
- Navigation -
+ Navigation ); } return ( diff --git a/frontend/src/lib/elementStyles.ts b/frontend/src/lib/elementStyles.ts index eed57fa..d07b2c4 100644 --- a/frontend/src/lib/elementStyles.ts +++ b/frontend/src/lib/elementStyles.ts @@ -70,10 +70,13 @@ export const ELEMENT_STYLE_PROPS = [ const NUMERIC_PROPS = ['opacity', 'zIndex'] as const; /** - * Get trimmed CSS value from unknown input + * 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(); }; diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx index 19212e9..5cf5a57 100644 --- a/frontend/src/pages/constructor.tsx +++ b/frontend/src/pages/constructor.tsx @@ -32,7 +32,7 @@ import { logger } from '../lib/logger'; import { resolveAssetPlaybackUrl } from '../lib/assetUrl'; import { parseJsonObject } from '../lib/parseJson'; import { waitForPageImages } from '../lib/imagePreDecode'; -import { buildElementStyle } from '../lib/elementStyles'; +import { buildElementStyle, ELEMENT_STYLE_PROPS } from '../lib/elementStyles'; import type { PreloadPageLink, PreloadElement } from '../types/preload'; import type { CanvasElementType, @@ -418,6 +418,23 @@ const mergeElementWithDefaults = ( yPercent: element.yPercent ?? defaults.yPercent ?? 50, }; + // For style properties, use defaults if element has empty/null/undefined value + // This ensures DB defaults are applied when element has no explicit value + const elementRecord = element as unknown as Record; + const defaultsRecord = defaults as unknown as Record; + const mergedRecord = merged as unknown as Record; + ELEMENT_STYLE_PROPS.forEach((prop) => { + const elementValue = elementRecord[prop]; + const defaultValue = defaultsRecord[prop]; + const elementIsEmpty = + elementValue === '' || elementValue === undefined || elementValue === null; + const defaultHasValue = + defaultValue !== undefined && defaultValue !== null && defaultValue !== ''; + if (elementIsEmpty && defaultHasValue) { + mergedRecord[prop] = defaultValue; + } + }); + merged.xPercent = clamp(Number(merged.xPercent ?? element.xPercent), 0, 100); merged.yPercent = clamp(Number(merged.yPercent ?? element.yPercent), 0, 100); merged.appearDelaySec = normalizeAppearDelaySec(merged.appearDelaySec); @@ -2089,17 +2106,20 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => { const fallbackNavLabel = getNavigationButtonLabel(element.type); const navigationLabel = element.navLabel?.trim() || fallbackNavLabel; if (element.iconUrl) { + // Use img tag with flexible sizing - auto for dimensions not provided + const imgStyle: React.CSSProperties = { + width: element.width || 'auto', + height: element.height || 'auto', + objectFit: 'contain', + }; + // eslint-disable-next-line @next/next/no-img-element return ( -
- -
+ Navigation icon ); }