2026-04-05 18:31:16 -07:00
|
|
|
import React, { CSSProperties } from 'react';
|
|
|
|
|
import { useNode, UserComponent } from '@craftjs/core';
|
|
|
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
2026-05-25 12:43:28 -07:00
|
|
|
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
|
|
|
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
2026-07-12 11:49:10 -07:00
|
|
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
interface CallToActionProps {
|
|
|
|
|
heading?: string;
|
|
|
|
|
description?: string;
|
2026-05-25 12:43:28 -07:00
|
|
|
ctas?: CtaButton[];
|
|
|
|
|
/** Legacy props kept for backward compat with saved projects. */
|
2026-04-05 18:31:16 -07:00
|
|
|
buttonText?: string;
|
|
|
|
|
buttonHref?: string;
|
|
|
|
|
secondaryButtonText?: string;
|
|
|
|
|
secondaryButtonHref?: string;
|
|
|
|
|
bgType?: 'color' | 'gradient' | 'image';
|
|
|
|
|
bgValue?: string;
|
|
|
|
|
overlayColor?: string;
|
|
|
|
|
overlayOpacity?: number;
|
|
|
|
|
textColor?: string;
|
|
|
|
|
buttonColor?: string;
|
2026-05-25 12:43:28 -07:00
|
|
|
anchorId?: string;
|
2026-04-05 18:31:16 -07:00
|
|
|
style?: CSSProperties;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)';
|
|
|
|
|
|
|
|
|
|
export const CallToAction: UserComponent<CallToActionProps> = ({
|
|
|
|
|
heading = 'Ready to Get Started?',
|
|
|
|
|
description = 'Join thousands of satisfied users and start building your dream website today.',
|
2026-05-25 12:43:28 -07:00
|
|
|
ctas,
|
|
|
|
|
buttonText,
|
|
|
|
|
buttonHref,
|
|
|
|
|
secondaryButtonText,
|
|
|
|
|
secondaryButtonHref,
|
2026-04-05 18:31:16 -07:00
|
|
|
bgType = 'gradient',
|
|
|
|
|
bgValue = defaultGradient,
|
|
|
|
|
overlayColor = '#000000',
|
|
|
|
|
overlayOpacity = 0,
|
|
|
|
|
textColor = '#ffffff',
|
|
|
|
|
buttonColor = '#ffffff',
|
2026-05-25 12:43:28 -07:00
|
|
|
anchorId,
|
2026-04-05 18:31:16 -07:00
|
|
|
style = {},
|
|
|
|
|
}) => {
|
|
|
|
|
const {
|
|
|
|
|
connectors: { connect, drag },
|
|
|
|
|
selected,
|
|
|
|
|
} = useNode((node) => ({
|
|
|
|
|
selected: node.events.selected,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const bgStyle: CSSProperties = {};
|
|
|
|
|
if (bgType === 'color') {
|
|
|
|
|
bgStyle.backgroundColor = bgValue;
|
|
|
|
|
} else if (bgType === 'gradient') {
|
|
|
|
|
bgStyle.background = bgValue;
|
|
|
|
|
} else if (bgType === 'image') {
|
|
|
|
|
bgStyle.backgroundImage = `url(${bgValue})`;
|
|
|
|
|
bgStyle.backgroundSize = 'cover';
|
|
|
|
|
bgStyle.backgroundPosition = 'center';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
|
|
|
|
|
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
|
|
|
|
|
|
2026-05-25 12:43:28 -07:00
|
|
|
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref });
|
|
|
|
|
const ctaDefaults = { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor };
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
2026-05-25 12:43:28 -07:00
|
|
|
id={anchorId || undefined}
|
2026-04-05 18:31:16 -07:00
|
|
|
style={{
|
|
|
|
|
position: 'relative',
|
|
|
|
|
padding: '80px 20px',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
...bgStyle,
|
|
|
|
|
...style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* Overlay */}
|
|
|
|
|
{bgType === 'image' && overlayOpacity > 0 && (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
inset: 0,
|
|
|
|
|
backgroundColor: overlayColor,
|
|
|
|
|
opacity: overlayOpacity / 100,
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div style={{ maxWidth: '700px', margin: '0 auto', position: 'relative', zIndex: 1 }}>
|
|
|
|
|
<h2 style={{ fontSize: '36px', fontWeight: '700', color: textColor, marginBottom: '12px' }}>
|
|
|
|
|
{heading}
|
|
|
|
|
</h2>
|
|
|
|
|
<p style={{ fontSize: '18px', color: textColor, opacity: 0.85, marginBottom: '28px', lineHeight: '1.6' }}>
|
|
|
|
|
{description}
|
|
|
|
|
</p>
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}>
|
2026-05-25 12:43:28 -07:00
|
|
|
{effectiveCtas.map((cta, i) => (
|
|
|
|
|
<a key={i} href={cta.href || '#'} onClick={(e) => e.preventDefault()}
|
|
|
|
|
style={ctaInlineStyle(cta, ctaDefaults)}>
|
|
|
|
|
{cta.text}
|
2026-04-05 18:31:16 -07:00
|
|
|
</a>
|
2026-05-25 12:43:28 -07:00
|
|
|
))}
|
2026-04-05 18:31:16 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Settings panel ---------- */
|
|
|
|
|
|
|
|
|
|
const CallToActionSettings: React.FC = () => {
|
|
|
|
|
const { actions: { setProp }, props } = useNode((node) => ({
|
|
|
|
|
props: node.data.props as CallToActionProps,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const gradientPresets = [
|
|
|
|
|
{ label: 'Blue-Purple', value: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)' },
|
|
|
|
|
{ label: 'Purple', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
|
|
|
|
{ label: 'Teal', value: 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)' },
|
|
|
|
|
{ label: 'Sunset', value: 'linear-gradient(135deg, #f97316 0%, #ec4899 100%)' },
|
|
|
|
|
{ label: 'Dark', value: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)' },
|
|
|
|
|
{ label: 'Ocean', value: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const colorPresets = ['#2563eb', '#7c3aed', '#0d9488', '#18181b', '#0f172a', '#1e293b', '#dc2626', '#f97316'];
|
|
|
|
|
const buttonColorPresets = ['#ffffff', '#18181b', '#3b82f6', '#10b981', '#ef4444', '#8b5cf6', '#f59e0b', '#ec4899'];
|
|
|
|
|
const textColorPresets = ['#ffffff', '#f8fafc', '#e2e8f0', '#18181b', '#1e293b', '#fef3c7'];
|
|
|
|
|
|
|
|
|
|
const inputStyle: CSSProperties = {
|
|
|
|
|
width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7',
|
|
|
|
|
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-25 12:43:28 -07:00
|
|
|
const effectiveCtas = normalizeCtas(props);
|
|
|
|
|
|
2026-04-05 18:31:16 -07:00
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
2026-05-25 12:43:28 -07:00
|
|
|
<AnchorIdField />
|
2026-04-05 18:31:16 -07:00
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Heading</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.heading || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CallToActionProps) => { p.heading = e.target.value; })}
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Description</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={props.description || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CallToActionProps) => { p.description = e.target.value; })}
|
|
|
|
|
rows={2}
|
|
|
|
|
style={{ ...inputStyle, resize: 'vertical' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-05-25 12:43:28 -07:00
|
|
|
<CtasEditor
|
|
|
|
|
ctas={effectiveCtas}
|
|
|
|
|
onChange={(next) => setProp((p: CallToActionProps) => {
|
|
|
|
|
p.ctas = next;
|
|
|
|
|
p.buttonText = undefined;
|
|
|
|
|
p.buttonHref = undefined;
|
|
|
|
|
p.secondaryButtonText = undefined;
|
|
|
|
|
p.secondaryButtonHref = undefined;
|
|
|
|
|
})}
|
|
|
|
|
/>
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
{/* Background Type */}
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background Type</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
|
|
|
{(['color', 'gradient', 'image'] as const).map((t) => (
|
|
|
|
|
<button
|
|
|
|
|
key={t}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setProp((p: CallToActionProps) => {
|
|
|
|
|
p.bgType = t;
|
|
|
|
|
if (t === 'color') p.bgValue = '#2563eb';
|
|
|
|
|
if (t === 'gradient') p.bgValue = defaultGradient;
|
|
|
|
|
if (t === 'image') p.bgValue = '';
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
style={{
|
|
|
|
|
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
|
|
|
|
border: '1px solid #3f3f46',
|
|
|
|
|
background: props.bgType === t ? '#3b82f6' : '#27272a',
|
|
|
|
|
color: '#e4e4e7',
|
|
|
|
|
textTransform: 'capitalize',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{t}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Background sub-controls */}
|
|
|
|
|
{props.bgType === 'color' && (
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background Color</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
{colorPresets.map((c) => (
|
|
|
|
|
<button
|
|
|
|
|
key={c}
|
|
|
|
|
onClick={() => setProp((p: CallToActionProps) => { p.bgValue = c; })}
|
|
|
|
|
style={{
|
|
|
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
|
|
|
|
backgroundColor: c, cursor: 'pointer',
|
|
|
|
|
outline: props.bgValue === c ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
outlineOffset: 1,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{props.bgType === 'gradient' && (
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Gradient</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
{gradientPresets.map((g) => (
|
|
|
|
|
<button
|
|
|
|
|
key={g.label}
|
|
|
|
|
onClick={() => setProp((p: CallToActionProps) => { p.bgValue = g.value; })}
|
|
|
|
|
title={g.label}
|
|
|
|
|
style={{
|
|
|
|
|
width: 32, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
|
|
|
|
background: g.value, cursor: 'pointer',
|
|
|
|
|
outline: props.bgValue === g.value ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
outlineOffset: 1,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{props.bgType === 'image' && (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Image URL</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.bgValue || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CallToActionProps) => { p.bgValue = e.target.value; })}
|
|
|
|
|
placeholder="https://..."
|
|
|
|
|
style={inputStyle}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>
|
|
|
|
|
Overlay Opacity: {props.overlayOpacity ?? 0}%
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="range"
|
|
|
|
|
min={0}
|
|
|
|
|
max={100}
|
|
|
|
|
value={props.overlayOpacity ?? 0}
|
|
|
|
|
onChange={(e) => setProp((p: CallToActionProps) => { p.overlayOpacity = parseInt(e.target.value); })}
|
|
|
|
|
style={{ width: '100%', accentColor: '#3b82f6' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Overlay Color</label>
|
|
|
|
|
<input
|
|
|
|
|
type="color"
|
|
|
|
|
value={props.overlayColor || '#000000'}
|
|
|
|
|
onChange={(e) => setProp((p: CallToActionProps) => { p.overlayColor = e.target.value; })}
|
|
|
|
|
style={{ width: 32, height: 24, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', background: 'none', padding: 0 }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Text Color */}
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Text Color</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
{textColorPresets.map((c) => (
|
|
|
|
|
<button
|
|
|
|
|
key={c}
|
|
|
|
|
onClick={() => setProp((p: CallToActionProps) => { p.textColor = c; })}
|
|
|
|
|
style={{
|
|
|
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
|
|
|
|
backgroundColor: c, cursor: 'pointer',
|
|
|
|
|
outline: props.textColor === c ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
outlineOffset: 1,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Button Color */}
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Button Color</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
{buttonColorPresets.map((c) => (
|
|
|
|
|
<button
|
|
|
|
|
key={c}
|
|
|
|
|
onClick={() => setProp((p: CallToActionProps) => { p.buttonColor = c; })}
|
|
|
|
|
style={{
|
|
|
|
|
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
|
|
|
|
backgroundColor: c, cursor: 'pointer',
|
|
|
|
|
outline: props.buttonColor === c ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
outlineOffset: 1,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
|
|
|
|
|
|
CallToAction.craft = {
|
|
|
|
|
displayName: 'Call to Action',
|
|
|
|
|
props: {
|
|
|
|
|
heading: 'Ready to Get Started?',
|
|
|
|
|
description: 'Join thousands of satisfied users and start building your dream website today.',
|
2026-05-25 12:43:28 -07:00
|
|
|
ctas: [
|
|
|
|
|
{ text: 'Get Started', href: '#', variant: 'primary' },
|
|
|
|
|
{ text: 'Learn More', href: '#', variant: 'outline' },
|
|
|
|
|
] as CtaButton[],
|
|
|
|
|
anchorId: '',
|
2026-04-05 18:31:16 -07:00
|
|
|
bgType: 'gradient',
|
|
|
|
|
bgValue: defaultGradient,
|
|
|
|
|
overlayColor: '#000000',
|
|
|
|
|
overlayOpacity: 0,
|
|
|
|
|
textColor: '#ffffff',
|
|
|
|
|
buttonColor: '#ffffff',
|
|
|
|
|
style: {},
|
|
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
canDrag: () => true,
|
|
|
|
|
canMoveIn: () => false,
|
|
|
|
|
canMoveOut: () => true,
|
|
|
|
|
},
|
|
|
|
|
related: {
|
|
|
|
|
settings: CallToActionSettings,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
|
|
|
|
|
|
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
|
|
|
|
|
const bgType = props.bgType || 'gradient';
|
|
|
|
|
const bgValue = props.bgValue || defaultGradient;
|
|
|
|
|
const textColor = props.textColor || '#ffffff';
|
|
|
|
|
const buttonColor = props.buttonColor || '#ffffff';
|
|
|
|
|
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
|
|
|
|
|
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
|
|
|
|
|
|
|
|
|
|
const sectionCss: CSSProperties = {
|
|
|
|
|
position: 'relative',
|
|
|
|
|
padding: '80px 20px',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
...props.style,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (bgType === 'color') {
|
|
|
|
|
sectionCss.backgroundColor = bgValue;
|
|
|
|
|
} else if (bgType === 'gradient') {
|
|
|
|
|
sectionCss.background = bgValue;
|
|
|
|
|
} else if (bgType === 'image') {
|
|
|
|
|
sectionCss.backgroundImage = `url(${bgValue})`;
|
|
|
|
|
sectionCss.backgroundSize = 'cover';
|
|
|
|
|
sectionCss.backgroundPosition = 'center';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sectionStyle = cssPropsToString(sectionCss);
|
|
|
|
|
|
|
|
|
|
let overlayHtml = '';
|
|
|
|
|
if (bgType === 'image' && (props.overlayOpacity || 0) > 0) {
|
|
|
|
|
const overlayStyle = cssPropsToString({
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
inset: '0',
|
|
|
|
|
backgroundColor: props.overlayColor || '#000000',
|
|
|
|
|
opacity: String((props.overlayOpacity || 0) / 100) as any,
|
|
|
|
|
pointerEvents: 'none',
|
|
|
|
|
});
|
|
|
|
|
overlayHtml = `<div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div>`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 12:43:28 -07:00
|
|
|
const ctas = normalizeCtas(props);
|
|
|
|
|
const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor });
|
2026-07-12 11:49:10 -07:00
|
|
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
return {
|
2026-05-25 12:43:28 -07:00
|
|
|
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
2026-04-05 18:31:16 -07:00
|
|
|
${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1">
|
2026-07-12 11:49:10 -07:00
|
|
|
<h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
|
|
|
|
|
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
|
2026-05-25 12:43:28 -07:00
|
|
|
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
2026-04-05 18:31:16 -07:00
|
|
|
</div>
|
|
|
|
|
</section>`,
|
|
|
|
|
};
|
|
|
|
|
};
|