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
+9 -1
View File
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, Element, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { Container } from './Container';
import { AnchorIdField } from '../../ui/AnchorIdField';
/* ---------- Shape Divider SVG Paths ---------- */
@@ -27,6 +28,7 @@ interface SectionProps {
bottomDivider?: DividerShape;
bottomDividerColor?: string;
bottomDividerHeight?: string;
anchorId?: string;
}
/* ---------- Divider renderer ---------- */
@@ -85,6 +87,7 @@ export const Section: UserComponent<SectionProps> = ({
bottomDivider = 'none',
bottomDividerColor = '#ffffff',
bottomDividerHeight = '50px',
anchorId,
}) => {
const { connectors: { connect, drag } } = useNode();
@@ -94,6 +97,7 @@ export const Section: UserComponent<SectionProps> = ({
return (
<section
ref={(ref: HTMLElement | null) => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
width: '100%',
position: (hasTopDivider || hasBottomDivider) ? 'relative' : undefined,
@@ -229,6 +233,7 @@ const SectionSettings: React.FC = () => {
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
<AnchorIdField />
<div>
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background Color</label>
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
@@ -333,6 +338,7 @@ Section.craft = {
bottomDivider: 'none',
bottomDividerColor: '#ffffff',
bottomDividerHeight: '50px',
anchorId: '',
},
rules: {
canDrag: () => true,
@@ -377,6 +383,7 @@ function buildDividerHtml(
}
(Section as any).toHtml = (props: SectionProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const hasTopDivider = props.topDivider && props.topDivider !== 'none';
const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none';
@@ -394,8 +401,9 @@ function buildDividerHtml(
const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top');
const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom');
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
return {
html: `<section${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`,
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`,
};
};