This commit is contained in:
Your Name committed 2026-10-06 16:58:21 +07:00
1 parent 641cc4e42b
commit 46968782b6
17 files changed
+265 -30

No files matched your search

@@ -0,0 +1,130 @@
import { MoveVertical, Sparkles } from 'lucide-react';
interface SectionSpacingControlsProps {
payload: Record<string, any>;
updatePayloadValue: (field: string, value: any) => void;
}
export default function SectionSpacingControls({
payload,
updatePayloadValue,
}: SectionSpacingControlsProps) {
const currentTop = payload?.paddingTop || payload?.sectionPadding || 'normal';
const currentBottom = payload?.paddingBottom || payload?.sectionPadding || 'normal';
const applyPreset = (top: string, bottom: string) => {
updatePayloadValue('paddingTop', top);
updatePayloadValue('paddingBottom', bottom);
updatePayloadValue('sectionPadding', top === bottom ? top : 'normal');
};
return (
<div className="bg-gradient-to-r from-night-950/70 via-night-900/50 to-night-950/70 p-4 rounded-2xl border border-white/10 space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-white/5 pb-2.5">
<div className="flex items-center gap-2">
<MoveVertical className="w-4 h-4 text-cyan-glow" />
<span className="text-xs font-bold uppercase tracking-widest text-slate-200">
ระยะห่างแนวตั้งของบล็อก (Vertical Spacing)
</span>
</div>
<div className="flex items-center gap-1.5 text-[11px] font-mono text-cyan-glow/80 bg-cyan-glow/10 px-2 py-0.5 rounded-lg border border-cyan-glow/20">
<span>Top: {currentTop}</span>
<span className="text-white/30">•</span>
<span>Bottom: {currentBottom}</span>
</div>
</div>
{/* Quick Spacing Presets */}
<div className="flex items-center gap-2 overflow-x-auto hide-scrollbar pb-0.5">
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-400 shrink-0 flex items-center gap-1">
<Sparkles className="w-3 h-3 text-cyan-glow" /> Preset:
</span>
<button
type="button"
onClick={() => applyPreset('compact', 'compact')}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition-all cursor-pointer whitespace-nowrap border ${
currentTop === 'compact' && currentBottom === 'compact'
? 'bg-cyan-glow/20 border-cyan-glow text-cyan-300'
: 'bg-white/5 border-white/5 text-slate-300 hover:bg-white/10'
}`}
>
กะทัดรัด (Compact 24-40px)
</button>
<button
type="button"
onClick={() => applyPreset('normal', 'normal')}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition-all cursor-pointer whitespace-nowrap border ${
currentTop === 'normal' && currentBottom === 'normal'
? 'bg-cyan-glow/20 border-cyan-glow text-cyan-300'
: 'bg-white/5 border-white/5 text-slate-300 hover:bg-white/10'
}`}
>
มาตรฐาน (Normal 40-56px)
</button>
<button
type="button"
onClick={() => applyPreset('spacious', 'spacious')}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition-all cursor-pointer whitespace-nowrap border ${
currentTop === 'spacious' && currentBottom === 'spacious'
? 'bg-cyan-glow/20 border-cyan-glow text-cyan-300'
: 'bg-white/5 border-white/5 text-slate-300 hover:bg-white/10'
}`}
>
กว้างโปร่ง (Spacious 80-96px)
</button>
<button
type="button"
onClick={() => applyPreset('none', 'none')}
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium transition-all cursor-pointer whitespace-nowrap border ${
currentTop === 'none' && currentBottom === 'none'
? 'bg-cyan-glow/20 border-cyan-glow text-cyan-300'
: 'bg-white/5 border-white/5 text-slate-300 hover:bg-white/10'
}`}
>
ชิดติดกัน (Flush 0px)
</button>
</div>
{/* Manual Fine-grained Controls */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
ระยะห่างด้านบน (Top Padding)
</label>
<select
value={currentTop}
onChange={(e) => {
updatePayloadValue('paddingTop', e.target.value);
updatePayloadValue('sectionPadding', e.target.value === currentBottom ? e.target.value : 'normal');
}}
className="admin-input w-full text-xs bg-slate-900 border-white/10"
>
<option value="none">ไม่มีระยะห่างด้านบน (None - 0px)</option>
<option value="compact">กะทัดรัด (Compact - 24-40px)</option>
<option value="normal">มาตรฐาน (Normal - 40-56px)</option>
<option value="spacious">กว้างโปร่ง (Spacious - 80-96px)</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">
ระยะห่างด้านล่าง (Bottom Padding)
</label>
<select
value={currentBottom}
onChange={(e) => {
updatePayloadValue('paddingBottom', e.target.value);
updatePayloadValue('sectionPadding', currentTop === e.target.value ? e.target.value : 'normal');
}}
className="admin-input w-full text-xs bg-slate-900 border-white/10"
>
<option value="none">ไม่มีระยะห่างด้านล่าง (None - 0px)</option>
<option value="compact">กะทัดรัด (Compact - 24-40px)</option>
<option value="normal">มาตรฐาน (Normal - 40-56px)</option>
<option value="spacious">กว้างโปร่ง (Spacious - 80-96px)</option>
</select>
</div>
</div>
</div>
);
}
+9
View File
@@ -68,6 +68,7 @@ import DividerForm from '../components/page-builder/DividerForm';
import AddSectionModal from '../components/page-builder/AddSectionModal';
import FeaturedTilesForm from '../components/page-builder/FeaturedTilesForm';
import HeroVideoForm from '../components/page-builder/HeroVideoForm';
import SectionSpacingControls from '../components/page-builder/SectionSpacingControls';
import {
HeroMinimalForm,
HeroSplitMinimalForm,
@@ -1496,6 +1497,14 @@ export default function PageBuilderPage() {
</div>
)}
{/* ── Universal Section Spacing Controls (Full Control for all block types) ── */}
{editingSection.type?.toUpperCase() !== 'DIVIDER' && (
<SectionSpacingControls
payload={editingSection.payload || {}}
updatePayloadValue={updatePayloadValue}
/>
)}
{/* Dynamic Forms based on Type */}
{editingSection.type?.toUpperCase() === 'HERO_MINIMAL' && (
<HeroMinimalForm
+6 -1
View File
@@ -43,6 +43,7 @@ import {
BannerMinimalSectionPayloadSchema,
AnnouncementBarMinimalSectionPayloadSchema,
UspMinimalSectionPayloadSchema,
SectionSpacingPayloadSchema,
PAGE_SECTION_LOCALIZED_PATHS,
type SectionType,
} from "@homaroma/types";
@@ -1440,7 +1441,11 @@ export function validateSectionPayload(
};
}
const res = schema.safeParse(payload);
const targetSchema = (schema instanceof z.ZodObject)
? (schema as any).merge(SectionSpacingPayloadSchema)
: schema;
const res = targetSchema.safeParse(payload);
if (!res.success) {
return { success: false, issues: res.error.issues };
}
+2 -1
View File
@@ -3,6 +3,7 @@ import type { BannerSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getHeroPositionClasses } from './DynamicHero';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicBannerProps {
payload: BannerSectionPayload;
@@ -66,7 +67,7 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
);
return (
<section className="ui-section-band bg-pop-bg relative overflow-hidden">
<section className={`bg-pop-bg relative overflow-hidden ${getSectionPaddingClasses(payload)}`}>
{linkUrl ? (
<Link to={linkUrl} className={`ui-banner-shell group flex flex-col ${shellWidthClass} ${wrapperClasses} cursor-pointer`}>
{bannerInner}
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { getCategoryImage, resolveCategoryMediaUrl, type Category, type CategoriesSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicCategoriesProps {
payload: CategoriesSectionPayload;
@@ -26,7 +27,7 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
}, [categories, categoryIds]);
return (
<section id="categories" className="ui-section-band relative overflow-hidden bg-pop-bg">
<section id="categories" className={`relative overflow-hidden bg-pop-bg ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner space-y-16 relative z-10">
<div className="space-y-4 text-center">
<h2 className="text-4xl font-bold tracking-tight text-pop-black md:text-5xl">
@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import type { EditorialTileSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicEditorialTileProps {
payload: EditorialTileSectionPayload;
@@ -54,7 +55,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
return (
<section
className="ui-section-band w-full overflow-hidden"
className={`w-full overflow-hidden ${getSectionPaddingClasses(payload)}`}
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
>
<div className="ui-section-inner grid grid-cols-1 items-center gap-12 md:grid-cols-2 md:gap-20">
@@ -3,6 +3,7 @@ import { useRef } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicProductCarouselProps {
payload: any;
@@ -21,7 +22,7 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' });
return (
<section className="ui-section-band bg-pop-bg">
<section className={`bg-pop-bg ${getSectionPaddingClasses(p)}`}>
<div className="ui-section-inner">
{(title || description) && (
<div className="text-center mb-12">
+2 -1
View File
@@ -5,6 +5,7 @@ import ProductCard from './ProductCard';
import type { Category, Product, ProductsSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicProductsProps {
payload: ProductsSectionPayload & { products?: Product[] };
@@ -24,7 +25,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' });
return (
<section className="ui-section-band relative bg-pop-bg">
<section className={`relative bg-pop-bg ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner relative z-10 space-y-12">
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6 border-b border-pop-black/10 pb-6">
<div>
@@ -1,6 +1,7 @@
import type { QuoteCalloutPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicQuoteCalloutProps {
payload: QuoteCalloutPayload & { role?: string };
@@ -14,7 +15,7 @@ export default function DynamicQuoteCallout({ payload }: DynamicQuoteCalloutProp
const role = getLocalizedField(payload.role, lang);
return (
<section className="ui-section-band bg-pop-bg">
<section className={`bg-pop-bg ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner text-center">
<div className="bg-pop-black/5 rounded-3xl p-8 max-w-4xl mx-auto">
<svg className="w-12 h-12 mx-auto text-pop-black/40 mb-8" fill="currentColor" viewBox="0 0 24 24">
+2 -1
View File
@@ -1,6 +1,7 @@
import type { PageSection } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicRichTextProps {
section: PageSection;
@@ -17,7 +18,7 @@ export default function DynamicRichText({ section }: DynamicRichTextProps) {
if (!content) return null;
return (
<section className="ui-section-band bg-pop-bg relative">
<section className={`bg-pop-bg relative ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner space-y-6">
{title && (
<h2 className="font-serif text-3xl font-semibold text-pop-black md:text-4xl text-center">
@@ -1,6 +1,7 @@
import type { ScentNotesBreakdownPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicScentNotesBreakdownProps {
payload: ScentNotesBreakdownPayload;
@@ -28,7 +29,7 @@ export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNote
const baseNotes = notes(payload.baseNotes);
return (
<section className="ui-section-band bg-pop-bg">
<section className={`bg-pop-bg ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner text-center">
<h2 className="mb-6 font-serif text-3xl text-pop-black md:text-5xl">
{title || 'Scent Notes'}
+2 -1
View File
@@ -1,6 +1,7 @@
import type { TextSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
import { getSectionPaddingClasses } from '../utils/spacing';
interface DynamicTextProps {
payload: TextSectionPayload;
@@ -20,7 +21,7 @@ export default function DynamicText({ payload }: DynamicTextProps) {
}[textAlign] || 'text-left';
return (
<section className="ui-section-band bg-pop-bg relative">
<section className={`bg-pop-bg relative ${getSectionPaddingClasses(payload)}`}>
<div className={`ui-section-inner space-y-6 ${alignClass}`}>
{title && (
<h2 className="font-serif text-3xl font-semibold text-pop-black md:text-4xl">
@@ -3,6 +3,7 @@ import type { AccordionSectionPayload } from '@homaroma/types';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../../utils/localization';
import { getSectionPaddingClasses } from '../../utils/spacing';
interface AccordionSectionProps {
payload: AccordionSectionPayload & { title?: string };
@@ -20,7 +21,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
return (
<section
className="ui-section-band w-full px-4 md:px-8"
className={`w-full px-4 md:px-8 ${getSectionPaddingClasses(payload)}`}
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
>
<div className="ui-section-inner">
@@ -3,6 +3,7 @@ import type { ContactInfoSectionPayload } from '@homaroma/types';
import { MapPin, Mail, MessageCircle, Clock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../../utils/localization';
import { getSectionPaddingClasses } from '../../utils/spacing';
interface ContactInfoSectionProps {
payload: ContactInfoSectionPayload & { title?: string };
@@ -30,7 +31,7 @@ export default function ContactInfoSection({ payload }: ContactInfoSectionProps)
] as const;
return (
<section className="ui-section-band w-full bg-pop-bg px-4 md:px-8">
<section className={`w-full bg-pop-bg px-4 md:px-8 ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-inner">
{title && (
<div className="mb-10 text-center">
+37
View File
@@ -0,0 +1,37 @@
export function getSectionPaddingClasses(
payload?: Record<string, any>,
fallback: 'normal' | 'none' | 'compact' | 'spacious' = 'normal',
): string {
const padTop = payload?.paddingTop || payload?.sectionPadding || fallback;
const padBottom = payload?.paddingBottom || payload?.sectionPadding || fallback;
const getPt = (p: string) => {
switch (p) {
case 'none':
return 'pt-0';
case 'compact':
return 'pt-6 md:pt-10';
case 'spacious':
return 'pt-20 md:pt-28';
case 'normal':
default:
return 'pt-10 md:pt-16';
}
};
const getPb = (p: string) => {
switch (p) {
case 'none':
return 'pb-0';
case 'compact':
return 'pb-6 md:pb-10';
case 'spacious':
return 'pb-20 md:pb-28';
case 'normal':
default:
return 'pb-10 md:pb-16';
}
};
return `${getPt(padTop)} ${getPb(padBottom)}`;
}
@@ -64,6 +64,39 @@ function renderAccentTitle(text: string, accentClassName = 'ui-accent-text') {
export function getSectionPaddingClasses(
payload?: Record<string, unknown>,
fallback: 'normal' | 'none' | 'compact' | 'spacious' = 'normal',
) {
const padTop = (typeof payload?.paddingTop === 'string'
? payload.paddingTop
: (typeof payload?.sectionPadding === 'string' ? payload.sectionPadding : fallback)) as string
const padBottom = (typeof payload?.paddingBottom === 'string'
? payload.paddingBottom
: (typeof payload?.sectionPadding === 'string' ? payload.sectionPadding : fallback)) as string
const getPt = (p: string) => {
switch (p) {
case 'none': return 'pt-0'
case 'compact': return 'pt-6 md:pt-10'
case 'spacious': return 'pt-20 md:pt-28'
case 'normal': default: return 'pt-10 md:pt-16'
}
}
const getPb = (p: string) => {
switch (p) {
case 'none': return 'pb-0'
case 'compact': return 'pb-6 md:pb-10'
case 'spacious': return 'pb-20 md:pb-28'
case 'normal': default: return 'pb-10 md:pb-16'
}
}
return `px-6 ${getPt(padTop)} ${getPb(padBottom)}`
}
function AnnouncementBar({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
@@ -1040,7 +1073,7 @@ function CategoriesSection({
const displayed = categoryIds.length ? sourceCategories.filter((category) => categoryIds.includes(category.id)) : sourceCategories
return (
<section id="categories" className="bg-surface-page ui-section-band">
<section id="categories" className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-shell space-y-14">
<div className="mx-auto max-w-2xl space-y-4 text-center">
<h2 className="ui-section-title">{title}</h2>
@@ -1095,7 +1128,7 @@ function ProductsSection({ payload, lang }: { payload: Record<string, unknown>,
const scrollRight = () => carouselRef.current?.scrollBy({ left: 300, behavior: 'smooth' })
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="ui-section-shell space-y-10">
<div className="flex flex-col gap-4 border-b border-line-primary/20 pb-6 md:flex-row md:items-end md:justify-between">
<div>
@@ -1196,7 +1229,7 @@ function BannerSection({ payload, lang }: { payload: Record<string, unknown>, la
</div>
)
return <section className="bg-surface-page ui-section-band">{linkUrl ? <Link href={linkUrl}>{content}</Link> : content}</section>
return <section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>{linkUrl ? <Link href={linkUrl}>{content}</Link> : content}</section>
}
function TextSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
@@ -1211,7 +1244,7 @@ function TextSection({ payload, lang }: { payload: Record<string, unknown>, lang
: 'text-left'
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className={`mx-auto max-w-4xl space-y-6 ${alignClass}`}>
{title && <h2 className="text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
{content && <div className="leading-relaxed text-muted-text" dangerouslySetInnerHTML={{ __html: content }} />}
@@ -1234,7 +1267,7 @@ function ArticleHeaderSection({ payload, lang }: { payload: Record<string, unkno
if (!title) return null
return (
<header className="bg-surface-page ui-section-band">
<header className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-4xl text-center">
{category && <p className="mb-4 text-sm font-semibold uppercase text-brand-primary">{category}</p>}
@@ -1312,7 +1345,7 @@ function ArticleMediaSection({ payload, lang }: { payload: Record<string, unknow
) : null
return (
<section className={`${backgroundClass} ui-section-band`}>
<section className={`${backgroundClass} ${getSectionPaddingClasses(payload)}`}>
<div className={`mx-auto ${widthClass}`}>
{!media ? copy : imagePosition === 'top' ? (
<div className="space-y-8">
@@ -1344,7 +1377,7 @@ function ArticleGallerySection({ payload, lang }: { payload: Record<string, unkn
if (images.length < 2) return null
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-7xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<div className={featured ? 'grid gap-4 md:grid-cols-2' : 'grid gap-4 sm:grid-cols-2 lg:grid-cols-3'}>
@@ -1378,7 +1411,7 @@ function ArticleFactsSection({ payload, lang }: { payload: Record<string, unknow
if (items.length === 0) return null
return (
<section className="bg-surface-card ui-section-band">
<section className={`bg-surface-card ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-6xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<dl className="grid gap-x-10 gap-y-8 md:grid-cols-2">
@@ -1430,7 +1463,7 @@ function ArticleVideoSection({ payload, lang }: { payload: Record<string, unknow
if (!videoUrl) return null
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-6xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<div className={`${aspectClass} overflow-hidden rounded-lg bg-text-main`}>
@@ -1461,7 +1494,7 @@ function ArticleReferencesSection({ payload, lang }: { payload: Record<string, u
if (items.length === 0) return null
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-4xl">
<h2 className="mb-6 text-2xl font-semibold text-text-main">{title || 'References'}</h2>
<ol className="list-decimal space-y-4 pl-5 text-muted-text">
@@ -1498,7 +1531,7 @@ function ArticleTableSection({ payload, lang }: { payload: Record<string, unknow
if (columns.length < 2 || rows.length === 0) return null
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-6xl">
{title && <h2 className="mb-8 text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
<div className="overflow-x-auto rounded-lg border border-line-primary/20">
@@ -1549,7 +1582,7 @@ function ArticleDownloadSection({ payload, lang }: { payload: Record<string, unk
if (files.length === 0) return null
return (
<section className="bg-surface-card ui-section-band">
<section className={`bg-surface-card ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-5xl">
{title && <h2 className="text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>}
{description && <p className="mt-4 max-w-3xl leading-7 text-muted-text">{description}</p>}
@@ -1594,7 +1627,7 @@ function ArticleCtaSection({ payload, lang }: { payload: Record<string, unknown>
const secondaryClass = "inline-flex min-h-12 items-center justify-center rounded-lg border border-line-primary px-6 py-3 font-semibold text-text-main transition-colors hover:bg-surface-page"
return (
<section className="bg-surface-card ui-section-band">
<section className={`bg-surface-card ${getSectionPaddingClasses(payload)}`}>
<div className={`mx-auto max-w-5xl ${centered ? 'text-center' : 'text-left'}`}>
<h2 className="text-3xl font-semibold text-text-main md:text-4xl">{title}</h2>
{description && <p className={`mt-4 max-w-2xl leading-7 text-muted-text ${centered ? 'mx-auto' : ''}`}>{description}</p>}
@@ -1663,7 +1696,7 @@ function EditorialTileSection({ payload, lang }: { payload: Record<string, unkno
}
return (
<section className="overflow-hidden bg-surface-page ui-section-band" style={{ backgroundColor, color: textColor }}>
<section className={`overflow-hidden bg-surface-page ${getSectionPaddingClasses(payload)}`} style={{ backgroundColor, color: textColor }}>
<div className="mx-auto grid max-w-7xl gap-12 md:grid-cols-2 md:items-center">
{imageUrl && (
<div className={`group relative ${getAspectClass(imageAspectRatio)} overflow-hidden rounded-3xl shadow-card ${imagePosition === 'left' ? 'md:order-1' : 'md:order-2'}`}>
@@ -2115,7 +2148,7 @@ function ScentNotesBreakdownSection({ payload, lang }: { payload: Record<string,
]
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-6xl space-y-12 text-center">
<div className="mx-auto max-w-3xl space-y-4">
<h2 className="ui-section-title">{title}</h2>
@@ -2143,7 +2176,7 @@ function QuoteCalloutSection({ payload, lang }: { payload: Record<string, unknow
if (!quote) return null
return (
<section className="bg-surface-page px-6 py-24">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto max-w-4xl text-center">
<p className="mb-8 text-7xl leading-none text-brand-primary/25">&quot;</p>
<blockquote className="text-3xl font-semibold italic leading-relaxed text-text-main md:text-5xl">
@@ -2170,7 +2203,7 @@ function AccordionSection({ payload, lang }: { payload: Record<string, unknown>,
const iconColor = typeof payload.iconColor === 'string' ? payload.iconColor : undefined
return (
<section className="ui-section-band" style={{ backgroundColor, color: textColor }}>
<section className={`${getSectionPaddingClasses(payload)}`} style={{ backgroundColor, color: textColor }}>
<div className="mx-auto max-w-4xl space-y-10">
{sectionTitle && <h2 className="ui-section-title">{sectionTitle}</h2>}
{categories.map((category, categoryIndex) => {
@@ -2228,7 +2261,7 @@ function ContactInfoSection({ payload, lang }: { payload: Record<string, unknown
] as const
return (
<section className="bg-surface-page ui-section-band">
<section className={`bg-surface-page ${getSectionPaddingClasses(payload)}`}>
<div className="mx-auto grid max-w-6xl gap-6 md:grid-cols-2 lg:grid-cols-4">
{title && (
<div className="col-span-full mb-2">
+10
View File
@@ -635,6 +635,16 @@ export const DefaultTextColorSchemeSchema = (fallback: TextColorScheme = 'light'
.optional()
.transform((val) => (val ? (val as TextColorScheme) : fallback));
export const SectionPaddingLevelSchema = z.enum(['none', 'compact', 'normal', 'spacious']);
export type SectionPaddingLevel = z.infer<typeof SectionPaddingLevelSchema>;
export const SectionSpacingPayloadSchema = z.object({
paddingTop: SectionPaddingLevelSchema.optional(),
paddingBottom: SectionPaddingLevelSchema.optional(),
sectionPadding: SectionPaddingLevelSchema.optional(),
});
export type SectionSpacingPayload = z.infer<typeof SectionSpacingPayloadSchema>;
export const HeroSectionPayloadSchema = z.object({
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema.optional(),