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, useState } from 'react';
import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from './Container';
import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField';
type SplitOption =
| '100'
@@ -18,6 +19,7 @@ interface ColumnLayoutProps {
gap?: string;
style?: CSSProperties;
children?: React.ReactNode;
anchorId?: string;
}
const splitToWidths: Record<string, string[]> = {
@@ -59,6 +61,7 @@ export const ColumnLayout: UserComponent<ColumnLayoutProps> = ({
split = '50-50',
gap = '16px',
style = {},
anchorId,
}) => {
const { connectors: { connect, drag } } = useNode();
const widths = getWidths(split, columns);
@@ -66,6 +69,7 @@ export const ColumnLayout: UserComponent<ColumnLayoutProps> = ({
return (
<div
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
display: 'flex',
flexWrap: 'wrap',
@@ -124,6 +128,7 @@ const ColumnLayoutSettings: React.FC = () => {
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
<AnchorIdField />
{/* Preset layouts */}
<div>
<label style={labelStyle}>Column Layout</label>
@@ -270,6 +275,7 @@ ColumnLayout.craft = {
split: '50-50',
gap: '16px',
style: {},
anchorId: '',
},
rules: {
canDrag: () => true,
@@ -284,6 +290,7 @@ ColumnLayout.craft = {
/* ---------- HTML export ---------- */
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
const esc = (s: any) => String(s ?? "").replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const gap = props.gap || '16px';
const outerStyle = cssPropsToString({
display: 'flex',
@@ -292,7 +299,8 @@ ColumnLayout.craft = {
width: '100%',
...props.style,
});
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
return {
html: `<div${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
};
};