diff --git a/apps/admin/src/components/page-builder/ProductCarouselForm.tsx b/apps/admin/src/components/page-builder/ProductCarouselForm.tsx index 6319186..34126bb 100644 --- a/apps/admin/src/components/page-builder/ProductCarouselForm.tsx +++ b/apps/admin/src/components/page-builder/ProductCarouselForm.tsx @@ -126,8 +126,62 @@ export default function ProductCarouselForm({ } className="admin-input w-full bg-slate-900 border-white/10" > - - + + + + + + +
+ + +
+ +
+ + +
+ +
+ +
diff --git a/apps/admin/src/components/page-builder/ProductsForm.tsx b/apps/admin/src/components/page-builder/ProductsForm.tsx index 558c2ce..7157109 100644 --- a/apps/admin/src/components/page-builder/ProductsForm.tsx +++ b/apps/admin/src/components/page-builder/ProductsForm.tsx @@ -116,6 +116,42 @@ export default function ProductsForm({ +
+ + +
+ +
+ + +
+
- {/* ── 4. Desktop (PC) Header Settings ── */} + {/* ── 4. Product Card UI Presets ── */} +
+
+
+ +
+
+

Product Card UI Preset

+

เลือกรูปแบบการ์ดสินค้าหลักสำหรับทั้งร้านค้า (Catalog, Search & Default)

+
+
+ +
+ {[ + { + id: 'standard', + title: 'Standard Retail', + badge: 'Default Retail', + desc: 'กล่องการ์ดมีขอบ, ภาพ 1:1 จัตุรัส, ปุ่ม Select Options / Add to Cart ใต้ราคา เหมาะกับร้านค้าทั่วไปหรือสินค้าอิเล็กทรอนิกส์/ของใช้', + features: ['1:1 Square Image', 'Bordered Card Container', 'Action Button (Add to Cart)', 'Category Badge'], + }, + { + id: 'minimal', + title: 'Fashion Minimal', + badge: 'Pomelo / Zara Style', + desc: 'สไตล์แฟชั่นมินิมอล ภาพแนวตั้ง 3:4 ไร้กรอบแบนราบ แบรนด์ตัวหนา หัวใจ Wishlist แสดงยอดขาย คลีนหรูหรา', + features: ['3:4 Portrait Image', 'Borderless Flat Layout', 'Brand Name & Wishlist Heart', 'Price & Stock Status'], + }, + { + id: 'swatch', + title: 'Editorial Swatch', + badge: 'Lookbook / New Arrivals', + desc: 'สไตล์นิตยสารมินิมอล เน้นจุดสีกลม (Color Swatch Dots) และชื่อสินค้าแบบเรียบหรู เหมาะกับสินค้าหลายเฉดสี', + features: ['Color Swatch Dots', 'Clean Typography', 'Frameless Lookbook', 'Price with Subtitle'], + }, + ].map((preset) => { + const isSelected = cardPreset === preset.id + return ( +
setCardPreset(preset.id as any)} + className={`relative p-5 rounded-2xl border transition-all cursor-pointer flex flex-col justify-between ${ + isSelected + ? 'border-emerald-500/60 bg-emerald-500/10 ring-1 ring-emerald-500/40 shadow-lg' + : 'border-white/10 bg-night-900/60 hover:border-white/20 hover:bg-white/5' + }`} + > +
+
+ + {preset.badge} + +
+ {isSelected &&
} +
+
+ +
+

{preset.title}

+

{preset.desc}

+
+ +
+ {preset.features.map((feat, idx) => ( +
+ ✓ + {feat} +
+ ))} +
+
+
+ ) + })} +
+
+ + {/* ── 5. Desktop (PC) Header Settings ── */}
diff --git a/apps/backend/src/routes/pages.ts b/apps/backend/src/routes/pages.ts index 569cff1..ac7b1b4 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: support paddingTop, paddingBottom, and sectionPadding for FEATURED_TILES +// Trigger reload: support containerWidth, cardSize, and layout for PRODUCT_CAROUSEL and PRODUCTS import { LocalizedStringSchema, HeroSectionPayloadSchema, diff --git a/apps/backend/src/routes/settings.ts b/apps/backend/src/routes/settings.ts index 947836c..c9fdc98 100644 --- a/apps/backend/src/routes/settings.ts +++ b/apps/backend/src/routes/settings.ts @@ -188,6 +188,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) const headerBehavior = (settings.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll'; const mobileHeaderBehavior = (settings.themeTokens as any)?.mobileHeaderBehavior ?? headerBehavior; const mobileHeaderStyle = (settings.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center'; + const cardPreset = (settings.themeTokens as any)?.cardPreset ?? 'standard'; return { ...settingsData, footerLayout: normalizeFooterLayoutSettings((settingsData as any).footerLayout), @@ -195,6 +196,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, + cardPreset, footerDescription: localizedDescription ?? settingsData.footerDescription, defaultSeoTitle: localizedSeoTitle ?? settingsData.defaultSeoTitle, defaultSeoDescription: localizedSeoDescription ?? settingsData.defaultSeoDescription, @@ -369,7 +371,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) }, { beforeHandle: requireAdmin, body: t.Any() }) .put("/", async ({ body, set }: { body: any; set: any }) => { try { - const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any; + const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, cardPreset, translations, ...data } = body as any; if (footerLayout !== undefined) { set.status = 400; return { error: "Use PATCH /settings/footer-layout to update footer layout" }; @@ -387,6 +389,9 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) if (mobileHeaderStyle !== undefined) { updatedTokens.mobileHeaderStyle = mobileHeaderStyle; } + if (cardPreset !== undefined) { + updatedTokens.cardPreset = cardPreset; + } data.themeTokens = updatedTokens; let settings = await prisma.storeSettings.findFirst({ include: { translations: true } }); @@ -410,11 +415,13 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) const returnedBehavior = (settings?.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll'; const returnedMobileBehavior = (settings?.themeTokens as any)?.mobileHeaderBehavior ?? returnedBehavior; const returnedMobileStyle = (settings?.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center'; + const returnedCardPreset = (settings?.themeTokens as any)?.cardPreset ?? 'standard'; return { ...settings, headerBehavior: returnedBehavior, mobileHeaderBehavior: returnedMobileBehavior, mobileHeaderStyle: returnedMobileStyle, + cardPreset: returnedCardPreset, }; } catch (err) { console.error("[Settings] Error:", err); @@ -449,6 +456,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) headerBehavior: t.Optional(t.String()), mobileHeaderBehavior: t.Optional(t.String()), mobileHeaderStyle: t.Optional(t.String()), + cardPreset: t.Optional(t.String()), defaultSeoTitle: t.Optional(t.Nullable(t.String())), defaultSeoDescription: t.Optional(t.Nullable(t.String())), defaultOgImage: t.Optional(t.Nullable(t.String())), @@ -459,7 +467,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) }) .post("/", async ({ body, set }: { body: any; set: any }) => { try { - const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, translations, ...data } = body as any; + const { footerLayout, headerBehavior, mobileHeaderBehavior, mobileHeaderStyle, cardPreset, translations, ...data } = body as any; if (footerLayout !== undefined) { set.status = 400; return { error: "Use PATCH /settings/footer-layout to update footer layout" }; @@ -477,6 +485,9 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) if (mobileHeaderStyle !== undefined) { updatedTokens.mobileHeaderStyle = mobileHeaderStyle; } + if (cardPreset !== undefined) { + updatedTokens.cardPreset = cardPreset; + } data.themeTokens = updatedTokens; let settings = await prisma.storeSettings.findFirst({ include: { translations: true } }); @@ -500,11 +511,13 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) const returnedBehavior = (settings?.themeTokens as any)?.headerBehavior ?? 'hide_on_scroll'; const returnedMobileBehavior = (settings?.themeTokens as any)?.mobileHeaderBehavior ?? returnedBehavior; const returnedMobileStyle = (settings?.themeTokens as any)?.mobileHeaderStyle ?? 'logo_center'; + const returnedCardPreset = (settings?.themeTokens as any)?.cardPreset ?? 'standard'; return { ...settings, headerBehavior: returnedBehavior, mobileHeaderBehavior: returnedMobileBehavior, mobileHeaderStyle: returnedMobileStyle, + cardPreset: returnedCardPreset, }; } catch (err) { console.error("[Settings] Error:", err); @@ -539,6 +552,7 @@ export const settingsRoute = new Elysia({ prefix: "/settings" }) headerBehavior: t.Optional(t.String()), mobileHeaderBehavior: t.Optional(t.String()), mobileHeaderStyle: t.Optional(t.String()), + cardPreset: t.Optional(t.String()), defaultSeoTitle: t.Optional(t.Nullable(t.String())), defaultSeoDescription: t.Optional(t.Nullable(t.String())), defaultOgImage: t.Optional(t.Nullable(t.String())), diff --git a/apps/spa/src/components/DynamicProductCarousel.tsx b/apps/spa/src/components/DynamicProductCarousel.tsx index 9d2d7ab..cf38837 100644 --- a/apps/spa/src/components/DynamicProductCarousel.tsx +++ b/apps/spa/src/components/DynamicProductCarousel.tsx @@ -19,18 +19,57 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous const title = getLocalizedField(p.title, lang); const description = getLocalizedField(p.description, lang); const cardStyle = p.cardStyle || 'standard'; - const isMinimal = cardStyle === 'minimal_swatch'; + const isMinimal = cardStyle === 'minimal' || cardStyle === 'swatch' || cardStyle === 'minimal_swatch'; const viewAllLink = p.viewAllLink || '/products'; const viewAllText = getLocalizedField(p.viewAllText, lang) || 'View All'; const badgeText = getLocalizedField(p.badgeText, lang); + const layout = p.layout || 'carousel'; + const containerWidth = p.containerWidth || 'standard'; + const cardSize = p.cardSize || 'medium'; + + const getContainerWidthClass = (width: string) => { + switch (width) { + case 'full': + return 'w-full max-w-none'; + case 'wide': + return 'w-full max-w-[1600px] mx-auto'; + case 'standard': + default: + return 'ui-section-inner'; + } + }; + + const getCarouselCardWidth = () => { + switch (cardSize) { + case 'compact': + return 'w-[46vw] sm:w-[calc((100%-32px)/3)] md:w-[calc((100%-60px)/4)] lg:w-[calc((100%-100px)/6)] flex-none snap-start'; + case 'large': + return 'w-[78vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-20px)/2)] lg:w-[calc((100%-40px)/3)] flex-none snap-start'; + case 'medium': + default: + return 'w-[68vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-40px)/3)] lg:w-[calc((100%-60px)/4)] flex-none snap-start'; + } + }; + + const getGridColsClass = () => { + switch (cardSize) { + case 'compact': + return 'grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6'; + case 'large': + return 'grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'; + case 'medium': + default: + return 'grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'; + } + }; const carouselRef = useRef(null); const scrollLeft = () => carouselRef.current?.scrollBy({ left: -300, behavior: 'smooth' }); const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' }); return ( -
-
+
+
{isMinimal ? (
@@ -55,6 +94,18 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
No products to display.
+ ) : layout === 'grid' ? ( +
+ {products.map((prod: any) => ( + + ))} +
) : (
diff --git a/apps/spa/src/components/DynamicProducts.tsx b/apps/spa/src/components/DynamicProducts.tsx index 47c216a..8f71395 100644 --- a/apps/spa/src/components/DynamicProducts.tsx +++ b/apps/spa/src/components/DynamicProducts.tsx @@ -20,13 +20,53 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro const subtitle = getLocalizedField(payload.subtitle, lang); const categoryLookup = new Map(categories.map((category) => [category.id, category])); + const containerWidth = payload.containerWidth || 'standard'; + + const getContainerWidthClass = (width: string) => { + switch (width) { + case 'full': + return 'w-full max-w-none'; + case 'wide': + return 'w-full max-w-[1600px] mx-auto'; + case 'standard': + default: + return 'ui-section-inner'; + } + }; + + const cardSize = payload.cardSize || 'medium'; + + const getCarouselCardWidth = () => { + switch (cardSize) { + case 'compact': + return 'w-[46vw] sm:w-[calc((100%-32px)/3)] md:w-[calc((100%-60px)/4)] lg:w-[calc((100%-100px)/6)] flex-none snap-start'; + case 'large': + return 'w-[78vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-20px)/2)] lg:w-[calc((100%-40px)/3)] flex-none snap-start'; + case 'medium': + default: + return 'w-[68vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-40px)/3)] lg:w-[calc((100%-60px)/4)] flex-none snap-start'; + } + }; + + const getGridColsClass = () => { + switch (cardSize) { + case 'compact': + return 'grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6'; + case 'large': + return 'grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3'; + case 'medium': + default: + return 'grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'; + } + }; + const carouselRef = useRef(null); const scrollLeft = () => carouselRef.current?.scrollBy({ left: -300, behavior: 'smooth' }); const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' }); return ( -
-
+
+

@@ -60,7 +100,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
{products.map((p) => ( -
+
) : ( -
+
{products.map((p) => (
0 ? product.images!.slice(0, 3).map(img => img.url) : [fallbackImage]; const hasMultipleImages = images.length > 1; + const displayPrice = selectedVariant?.price ?? product.variants?.[0]?.price ?? 0; + const totalStock = product.variants?.reduce((sum, v) => sum + (v.stock ?? 0), 0) ?? 0; const handleWishlistClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -83,6 +95,123 @@ export default function ProductCard({ product, enableQuickShop = false, category navigate(`/products/${product.id}`); }; + if (cardStyle === 'swatch' || cardStyle === 'minimal_swatch') { + return ; + } + + if (cardStyle === 'minimal') { + return ( +
+ {/* 3:4 Image Frame */} +
+ {hasMultipleImages ? ( + <> + {/* Desktop Hover Effect */} +
+ {displayName} + {displayName} +
+ + {/* Mobile Native Slider */} +
e.stopPropagation()}> +
+ {images.map((img, idx) => ( +
+ {`${displayName} +
+ ))} +
+
+ {images.map((_, idx) => ( +
+ ))} +
+
+ + ) : ( + {displayName} { + (e.target as HTMLImageElement).style.display = 'none'; + }} + /> + )} + + {badgeText && ( + + {badgeText} + + )} +
+ + {/* Content Info (Fashion Minimal / Pomelo Style) */} +
+ {/* Row 1: Brand Name + Wishlist */} +
+ + {product.brand?.name || 'Brand'} + + +
+ + {/* Row 2: Product Name */} +

+ {displayName} +

+ + {/* Row 3: Price + Status */} +
+ + {formatPrice(displayPrice)} + + {totalStock > 0 ? ( + + {t('product.inStock', 'In Stock')} + + ) : ( + + {t('product.outOfStock', 'Out of Stock')} + + )} +
+ + {/* Row 4: Swatch Dots */} + {colorSwatches.length > 0 && ( +
+ {colorSwatches.slice(0, 5).map(({ colorCode, variant, label }) => ( + + ))} + {colorSwatches.length > 5 && ( + + +{colorSwatches.length - 5} + + )} +
+ )} +
+
+ ); + } + return (
- {/* Color Swatch Dots */} + {/* Color Swatch Dots (Display Only) */} {colorSwatches.length > 0 && ( -
- {colorSwatches.map(({ colorCode, variant }) => { - const isActive = activeVariant?.colorCode === colorCode; - return ( -
diff --git a/apps/website/src/components/ProductCard.tsx b/apps/website/src/components/ProductCard.tsx index 33f2be5..3cfb692 100644 --- a/apps/website/src/components/ProductCard.tsx +++ b/apps/website/src/components/ProductCard.tsx @@ -11,12 +11,15 @@ import { generateProductSlug } from '../lib/slugify' import { useLang } from '../hooks/useLang' import { useCartStore } from '../store/cartStore' import WishlistButton from './WishlistButton' +import ProductSwatchCard from './ProductSwatchCard' interface ProductCardProps { product: Product + cardStyle?: 'standard' | 'minimal' | 'swatch' | 'minimal_swatch' + badgeText?: string | null } -export default function ProductCard({ product }: ProductCardProps) { +export default function ProductCard({ product, cardStyle = 'standard', badgeText }: ProductCardProps) { const lang = useLang() const { addToCart } = useCartStore() const [selectedVariant, setSelectedVariant] = useState(product.variants?.[0] || null) @@ -79,6 +82,116 @@ export default function ProductCard({ product }: ProductCardProps) { const optionsText = optionsLabels[lang] || optionsLabels.en const selectOptionsText = selectOptionsLabels[lang] || selectOptionsLabels.en + if (cardStyle === 'swatch' || cardStyle === 'minimal_swatch') { + return + } + + if (cardStyle === 'minimal') { + return ( +
+ {/* 3:4 Image Frame */} +
+ {hasMultipleImages ? ( + <> + {/* Desktop Hover Effect */} + + {productName} + {productName} + + + {/* Mobile Native Slider */} +
+
+ {images.map((img, idx) => ( + + {`${productName} + + ))} +
+
+ {images.map((_, idx) => ( +
+ ))} +
+
+ + ) : ( + + {productName} + + )} + + {badgeText && ( + + {badgeText} + + )} +
+ + {/* Content Info (Fashion Minimal / Pomelo Style) */} +
+ {/* Row 1: Brand Name + Wishlist */} +
+ + {product.brand?.name || 'Brand'} + +
+ +
+
+ + {/* Row 2: Product Name */} + +

+ {productName} +

+ + + {/* Row 3: Price + Status */} +
+ + {formatPrice(displayPrice)} + + {totalStock > 0 ? ( + + มีสินค้า + + ) : ( + + สินค้าหมด + + )} +
+ + {/* Row 4: Swatch Dots */} + {colorSwatches.length > 0 && ( +
+ {colorSwatches.slice(0, 5).map(({ colorCode, variant, label }) => ( + + ))} + {colorSwatches.length > 5 && ( + + +{colorSwatches.length - 5} + + )} +
+ )} +
+
+ ) + } + return (
diff --git a/apps/website/src/components/ProductSwatchCard.tsx b/apps/website/src/components/ProductSwatchCard.tsx index bb4e331..5e12b83 100644 --- a/apps/website/src/components/ProductSwatchCard.tsx +++ b/apps/website/src/components/ProductSwatchCard.tsx @@ -91,34 +91,22 @@ export default function ProductSwatchCard({ product, badgeText }: ProductSwatchC {formatPrice(displayPrice)}
- {/* Color Swatch Dots */} + {/* Color Swatch Dots (Display Only) */} {colorSwatches.length > 0 && ( -
- {colorSwatches.map(({ colorCode, variant }) => { - const isActive = activeVariant?.colorCode === colorCode - return ( -
diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index c5d20b5..fd4ac77 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -1132,11 +1132,49 @@ function ProductsSection({ payload, lang }: { payload: Record, const scrollLeft = () => carouselRef.current?.scrollBy({ left: -300, behavior: 'smooth' }) const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' }) - const isMinimal = cardStyle === 'minimal_swatch' + const isMinimal = cardStyle === 'minimal' || cardStyle === 'swatch' || cardStyle === 'minimal_swatch' + const containerWidth = typeof payload.containerWidth === 'string' ? payload.containerWidth : 'standard' + const cardSize = typeof payload.cardSize === 'string' ? payload.cardSize : 'medium' + + const getContainerWidthClass = (width: string) => { + switch (width) { + case 'full': + return 'w-full max-w-none' + case 'wide': + return 'w-full max-w-[1600px] mx-auto' + case 'standard': + default: + return 'w-full max-w-7xl mx-auto' + } + } + + const getCarouselCardWidth = () => { + switch (cardSize) { + case 'compact': + return 'w-[46vw] sm:w-[calc((100%-32px)/3)] md:w-[calc((100%-60px)/4)] lg:w-[calc((100%-100px)/6)] flex-none snap-start' + case 'large': + return 'w-[78vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-20px)/2)] lg:w-[calc((100%-40px)/3)] flex-none snap-start' + case 'medium': + default: + return 'w-[68vw] sm:w-[calc((100%-16px)/2)] md:w-[calc((100%-40px)/3)] lg:w-[calc((100%-60px)/4)] flex-none snap-start' + } + } + + const getGridColsClass = () => { + switch (cardSize) { + case 'compact': + return 'grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6' + case 'large': + return 'grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3' + case 'medium': + default: + return 'grid grid-cols-2 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4' + } + } return ( -
-
+
+

@@ -1168,17 +1206,9 @@ function ProductsSection({ payload, lang }: { payload: Record, {products.map((product) => (
- {isMinimal ? ( - - ) : ( - - )} +
))}

@@ -1191,14 +1221,10 @@ function ProductsSection({ payload, lang }: { payload: Record,
) : ( -
- {products.map((product) => - isMinimal ? ( - - ) : ( - - ) - )} +
+ {products.map((product) => ( + + ))}
) ) : ( @@ -2595,7 +2621,7 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr case 'PRODUCT_CAROUSEL': return ( - + ) case 'BANNER': diff --git a/packages/types/index.ts b/packages/types/index.ts index d259037..a270abd 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -407,6 +407,7 @@ export interface StoreSettings { headerBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null mobileHeaderStyle?: 'logo_center' | 'logo_left' | 'search_bar_below' | string | null mobileHeaderBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null + cardPreset?: 'standard' | 'minimal' | 'swatch' | string | null defaultSeoTitle?: string | null defaultSeoDescription?: string | null defaultOgImage?: string | null @@ -680,6 +681,8 @@ export const ProductsSectionPayloadSchema = z.object({ limit: z.number().optional(), layout: z.enum(['grid', 'carousel']).optional(), enableQuickShop: z.boolean().optional(), + containerWidth: z.enum(['standard', 'wide', 'full']).optional().default('standard'), + cardSize: z.enum(['compact', 'medium', 'large']).optional().default('medium'), }); export type ProductsSectionPayload = z.infer; @@ -1035,7 +1038,10 @@ export const ProductCarouselSectionPayloadSchema = z.object({ showPrice: z.boolean().optional(), showAddToCart: z.boolean().optional(), products: z.array(z.any()).optional(), - cardStyle: z.enum(['standard', 'minimal_swatch']).optional().default('standard'), + cardStyle: z.enum(['standard', 'minimal', 'minimal_swatch', 'swatch']).optional().default('standard'), + containerWidth: z.enum(['standard', 'wide', 'full']).optional().default('standard'), + cardSize: z.enum(['compact', 'medium', 'large']).optional().default('medium'), + layout: z.enum(['carousel', 'grid']).optional().default('carousel'), viewAllText: LocalizedStringSchema.optional(), viewAllLink: z.string().optional(), badgeText: LocalizedStringSchema.optional(),