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:
2026-05-25 12:43:28 -07:00
parent 7b747f775f
commit d0925d9e2d
24 changed files with 723 additions and 237 deletions
+10 -2
View File
@@ -4,6 +4,7 @@ import { cssPropsToString } from '../../utils/style-helpers';
import { SettingsTabs } from '../../ui/SettingsTabs';
import { BorderControl } from '../../ui/BorderControl';
import { AdvancedTab } from '../../ui/AdvancedTab';
import { AnchorIdField } from '../../ui/AnchorIdField';
interface ContainerProps {
style?: CSSProperties;
@@ -11,6 +12,7 @@ interface ContainerProps {
children?: React.ReactNode;
cssId?: string;
cssClass?: string;
anchorId?: string;
hideOnDesktop?: boolean;
hideOnTablet?: boolean;
hideOnMobile?: boolean;
@@ -37,6 +39,7 @@ export const Container: UserComponent<ContainerProps> = ({
children,
fullWidth = false,
contentWidth = 'full',
anchorId,
}) => {
const { connectors: { connect, drag } } = useNode();
@@ -56,6 +59,7 @@ export const Container: UserComponent<ContainerProps> = ({
ref: (ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); },
style: outerStyle,
'data-craft-container': 'true',
id: anchorId || undefined,
},
needsBoxedWrapper
? React.createElement('div', { style: { maxWidth: '1200px', margin: '0 auto', ...flexStyles } }, children)
@@ -120,6 +124,7 @@ const ContainerSettings: React.FC = () => {
<SettingsTabs
general={
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<AnchorIdField />
{/* Tag */}
<div>
<label style={cLabelStyle}>HTML Element</label>
@@ -304,6 +309,7 @@ Container.craft = {
tag: 'div',
fullWidth: false,
contentWidth: 'full',
anchorId: '',
},
rules: {
canDrag: () => true,
@@ -318,6 +324,7 @@ Container.craft = {
/* ---------- HTML export ---------- */
(Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const tag = props.tag || 'div';
const isBoxed = props.contentWidth === 'boxed';
const flexStyles = flexAlignFromTextAlign(props.style?.textAlign);
@@ -333,11 +340,12 @@ Container.craft = {
}
const styleStr = cssPropsToString(outerCss);
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
if (isBoxed) {
const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles });
return { html: `<${tag}${styleStr ? ` style="${styleStr}"` : ''}><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></${tag}>` };
return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></${tag}>` };
}
return { html: `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}</${tag}>` };
return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}</${tag}>` };
};