x
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">"</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">
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user