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
@@ -1,6 +1,7 @@
import React, { CSSProperties, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { AnchorIdField } from '../../ui/AnchorIdField';
interface Testimonial {
quote: string;
@@ -16,6 +17,7 @@ interface TestimonialsProps {
style?: CSSProperties;
cardBg?: string;
starColor?: string;
anchorId?: string;
}
const defaultTestimonials: Testimonial[] = [
@@ -52,6 +54,7 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
style = {},
cardBg = '#f8fafc',
starColor = '#f59e0b',
anchorId,
}) => {
const {
connectors: { connect, drag },
@@ -86,6 +89,7 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
padding: '80px 20px',
backgroundColor: '#ffffff',
@@ -187,6 +191,7 @@ const TestimonialsSettings: React.FC = () => {
return (
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
<AnchorIdField />
{/* Layout */}
<div>
<label style={labelStyle}>Layout</label>
@@ -357,6 +362,7 @@ Testimonials.craft = {
style: { backgroundColor: '#ffffff' },
cardBg: '#f8fafc',
starColor: '#f59e0b',
anchorId: '',
},
rules: {
canDrag: () => true,
@@ -388,6 +394,7 @@ Testimonials.craft = {
backgroundColor: '#ffffff',
...style,
});
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
@@ -403,7 +410,7 @@ Testimonials.craft = {
if (layout === 'single') {
// For single layout, export as grid with 1 column (simpler static export)
return {
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div style="max-width:600px;margin:0 auto;display:grid;grid-template-columns:1fr;gap:24px">
${cards}
</div>
@@ -412,7 +419,7 @@ Testimonials.craft = {
}
return {
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(${columns},1fr);gap:24px">
${cards}
</div>