site-builder: dynamic CTAs, section anchors, edit-with-Sitesmith
Three related features: 1. Dynamic CTA buttons on HeroSimple, CTASection, CallToAction. New shared ctas[] array (text + href + variant + target) replaces the primary/secondary pair. Settings panel gets add/remove/reorder controls. Legacy fields stay readable for backwards compat — first user edit migrates the section onto the new array. 2. Anchor IDs on all layout/section components (Container, Section, BackgroundSection, ColumnLayout, plus 6 section blocks done by parallel subagent, plus Hero/CTA/CallToAction). Anchor input lives in the settings panel with an "auto from heading" button that walks the subtree for the first Heading.text. Renders as id="..." on the outermost element so #anchor URLs resolve. 3. Edit-with-Sitesmith targeted invocation. Right-click → "Ask Sitesmith" and a button at the top of the right-side settings panel both open the modal pre-targeted at the selected node. The node's serialized subtree is sent to the server; system prompt is augmented to require a patch with replace_node. Editor lifts modal state into a new SitesmithContext. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
|
||||
interface HeroProps {
|
||||
heading?: string;
|
||||
subtitle?: string;
|
||||
/** New dynamic CTAs. When set (length > 0), legacy primary/secondary fields are ignored. */
|
||||
ctas?: CtaButton[];
|
||||
/** Legacy — kept for backwards compatibility with saved projects. */
|
||||
buttonText?: string;
|
||||
buttonHref?: string;
|
||||
secondaryButtonText?: string;
|
||||
@@ -24,6 +29,7 @@ interface HeroProps {
|
||||
minHeight?: string;
|
||||
verticalAlign?: 'top' | 'center' | 'bottom';
|
||||
textAlign?: 'left' | 'center' | 'right';
|
||||
anchorId?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
@@ -43,10 +49,11 @@ function buildBackground(props: HeroProps): string {
|
||||
export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
heading = 'Build Something Amazing',
|
||||
subtitle = 'Create beautiful websites without writing a single line of code.',
|
||||
buttonText = 'Get Started',
|
||||
buttonHref = '#',
|
||||
secondaryButtonText = '',
|
||||
secondaryButtonHref = '#',
|
||||
ctas,
|
||||
buttonText,
|
||||
buttonHref,
|
||||
secondaryButtonText,
|
||||
secondaryButtonHref,
|
||||
bgType = 'color',
|
||||
bgColor = '#1e293b',
|
||||
bgGradientFrom = '#667eea',
|
||||
@@ -62,6 +69,7 @@ export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
minHeight = '500px',
|
||||
verticalAlign = 'center',
|
||||
textAlign = 'center',
|
||||
anchorId,
|
||||
style = {},
|
||||
}) => {
|
||||
const { connectors: { connect, drag } } = useNode();
|
||||
@@ -72,9 +80,17 @@ export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
|
||||
const justifyMap = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
|
||||
|
||||
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref });
|
||||
const ctaDefaults = {
|
||||
primaryBg: buttonBgColor,
|
||||
primaryText: buttonTextColor,
|
||||
outlineText: textColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
...style,
|
||||
background: bgType !== 'image' ? bg : undefined,
|
||||
@@ -134,25 +150,12 @@ export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
{subtitle}
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start', flexWrap: 'wrap' }}>
|
||||
{buttonText && (
|
||||
<a href={buttonHref} onClick={(e) => e.preventDefault()} style={{
|
||||
display: 'inline-block', padding: '14px 36px', backgroundColor: buttonBgColor,
|
||||
color: buttonTextColor, textDecoration: 'none', borderRadius: '8px',
|
||||
fontWeight: '600', fontSize: '16px',
|
||||
}}>
|
||||
{buttonText}
|
||||
{effectiveCtas.map((cta, i) => (
|
||||
<a key={i} href={cta.href || '#'} onClick={(e) => e.preventDefault()}
|
||||
style={ctaInlineStyle(cta, ctaDefaults)}>
|
||||
{cta.text}
|
||||
</a>
|
||||
)}
|
||||
{secondaryButtonText && (
|
||||
<a href={secondaryButtonHref} onClick={(e) => e.preventDefault()} style={{
|
||||
display: 'inline-block', padding: '14px 36px',
|
||||
backgroundColor: 'transparent', color: textColor,
|
||||
textDecoration: 'none', borderRadius: '8px', fontWeight: '600',
|
||||
fontSize: '16px', border: `2px solid ${textColor}`,
|
||||
}}>
|
||||
{secondaryButtonText}
|
||||
</a>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -181,8 +184,11 @@ const HeroSettings: React.FC = () => {
|
||||
props: node.data.props as HeroProps,
|
||||
}));
|
||||
|
||||
const effectiveCtas = normalizeCtas(props);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<AnchorIdField />
|
||||
{/* Content */}
|
||||
<div>
|
||||
<label style={labelStyle}>Heading</label>
|
||||
@@ -192,18 +198,20 @@ const HeroSettings: React.FC = () => {
|
||||
<label style={labelStyle}>Subtitle</label>
|
||||
<textarea value={props.subtitle || ''} onChange={(e) => setProp((p: HeroProps) => { p.subtitle = e.target.value; })} rows={3} style={{ ...inputStyle, resize: 'vertical' as const }} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Button Text</label>
|
||||
<input type="text" value={props.buttonText || ''} onChange={(e) => setProp((p: HeroProps) => { p.buttonText = e.target.value; })} style={inputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Button URL</label>
|
||||
<input type="text" value={props.buttonHref || ''} onChange={(e) => setProp((p: HeroProps) => { p.buttonHref = e.target.value; })} placeholder="#" style={inputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Secondary Button Text</label>
|
||||
<input type="text" value={props.secondaryButtonText || ''} onChange={(e) => setProp((p: HeroProps) => { p.secondaryButtonText = e.target.value; })} placeholder="Leave blank to hide" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
{/* Dynamic CTAs */}
|
||||
<CtasEditor
|
||||
ctas={effectiveCtas}
|
||||
onChange={(next) => setProp((p: HeroProps) => {
|
||||
p.ctas = next;
|
||||
// Once the user touches CTAs, the legacy fields are no longer
|
||||
// authoritative — clear them so the array is the only source.
|
||||
p.buttonText = undefined;
|
||||
p.buttonHref = undefined;
|
||||
p.secondaryButtonText = undefined;
|
||||
p.secondaryButtonHref = undefined;
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Background Type */}
|
||||
<div>
|
||||
@@ -370,10 +378,9 @@ HeroSimple.craft = {
|
||||
props: {
|
||||
heading: 'Build Something Amazing',
|
||||
subtitle: 'Create beautiful websites without writing a single line of code.',
|
||||
buttonText: 'Get Started',
|
||||
buttonHref: '#',
|
||||
secondaryButtonText: '',
|
||||
secondaryButtonHref: '#',
|
||||
ctas: [
|
||||
{ text: 'Get Started', href: '#', variant: 'primary' },
|
||||
] as CtaButton[],
|
||||
bgType: 'color',
|
||||
bgColor: '#1e293b',
|
||||
bgGradientFrom: '#667eea',
|
||||
@@ -389,6 +396,7 @@ HeroSimple.craft = {
|
||||
minHeight: '500px',
|
||||
verticalAlign: 'center',
|
||||
textAlign: 'center',
|
||||
anchorId: '',
|
||||
style: {},
|
||||
},
|
||||
rules: {
|
||||
@@ -436,16 +444,16 @@ HeroSimple.craft = {
|
||||
const textAlign = props.textAlign || 'center';
|
||||
const justifyBtn = textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start';
|
||||
|
||||
let buttonsHtml = '';
|
||||
if (props.buttonText) {
|
||||
buttonsHtml += `<a href="${props.buttonHref || '#'}" style="display:inline-block;padding:14px 36px;background-color:${props.buttonBgColor || '#3b82f6'};color:${props.buttonTextColor || '#fff'};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px">${esc(props.buttonText)}</a>`;
|
||||
}
|
||||
if (props.secondaryButtonText) {
|
||||
buttonsHtml += `<a href="${props.secondaryButtonHref || '#'}" style="display:inline-block;padding:14px 36px;background:transparent;color:${props.textColor || '#fff'};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${props.textColor || '#fff'}">${esc(props.secondaryButtonText)}</a>`;
|
||||
}
|
||||
const ctas = normalizeCtas(props);
|
||||
const buttonsHtml = ctasToHtml(ctas, {
|
||||
primaryBg: props.buttonBgColor || '#3b82f6',
|
||||
primaryText: props.buttonTextColor || '#fff',
|
||||
outlineText: props.textColor || '#fff',
|
||||
});
|
||||
|
||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
||||
return {
|
||||
html: `<section style="${sectionStyle}">
|
||||
html: `<section${idAttr} style="${sectionStyle}">
|
||||
${videoHtml}${overlayHtml}
|
||||
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}">
|
||||
<h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${esc(props.heading || '')}</h1>
|
||||
|
||||
Reference in New Issue
Block a user