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
+55
View File
@@ -0,0 +1,55 @@
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react';
/**
* Optional target for a Sitesmith chat session. When set, the modal renders a
* "Editing X" banner and the chat input is biased toward modifying just that
* subtree — the user's prompt is augmented server-side with the node's
* serialized tree, and the AI is instructed to return a `patch` op (typically
* `replace_node`) rather than a full-site replace.
*/
export interface SitesmithTarget {
/** Craft.js node id, used to find the node when applying the patch. */
nodeId: string;
/** Human-readable component name, shown in the modal header. */
displayName: string;
/** The component's serialized subtree (used to build a usable AI prompt). */
treeJson: string;
}
interface SitesmithContextValue {
isOpen: boolean;
target: SitesmithTarget | null;
open: (target?: SitesmithTarget) => void;
close: () => void;
}
const SitesmithCtx = createContext<SitesmithContextValue>({
isOpen: false,
target: null,
open: () => {},
close: () => {},
});
export const useSitesmithModal = () => useContext(SitesmithCtx);
export const SitesmithProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const [target, setTarget] = useState<SitesmithTarget | null>(null);
const open = useCallback((t?: SitesmithTarget) => {
setTarget(t ?? null);
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
setTarget(null);
}, []);
const value = useMemo<SitesmithContextValue>(
() => ({ isOpen, target, open, close }),
[isOpen, target, open, close],
);
return <SitesmithCtx.Provider value={value}>{children}</SitesmithCtx.Provider>;
};