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 (
-
-
-
+
);
}
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 (
-
-
-
+
);
}