the builder's chrome is drawn inside the rendered page
A component that lives at the PLUGIN level and appears on the site's page: an empty drop zone with an Add section button, injected into the frame at runtime. Nothing of it exists in the website's source, so the site never carries code only an editor would see, and the builder can change without the site being rebuilt. It works because the preview frame is SAME ORIGIN — through the platform's dev proxy, on Officer's own host — so contentDocument is readable and writable. That single fact is what the page builder rests on, and it is why swapping this plugin's hand-rolled cross-origin proxy for the platform's mattered beyond the bugs it fixed. SitePreview grew one prop, `onDocument`, called with the frame's document after every load and returning a cleanup that runs before the next one. So a reload never leaves two sets of controls behind, and nothing injected outlives the document it went into. Plain DOM rather than React. Portalling would mean running a reconciler against a document that reloads underneath it whenever the dev server rebuilds; these are a handful of elements with a click handler, and this way a reload is a document that no longer exists rather than a tree React still thinks it owns. Inline styles, not classes. The frame carries the SITE's stylesheet, built by scanning the site's own templates — a class invented in this file was never scanned and would silently do nothing. The button takes its colours from --site-fg and --site-bg, so it belongs to whatever theme the site is wearing. The page's existing content is HIDDEN rather than removed, so the same call will leave real blocks visible once there are any. The panel counts the presses for now. That is the only visible proof the wire runs both ways, and it becomes the section picker when there are sections to pick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { readableError } from '../errors';
|
||||
import { SitePreview, useDevPort } from '../SitePreview';
|
||||
import { paintCanvas } from '../canvas';
|
||||
import { usePages, type Page } from '../usePages';
|
||||
|
||||
/**
|
||||
@@ -32,6 +33,19 @@ export const PageBuilderPanel = () => {
|
||||
const { pages, announce } = usePages();
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
/** Proof the frame is talking back. Replaced by the section picker once there are sections to pick. */
|
||||
const [asked, setAsked] = useState(0);
|
||||
|
||||
/*
|
||||
The builder's chrome, painted into the rendered page.
|
||||
|
||||
Stable, because SitePreview holds it and re-runs it on every load. A new function each render would
|
||||
be a new injection each render.
|
||||
*/
|
||||
const onDocument = useCallback(
|
||||
(frameDocument: Document) => paintCanvas(frameDocument, { onAddSection: () => setAsked((n) => n + 1) }),
|
||||
[],
|
||||
);
|
||||
|
||||
/*
|
||||
The list, if nobody has announced it yet.
|
||||
@@ -66,18 +80,23 @@ export const PageBuilderPanel = () => {
|
||||
<span className="font-mono text-[10px] text-muted-foreground">{page.path}</span>
|
||||
</div>
|
||||
|
||||
{/* The real page, at its own address. `path` has no leading slash — Home's is the empty string,
|
||||
which the proxy resolves to the site's root. */}
|
||||
<SitePreview port={devPort} path={page.slug} />
|
||||
{/* The real page, at its own address, with the builder's own controls drawn inside it. `path`
|
||||
has no leading slash — Home's is the empty string, which the proxy resolves to the root. */}
|
||||
<SitePreview port={devPort} path={page.slug} onDocument={onDocument} />
|
||||
|
||||
{/*
|
||||
Where the blocks go.
|
||||
The other end of the wire.
|
||||
|
||||
Said rather than left blank, because an empty panel under a working preview reads as something
|
||||
that failed to load. This is the next thing to build and the panel should say so.
|
||||
Temporary, and it earns its place until the section picker exists: it is the only visible proof
|
||||
that a button drawn inside the rendered page reaches React out here. When there are sections to
|
||||
choose, this becomes the chooser.
|
||||
*/}
|
||||
<div className="shrink-0 rounded-lg border border-dashed border-border p-4 text-center">
|
||||
<p className="text-xs text-muted-foreground">The blocks that make up this page go here.</p>
|
||||
<div className="shrink-0 rounded-lg border border-dashed border-border p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{asked === 0
|
||||
? 'Press Add section on the page.'
|
||||
: `Add section pressed ${asked} time${asked === 1 ? '' : 's'} — the page is talking to the panel.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user