diff --git a/apps/admin/src/components/category/CategoryHeroCarouselBlockForm.tsx b/apps/admin/src/components/category/CategoryHeroCarouselBlockForm.tsx index 73af690..961036d 100644 --- a/apps/admin/src/components/category/CategoryHeroCarouselBlockForm.tsx +++ b/apps/admin/src/components/category/CategoryHeroCarouselBlockForm.tsx @@ -265,6 +265,10 @@ export default function CategoryHeroCarouselBlockForm({ mobilePosition={getString(slide.mobileButtonPosition) || undefined} onChangeDesktop={(pos) => updateSlide(slideIndex, { buttonPosition: pos })} onChangeMobile={(pos) => updateSlide(slideIndex, { mobileButtonPosition: pos || '' })} + contentWidth={getString(slide.contentWidth, 'standard')} + onChangeContentWidth={(w) => updateSlide(slideIndex, { contentWidth: w })} + textColorScheme={getString(slide.textColorScheme, 'light')} + onChangeTextColorScheme={(scheme) => updateSlide(slideIndex, { textColorScheme: scheme })} title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)" /> diff --git a/apps/admin/src/components/page-builder/BannerForm.tsx b/apps/admin/src/components/page-builder/BannerForm.tsx index bd6814c..52550a6 100644 --- a/apps/admin/src/components/page-builder/BannerForm.tsx +++ b/apps/admin/src/components/page-builder/BannerForm.tsx @@ -106,6 +106,10 @@ export default function BannerForm({ mobilePosition={editingSection.payload?.mobileButtonPosition} onChangeDesktop={(pos) => updatePayloadValue('buttonPosition', pos)} onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)} + contentWidth={editingSection.payload?.contentWidth} + onChangeContentWidth={(w) => updatePayloadValue('contentWidth', w)} + textColorScheme={editingSection.payload?.textColorScheme} + onChangeTextColorScheme={(scheme) => updatePayloadValue('textColorScheme', scheme)} /> diff --git a/apps/admin/src/components/page-builder/ContentPositionPicker.tsx b/apps/admin/src/components/page-builder/ContentPositionPicker.tsx index 8ddbc28..06128cc 100644 --- a/apps/admin/src/components/page-builder/ContentPositionPicker.tsx +++ b/apps/admin/src/components/page-builder/ContentPositionPicker.tsx @@ -1,4 +1,4 @@ -import { Monitor, Smartphone, RotateCcw } from 'lucide-react'; +import { Monitor, Smartphone, RotateCcw, Maximize2, Sun, Moon, Palette } from 'lucide-react'; export const GRID_POSITIONS = [ { id: 'top-left', label: 'Top Left (บนซ้าย)', short: 'TL', icon: '↖' }, @@ -12,12 +12,39 @@ export const GRID_POSITIONS = [ { id: 'bottom-right', label: 'Bottom Right (ล่างขวา)', short: 'BR', icon: '↘' }, ]; +export const WIDTH_OPTIONS = [ + { id: 'full', label: 'ขอบนอกสุด (Full Bleed)', desc: 'เต็มความกว้างจอ ชิดขอบซ้าย/ขวา' }, + { id: 'standard', label: 'กรอบมาตรฐาน (1400px)', desc: 'ตามระยะเว็บหลัก max-w-7xl (แนะนำ)' }, + { id: 'compact', label: 'กรอบกระชับ (768px)', desc: 'กล่องกะทัดรัด max-w-3xl' }, +] as const; + +export const COLOR_SCHEME_OPTIONS = [ + { + id: 'light', + label: 'สว่าง (Light / ตัวอักษรสีขาว)', + desc: 'สำหรับภาพพื้นหลังโทนเข้ม หรือมี Overlay มืด (ค่าเริ่มต้น)', + icon: Sun, + badgeBg: 'bg-white/10 text-white border-white/20', + }, + { + id: 'dark', + label: 'มืด (Dark / ตัวอักษรสีเข้ม)', + desc: 'สำหรับภาพพื้นหลังโทนขาว หรือภาพสว่างสดใส', + icon: Moon, + badgeBg: 'bg-neutral-800 text-slate-200 border-white/10', + }, +] as const; + export interface ContentPositionPickerProps { desktopPosition?: string; mobilePosition?: string; defaultDesktop?: string; onChangeDesktop: (position: string) => void; onChangeMobile: (position: string | undefined) => void; + contentWidth?: string; + onChangeContentWidth?: (width: string) => void; + textColorScheme?: 'light' | 'dark' | string; + onChangeTextColorScheme?: (scheme: 'light' | 'dark') => void; title?: string; subtitle?: string; compact?: boolean; @@ -29,6 +56,10 @@ export default function ContentPositionPicker({ defaultDesktop = 'center', onChangeDesktop, onChangeMobile, + contentWidth, + onChangeContentWidth, + textColorScheme, + onChangeTextColorScheme, title = 'ตำแหน่งปุ่ม & เนื้อหา (Button & Content Position)', subtitle = 'สามารถแยกตำแหน่งระหว่าง Desktop (PC) และ Mobile (มือถือ) ได้อิสระ', compact = false, @@ -148,6 +179,82 @@ export default function ContentPositionPicker({ + + {/* Container Width / Outer Boundary Selection */} + {onChangeContentWidth && ( +
+
+ + + ความกว้างกรอบเนื้อหา (Content Width / Boundary) + + + {contentWidth || 'standard'} + +
+
+ {WIDTH_OPTIONS.map((opt) => { + const isSelected = (contentWidth || 'standard') === opt.id; + return ( + + ); + })} +
+
+ )} + + {/* Text Color Scheme Preset Selection */} + {onChangeTextColorScheme && ( +
+
+ + + โทนสีตัวอักษร (Text Color Scheme Preset) + + + {(textColorScheme || 'light').toUpperCase()} + +
+
+ {COLOR_SCHEME_OPTIONS.map((opt) => { + const isSelected = (textColorScheme || 'light') === opt.id; + const Icon = opt.icon; + return ( + + ); + })} +
+
+ )} ); } diff --git a/apps/admin/src/components/page-builder/HeroCarouselForm.tsx b/apps/admin/src/components/page-builder/HeroCarouselForm.tsx index f342d0b..25b76a5 100644 --- a/apps/admin/src/components/page-builder/HeroCarouselForm.tsx +++ b/apps/admin/src/components/page-builder/HeroCarouselForm.tsx @@ -381,6 +381,10 @@ export default function HeroCarouselForm({ mobilePosition={slide.mobileButtonPosition} onChangeDesktop={(pos) => updateSlidePayloadValue(idx, 'buttonPosition', pos)} onChangeMobile={(pos) => updateSlidePayloadValue(idx, 'mobileButtonPosition', pos || '')} + contentWidth={slide.contentWidth} + onChangeContentWidth={(w) => updateSlidePayloadValue(idx, 'contentWidth', w)} + textColorScheme={slide.textColorScheme} + onChangeTextColorScheme={(scheme) => updateSlidePayloadValue(idx, 'textColorScheme', scheme)} title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)" /> diff --git a/apps/admin/src/components/page-builder/HeroForm.tsx b/apps/admin/src/components/page-builder/HeroForm.tsx index dfa2439..08a5cd5 100644 --- a/apps/admin/src/components/page-builder/HeroForm.tsx +++ b/apps/admin/src/components/page-builder/HeroForm.tsx @@ -225,13 +225,17 @@ export default function HeroForm({ /> - {/* 9-Box Grid Button Position Selector (Desktop & Mobile) */} + {/* 9-Box Grid Button Position & Content Width & Text Color Scheme Selector */}
updatePayloadValue('buttonPosition', pos)} onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)} + contentWidth={editingSection.payload?.contentWidth} + onChangeContentWidth={(w) => updatePayloadValue('contentWidth', w)} + textColorScheme={editingSection.payload?.textColorScheme} + onChangeTextColorScheme={(scheme) => updatePayloadValue('textColorScheme', scheme)} />
diff --git a/apps/admin/src/components/page-builder/HeroVideoForm.tsx b/apps/admin/src/components/page-builder/HeroVideoForm.tsx index c33e17f..92b66f9 100644 --- a/apps/admin/src/components/page-builder/HeroVideoForm.tsx +++ b/apps/admin/src/components/page-builder/HeroVideoForm.tsx @@ -293,12 +293,16 @@ export default function HeroVideoForm({ - {/* Button & Content Position (9-Box Grid for Desktop & Mobile) */} + {/* Button & Content Position (9-Box Grid for Desktop & Mobile) & Color Scheme */} updatePayloadValue('buttonPosition', pos)} onChangeMobile={(pos) => updatePayloadValue('mobileButtonPosition', pos)} + contentWidth={payload.contentWidth} + onChangeContentWidth={(w) => updatePayloadValue('contentWidth', w)} + textColorScheme={payload.textColorScheme} + onChangeTextColorScheme={(scheme) => updatePayloadValue('textColorScheme', scheme)} /> ); diff --git a/apps/admin/src/components/products/ProductHeroCarouselBlockForm.tsx b/apps/admin/src/components/products/ProductHeroCarouselBlockForm.tsx index 3e028e6..f222698 100644 --- a/apps/admin/src/components/products/ProductHeroCarouselBlockForm.tsx +++ b/apps/admin/src/components/products/ProductHeroCarouselBlockForm.tsx @@ -265,6 +265,10 @@ export default function ProductHeroCarouselBlockForm({ mobilePosition={getString(slide.mobileButtonPosition) || undefined} onChangeDesktop={(pos) => updateSlide(slideIndex, { buttonPosition: pos })} onChangeMobile={(pos) => updateSlide(slideIndex, { mobileButtonPosition: pos || '' })} + contentWidth={getString(slide.contentWidth, 'standard')} + onChangeContentWidth={(w) => updateSlide(slideIndex, { contentWidth: w })} + textColorScheme={getString(slide.textColorScheme, 'light')} + onChangeTextColorScheme={(scheme) => updateSlide(slideIndex, { textColorScheme: scheme })} title="ตำแหน่งปุ่ม & ข้อความของสไลด์นี้ (Slide Content Position)" /> diff --git a/apps/backend/src/routes/pages.ts b/apps/backend/src/routes/pages.ts index 10a8f18..56771e1 100644 --- a/apps/backend/src/routes/pages.ts +++ b/apps/backend/src/routes/pages.ts @@ -1,6 +1,6 @@ import { Elysia, t } from "elysia"; import prisma from "../lib/prisma"; -// Trigger reload: resilient position schemas with empty string handling +// Trigger reload: resilient position and contentWidth support import { LocalizedStringSchema, HeroSectionPayloadSchema, diff --git a/apps/spa/src/components/DynamicBanner.tsx b/apps/spa/src/components/DynamicBanner.tsx index d4a46f5..4d532f8 100644 --- a/apps/spa/src/components/DynamicBanner.tsx +++ b/apps/spa/src/components/DynamicBanner.tsx @@ -20,8 +20,17 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) { const desktopPosition = payload.buttonPosition || 'center-left'; const mobilePosition = payload.mobileButtonPosition || desktopPosition; + const contentWidth = (payload as any).contentWidth || 'standard'; + const textColorScheme = (payload as any).textColorScheme || 'light'; + const isDarkText = textColorScheme === 'dark'; const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition); + const shellWidthClass = contentWidth === 'full' + ? '!max-w-none !rounded-none md:!rounded-3xl mx-4 md:mx-8' + : contentWidth === 'compact' + ? '!max-w-4xl' + : '!max-w-7xl'; + const bannerInner = ( <>
@@ -31,23 +40,23 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) { alt={title || "Banner"} style={imageStyle} /> -
+
{title && ( -

+

{title}

)} {description && ( -

+

{description}

)} {buttonText && (
- + {buttonText}
@@ -59,11 +68,11 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) { return (
{linkUrl ? ( - + {bannerInner} ) : ( -
+
{bannerInner}
)} diff --git a/apps/spa/src/components/DynamicHero.tsx b/apps/spa/src/components/DynamicHero.tsx index 290ab53..a19cfe4 100644 --- a/apps/spa/src/components/DynamicHero.tsx +++ b/apps/spa/src/components/DynamicHero.tsx @@ -139,12 +139,25 @@ export function getHeroPositionClasses(mobilePos: string, desktopPos: string) { }; } +export function getHeroContainerWidthClasses(contentWidth?: string) { + switch (contentWidth) { + case 'full': + return 'w-full max-w-none px-6 md:px-12 lg:px-16'; + case 'compact': + return 'w-full max-w-4xl mx-auto px-6'; + case 'standard': + default: + return 'w-full max-w-7xl mx-auto px-6 md:px-10 lg:px-12'; + } +} + export default function DynamicHero({ payload }: DynamicHeroProps) { const { t, i18n } = useTranslation(); const lang = i18n.language; const { imageUrl, buttonLink } = payload; const desktopPosition = (payload as any).buttonPosition || 'center'; const mobilePosition = (payload as any).mobileButtonPosition || desktopPosition; + const contentWidth = (payload as any).contentWidth || 'standard'; const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined; // Localized values @@ -167,6 +180,9 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { ? rawButtonText.trim() : t('hero.exploreCollection'); + const textColorScheme = (payload as any).textColorScheme || 'light'; + const isDarkText = textColorScheme === 'dark'; + const renderTitle = (text: string) => { if (!text) return null; return text.split('[').map((part, i) => { @@ -174,7 +190,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { const [accent, rest] = part.split(']'); return ( - + {accent} {rest} @@ -201,7 +217,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { return (
); } @@ -211,18 +227,22 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { const midAlpha = opacityRatio; const topAlpha = Math.max(0, opacityRatio * 0.65); + const rgb = isDarkText ? '255, 255, 255' : '0, 0, 0'; + return (
); }; + const containerClasses = getHeroContainerWidthClasses(contentWidth); + return ( -
+
- {/* Dynamic Dark Gradient / Solid Overlay */} + {/* Dynamic Dark / Light Gradient / Solid Overlay */} {renderOverlay()}
-
+
{tagline && ( -

+

{tagline}

)} {title && ( -

+

{renderTitle(title)}

)} {description && ( -

+

{description}

)} @@ -265,7 +285,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) { )} {t('hero.learnMore')} → diff --git a/apps/spa/src/components/DynamicHeroCarousel.tsx b/apps/spa/src/components/DynamicHeroCarousel.tsx index 3a99fe2..d37eba6 100644 --- a/apps/spa/src/components/DynamicHeroCarousel.tsx +++ b/apps/spa/src/components/DynamicHeroCarousel.tsx @@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'; import type { HeroCarouselSectionPayload } from '@homaroma/types'; import { useTranslation } from 'react-i18next'; import { getLocalizedField } from '../utils/localization'; -import { getHeroPositionClasses } from './DynamicHero'; +import { getHeroPositionClasses, getHeroContainerWidthClasses } from './DynamicHero'; // Import Swiper styles import 'swiper/css'; @@ -119,28 +119,34 @@ export default function DynamicHeroCarousel({ payload }: DynamicHeroCarouselProp {(slide.title || slide.subtitle || slide.buttonText) && (() => { const slideDesktopPos = (slide as any).buttonPosition || 'bottom-left'; const slideMobilePos = (slide as any).mobileButtonPosition || slideDesktopPos; + const slideWidth = (slide as any).contentWidth || 'standard'; + const slideColor = (slide as any).textColorScheme || 'light'; + const isDarkSlide = slideColor === 'dark'; const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(slideMobilePos, slideDesktopPos); + const containerClasses = getHeroContainerWidthClasses(slideWidth); return ( -
-
- {slide.subtitle && ( -

- {slide.subtitle} -

- )} - {slide.title && ( -

- {renderTitle(slide.title)} -

- )} - {slide.buttonText && ( -
- - {slide.buttonText} - -
- )} +
+
+
+ {slide.subtitle && ( +

+ {slide.subtitle} +

+ )} + {slide.title && ( +

+ {renderTitle(slide.title)} +

+ )} + {slide.buttonText && ( +
+ + {slide.buttonText} + +
+ )} +
); diff --git a/apps/spa/src/components/DynamicHeroVideo.tsx b/apps/spa/src/components/DynamicHeroVideo.tsx index 0852044..e351d82 100644 --- a/apps/spa/src/components/DynamicHeroVideo.tsx +++ b/apps/spa/src/components/DynamicHeroVideo.tsx @@ -2,7 +2,7 @@ import { Link } from 'react-router-dom'; import type { HeroVideoSectionPayload } from '@homaroma/types'; import { useTranslation } from 'react-i18next'; import { getLocalizedField } from '../utils/localization'; -import { getHeroPositionClasses } from './DynamicHero'; +import { getHeroPositionClasses, getHeroContainerWidthClasses } from './DynamicHero'; interface DynamicHeroVideoProps { payload: HeroVideoSectionPayload; @@ -92,60 +92,73 @@ export default function DynamicHeroVideo({ payload }: DynamicHeroVideoProps) { ) : null} {/* Customizable Overlay Opacity */} -
- - {/* Overlay Typography Content */} {(() => { + const textColorScheme = (payload as any).textColorScheme || 'light'; + const isDarkText = textColorScheme === 'dark'; const desktopPosition = (payload as any).buttonPosition || 'center'; const mobilePosition = (payload as any).mobileButtonPosition || desktopPosition; + const contentWidth = (payload as any).contentWidth || 'standard'; const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition); + const containerClasses = getHeroContainerWidthClasses(contentWidth); return ( -
-
- {tagline && ( -

- {tagline} -

- )} + <> +
-

- {renderTitle(title) || renderTitle(t('hero.defaultTitle'))} -

+
+
+ {tagline && ( +

+ {tagline} +

+ )} - {description && ( -

- {description} -

- )} +

+ {renderTitle(title) || renderTitle(t('hero.defaultTitle'))} +

- {/* Dual Action Buttons */} - {(buttonText || secondaryButtonText) && ( -
- {buttonText && payload.buttonLink && ( - - {buttonText} - - )} + {description && ( +

+ {description} +

+ )} - {secondaryButtonText && payload.secondaryButtonLink && ( - - {secondaryButtonText} - - )} -
- )} + {/* Dual Action Buttons */} + {(buttonText || secondaryButtonText) && ( +
+ {buttonText && payload.buttonLink && ( + + {buttonText} + + )} + + {secondaryButtonText && payload.secondaryButtonLink && ( + + {secondaryButtonText} + + )} +
+ )} +
-
+ ); })()}
diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index 746d10e..5f5d960 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -227,6 +227,18 @@ function getHeroPositionClasses(mobilePos: string, desktopPos: string) { } } +function getHeroContainerWidthClasses(contentWidth?: string) { + switch (contentWidth) { + case 'full': + return 'w-full max-w-none px-6 md:px-12 lg:px-16' + case 'compact': + return 'w-full max-w-4xl mx-auto px-6' + case 'standard': + default: + return 'w-full max-w-7xl mx-auto px-6 md:px-10 lg:px-12' + } +} + function HeroSection({ payload, lang }: { payload: Record, lang: string }) { const { t } = useTranslation() const i18nLang = useLang() @@ -239,6 +251,9 @@ function HeroSection({ payload, lang }: { payload: Record, lang const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop' const desktopPosition = typeof payload.buttonPosition === 'string' ? payload.buttonPosition : 'center' const mobilePosition = typeof payload.mobileButtonPosition === 'string' ? payload.mobileButtonPosition : desktopPosition + const contentWidth = typeof payload.contentWidth === 'string' ? payload.contentWidth : 'standard' + const textColorScheme = typeof payload.textColorScheme === 'string' ? payload.textColorScheme : 'light' + const isDarkText = textColorScheme === 'dark' const focalPoint = typeof payload.focalPoint === 'object' && payload.focalPoint ? (payload.focalPoint as { x: string, y: string }) : undefined // If fields are explicitly defined as empty strings, respect them as empty @@ -256,6 +271,7 @@ function HeroSection({ payload, lang }: { payload: Record, lang : 'Explore Collection' const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition) + const containerClasses = getHeroContainerWidthClasses(contentWidth) const overlayStyle = typeof payload.overlayStyle === 'string' ? payload.overlayStyle : 'gradient' const overlayOpacity = typeof payload.overlayOpacity === 'number' ? payload.overlayOpacity : 45 @@ -266,12 +282,13 @@ function HeroSection({ payload, lang }: { payload: Record, lang } const opacityRatio = overlayOpacity / 100 + const rgb = isDarkText ? '255, 255, 255' : '0, 0, 0' if (overlayStyle === 'solid') { return (
) } @@ -285,14 +302,14 @@ function HeroSection({ payload, lang }: { payload: Record, lang
) } return ( -
+
, lang className="object-cover md:scale-[1.02]" style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }} /> - {/* Dynamic Dark Gradient / Solid Overlay */} + {/* Dynamic Dark / Light Gradient / Solid Overlay */} {renderOverlay()}
-
+
{tagline && ( -

+

{tagline}

)} {title && ( -

- {renderAccentTitle(title, 'ui-accent-text font-medium')} +

+ {renderAccentTitle(title, isDarkText ? 'text-brand-primary font-medium' : 'ui-accent-text font-medium')}

)} {description && ( -

+

{description}

)} @@ -333,7 +350,7 @@ function HeroSection({ payload, lang }: { payload: Record, lang )}
{t('dynamic.heroLearnMore', { lng: resolvedLang })} → @@ -438,19 +455,25 @@ function HeroCarouselSection({ payload, lang }: { payload: Record { const slideDesktopPos = typeof slide.buttonPosition === 'string' ? slide.buttonPosition : 'bottom-left' const slideMobilePos = typeof slide.mobileButtonPosition === 'string' ? slide.mobileButtonPosition : slideDesktopPos + const slideWidth = typeof slide.contentWidth === 'string' ? slide.contentWidth : 'standard' + const slideColor = typeof slide.textColorScheme === 'string' ? slide.textColorScheme : 'light' + const isDarkSlide = slideColor === 'dark' const { wrapperClasses: sWrap, contentClasses: sContent, actionsClasses: sActions } = getHeroPositionClasses(slideMobilePos, slideDesktopPos) + const sContainer = getHeroContainerWidthClasses(slideWidth) return ( -
-
- {subtitle &&

{subtitle}

} - {title &&

{renderAccentTitle(title)}

} - {buttonText && ( -
- - {buttonText} - -
- )} +
+
+
+ {subtitle &&

{subtitle}

} + {title &&

{renderAccentTitle(title)}

} + {buttonText && ( +
+ + {buttonText} + +
+ )} +
) @@ -505,10 +528,14 @@ function HeroVideoSection({ payload, lang }: { payload: Record, const desktopPosition = typeof payload.buttonPosition === 'string' ? payload.buttonPosition : 'center' const mobilePosition = typeof payload.mobileButtonPosition === 'string' ? payload.mobileButtonPosition : desktopPosition + const contentWidth = typeof payload.contentWidth === 'string' ? payload.contentWidth : 'standard' + const textColorScheme = typeof payload.textColorScheme === 'string' ? payload.textColorScheme : 'light' + const isDarkText = textColorScheme === 'dark' const { wrapperClasses, contentClasses, actionsClasses } = getHeroPositionClasses(mobilePosition, desktopPosition) + const containerClasses = getHeroContainerWidthClasses(contentWidth) return ( -
+
{videoUrl ? (