@plenary/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @plenary/react
2
+
3
+ The React renderer for Plenary's page builder: render an entry's `blocks`
4
+ component tree, register your own components so editors can use them, and
5
+ embed the live-preview bridge that lets the Plenary console edit your site
6
+ in place.
7
+
8
+ ```sh
9
+ npm install @plenary/react @plenary/client
10
+ ```
11
+
12
+ ESM-only; React 18 or 19.
13
+
14
+ ## Rendering a page
15
+
16
+ `PlenaryPage` is an async server component that renders a `PlenaryDocument` —
17
+ the JSON DOM stored in an entry's `blocks` field:
18
+
19
+ ```tsx
20
+ import { createClient } from "@plenary/client";
21
+ import { PlenaryPage } from "@plenary/react";
22
+
23
+ const plenary = createClient({
24
+ baseUrl: process.env.PLENARY_URL!,
25
+ siteKey: "main",
26
+ apiKey: process.env.PLENARY_API_KEY!,
27
+ locale: "en",
28
+ });
29
+
30
+ export default async function Page({ params }) {
31
+ const entry = await plenary.getEntry("page", params.slug);
32
+ return <PlenaryPage document={entry.data.blocks} client={plenary} />;
33
+ }
34
+ ```
35
+
36
+ The built-in components — `Box`, `Section`, `Text`, `Image`, `Video`, `Form` —
37
+ cover layout, media (with server-generated variants and focal-point crops),
38
+ and form rendering out of the box. Pass a `client` so media and form props
39
+ resolve; interactive pieces (form submission) need your tree wrapped in a
40
+ `PlenaryProvider`:
41
+
42
+ ```tsx
43
+ import { PlenaryProvider } from "@plenary/react";
44
+
45
+ <PlenaryProvider client={plenary}>{children}</PlenaryProvider>
46
+ ```
47
+
48
+ ## Registering your own components
49
+
50
+ Editors can only place components the site registers. Registration pairs a
51
+ React component with the metadata the console's page builder needs — prop
52
+ definitions, named slots, style panels:
53
+
54
+ ```tsx
55
+ import { register } from "@plenary/react";
56
+
57
+ function Hero({ heading, children }) {
58
+ return (
59
+ <header>
60
+ <h1>{heading}</h1>
61
+ {children}
62
+ </header>
63
+ );
64
+ }
65
+
66
+ register(Hero, {
67
+ key: "hero",
68
+ label: "Hero",
69
+ props: [{ key: "heading", type: "text" }],
70
+ slots: [{ name: "children", label: "Content" }],
71
+ });
72
+ ```
73
+
74
+ `registerTheme` declares the site's design tokens so the console's style
75
+ editor offers them alongside raw CSS.
76
+
77
+ ## Live preview
78
+
79
+ `PlenaryBridge` (from `@plenary/react/bridge`) turns a page into a live
80
+ preview surface for the console: rendered inside the page builder's iframe,
81
+ it receives draft edits over postMessage and re-renders in place, reporting
82
+ the registered component manifest back to the editor.
83
+
84
+ ```tsx
85
+ import { PlenaryBridge } from "@plenary/react/bridge";
86
+
87
+ <PlenaryBridge
88
+ region="blocks"
89
+ editorOrigins={["https://cms.example.com", "http://localhost:*"]}
90
+ />
91
+ ```
92
+
93
+ Only messages from `editorOrigins` are honored — exact origins,
94
+ `https://*.example.com` subdomain patterns, or `http://localhost:*` port
95
+ patterns; everything else is ignored.
96
+
97
+ Full documentation at [docs.plenarycms.com](https://docs.plenarycms.com).
package/dist/bridge.js CHANGED
@@ -256,6 +256,24 @@ function PlenaryBridge({
256
256
  setHoverId(p.hoverId);
257
257
  break;
258
258
  }
259
+ // A palette drag the console is tracking on our behalf. It reports the
260
+ // pointer in our own coordinate space; resolving it against the
261
+ // rendered DOM is something only this side can do.
262
+ case "canvasDragOver": {
263
+ const p = msg.payload;
264
+ setDropHint(resolveDrop(p.x, p.y));
265
+ break;
266
+ }
267
+ case "canvasDragLeave":
268
+ setDropHint(null);
269
+ break;
270
+ case "canvasDragDrop": {
271
+ const p = msg.payload;
272
+ setDropHint(null);
273
+ const { target } = resolveDrop(p.x, p.y);
274
+ send("canvasDrop", { component: p.component, target }, e.origin);
275
+ break;
276
+ }
259
277
  }
260
278
  };
261
279
  window.addEventListener("message", onMessage);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bridge.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type DragEvent as ReactDragEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from \"react\";\nimport {\n PROTOCOL_VERSION,\n DND_NEW,\n hasWildcardPattern,\n isAllowedEditorOrigin,\n type PlenaryDocument,\n type PlenaryNode,\n type ManifestPayload,\n type RenderPayload,\n type PatchPayload,\n type HighlightPayload,\n type CanvasDropPayload,\n type CanvasDropTarget,\n type SizePayload,\n type WheelPayload,\n type PanPayload,\n type KeyPayload,\n type EditPayload,\n} from \"@plenary/core\";\nimport { renderDocumentSync } from \"./render.js\";\nimport { getManifest, getTheme } from \"./registry.js\";\n\nexport interface PlenaryBridgeProps {\n /**\n * The `blocks` field on the entry this bridge previews — the key in the draft's\n * `data` whose component tree it renders. One `<PlenaryBridge>` drives one region.\n */\n region: string;\n /**\n * Console origins allowed to drive this preview; others are ignored. Each\n * entry is an exact origin (`https://cms.example.com`, `http://localhost:5173`)\n * or a pattern: `https://*.example.com` allows any subdomain (never the bare\n * apex), `http://localhost:*` allows any port — so a locally-running console\n * can drive a production site. See `isAllowedEditorOrigin` in `@plenary/core`.\n */\n editorOrigins: string[];\n /** Reported to the console in the `hello` handshake. */\n sdkVersion?: string;\n /** Normal page content, rendered until a draft render takes over. */\n children?: ReactNode;\n}\n\ninterface Draft {\n data: Record<string, unknown>;\n locale: string;\n}\n\n/** Return a new document with `propsPatch` merged into the node with `nodeId`. */\nfunction patchDom(\n doc: PlenaryDocument,\n nodeId: string,\n propsPatch: Record<string, unknown>,\n): PlenaryDocument {\n const patch = (nodes: PlenaryNode[]): PlenaryNode[] =>\n nodes.map((node) => {\n let next = node;\n if (node.id === nodeId) next = { ...next, props: { ...next.props, ...propsPatch } };\n if (next.slots) {\n const slots: Record<string, PlenaryNode[]> = {};\n for (const [name, children] of Object.entries(next.slots)) slots[name] = patch(children);\n next = { ...next, slots };\n }\n return next;\n });\n return { ...doc, root: patch(doc.root) };\n}\n\nfunction nodeIdFromEvent(e: ReactMouseEvent): string | null {\n const el = (e.target as HTMLElement | null)?.closest?.(\"[data-plenary-id]\");\n return el?.getAttribute(\"data-plenary-id\") ?? null;\n}\n\nfunction findNodeById(nodes: PlenaryNode[], id: string): PlenaryNode | null {\n for (const node of nodes) {\n if (node.id === id) return node;\n if (node.slots) {\n for (const children of Object.values(node.slots)) {\n const hit = findNodeById(children, id);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/**\n * The contenteditable mode for inline editing: `plaintext-only` where supported\n * (assigning an unsupported value to the property throws per spec), else `true`\n * — pasted markup is flattened on read either way.\n */\nlet editableMode: string | null = null;\nfunction getEditableMode(): string {\n if (editableMode) return editableMode;\n const probe = document.createElement(\"div\");\n try {\n probe.contentEditable = \"plaintext-only\";\n editableMode = \"plaintext-only\";\n } catch {\n editableMode = \"true\";\n }\n return editableMode;\n}\n\n/** A live inline-editing session on a text block's rendered element. */\ninterface EditSession {\n nodeId: string;\n el: HTMLElement;\n /** The committed content when editing began, to skip no-op edits. */\n original: string;\n onBlur: () => void;\n}\n\n/**\n * Keep the framed page from navigating away from the entry being edited. While\n * `locked`, link clicks and form submits inside the frame are cancelled — but\n * nothing else is: the listeners run in the capture phase and only call\n * `preventDefault()`, never `stopPropagation()`, so the bridge's own click/hover\n * selection (and any other event the SDK relies on) still fire normally.\n *\n * Toggleable by design: pass `false` and the lock lifts. It is tied to the\n * active preview today; when the console later lets the user browse their own\n * site and resolves the entry from the preview URL, that mode would drive this\n * flag (over the protocol) instead of blocking outright.\n */\nfunction useNavigationLock(locked: boolean) {\n useEffect(() => {\n if (!locked || typeof document === \"undefined\") return;\n const onClick = (e: MouseEvent) => {\n if ((e.target as Element | null)?.closest?.(\"a[href]\")) e.preventDefault();\n };\n const onSubmit = (e: Event) => e.preventDefault();\n document.addEventListener(\"click\", onClick, true);\n document.addEventListener(\"submit\", onSubmit, true);\n return () => {\n document.removeEventListener(\"click\", onClick, true);\n document.removeEventListener(\"submit\", onSubmit, true);\n };\n }, [locked]);\n}\n\ninterface DropHint {\n target: CanvasDropTarget;\n /** Viewport rect of the reference element, for drawing the indicator. */\n rect: DOMRect | null;\n mode: \"before\" | \"after\" | \"into\";\n}\n\n/**\n * Resolve a pointer position over the rendered preview to a tree drop target:\n * before/after the block under the cursor, into an empty Section column, or the\n * region root. Uses the `data-plenary-id` / `data-plenary-column` annotations the\n * renderer emits.\n */\nfunction resolveDrop(x: number, y: number): DropHint {\n const root = document.querySelector(\"[data-plenary-preview]\");\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n if (!root || !el || !root.contains(el)) {\n return { target: { at: \"root\" }, rect: root?.getBoundingClientRect() ?? null, mode: \"after\" };\n }\n const marker = el.closest(\"[data-plenary-id]\") as HTMLElement | null;\n const column = el.closest(\"[data-plenary-column]\") as HTMLElement | null;\n\n // Empty column space: the nearest block marker is the Section (an ancestor of\n // the column), not a child inside it → drop into that column's slot.\n if (column && (!marker || !column.contains(marker))) {\n const sectionId = column.closest(\"[data-plenary-id]\")?.getAttribute(\"data-plenary-id\");\n const slot = column.getAttribute(\"data-plenary-column\");\n if (sectionId && slot != null) {\n return { target: { at: \"slot\", nodeId: sectionId, slot }, rect: column.getBoundingClientRect(), mode: \"into\" };\n }\n }\n\n if (marker) {\n const id = marker.getAttribute(\"data-plenary-id\")!;\n // Markers use display:contents (no box); measure the rendered child.\n const box = (marker.firstElementChild as HTMLElement | null) ?? marker;\n const rect = box.getBoundingClientRect();\n const after = y > rect.top + rect.height / 2;\n return { target: { at: after ? \"after\" : \"before\", nodeId: id }, rect, mode: after ? \"after\" : \"before\" };\n }\n\n return { target: { at: \"root\" }, rect: root.getBoundingClientRect(), mode: \"after\" };\n}\n\n/**\n * The iframe side of live preview. Mounted by the site; inert until a valid\n * handshake from an allowed origin arrives. Once active it renders the draft the\n * console pushes over postMessage instead of the normal page, and reports\n * clicks/hovers back for block selection.\n *\n * Origin discipline is the security model: every inbound message must come from\n * an `editorOrigins` origin and carry `{ plenary: true }`.\n */\nexport function PlenaryBridge({\n region,\n editorOrigins,\n sdkVersion = \"0.0.0\",\n children,\n}: PlenaryBridgeProps) {\n const [draft, setDraft] = useState<Draft | null>(null);\n const [active, setActive] = useState(false);\n // Two outline channels the console drives independently: the selected block and\n // whatever is hovered (a tree row or a preview element). Drawn with distinct styles.\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [hoverId, setHoverId] = useState<string | null>(null);\n const [dropHint, setDropHint] = useState<DropHint | null>(null);\n const consoleOrigin = useRef<string | null>(null);\n // Nodes whose DOM must be rebuilt from the document on the next render (their\n // key changes), because an inline edit mutated DOM React never tracked.\n const [remountEpochs, setRemountEpochs] = useState<ReadonlyMap<string, number>>(new Map());\n const editSession = useRef<EditSession | null>(null);\n\n // While the console is driving the preview, don't let a stray link or form\n // submit navigate the frame off the entry being edited.\n useNavigationLock(active);\n\n // The component tree this bridge renders: the `region` blocks field of the draft.\n const regionDoc = draft ? (draft.data[region] as PlenaryDocument | undefined) : undefined;\n\n const send = useCallback((type: string, payload: unknown, origin?: string) => {\n const target = origin ?? consoleOrigin.current;\n if (typeof window === \"undefined\" || !target) return;\n window.parent.postMessage({ plenary: true, v: PROTOCOL_VERSION, type, payload }, target);\n }, []);\n\n /** End the inline-editing session: tear down, remount the node, send the change up. */\n const endEdit = useCallback(\n (commit: boolean) => {\n const session = editSession.current;\n if (!session) return;\n editSession.current = null;\n const { nodeId, el, original, onBlur } = session;\n el.removeEventListener(\"blur\", onBlur);\n el.removeAttribute(\"contenteditable\");\n const text = (el.innerText ?? el.textContent ?? \"\").toString();\n // Rebuild the node from the document: typing left DOM state React never saw.\n setRemountEpochs((prev) => new Map(prev).set(nodeId, (prev.get(nodeId) ?? 0) + 1));\n if (!commit || text === original) return;\n // Apply locally first so the remounted node renders the new text at once;\n // the console's render echo then arrives already matching.\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, nodeId, { content: text }) } };\n });\n send(\"edit\", { nodeId, props: { content: text } } satisfies EditPayload);\n },\n [region, send],\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\" || window.parent === window) return; // not framed\n\n // Report the page's content height so the console's artboard can size the\n // iframe to the whole page (no inner scrollbar). Sent on init and, trailing-\n // throttled, whenever the body's size changes; send() drops it until the\n // console origin is known.\n const reportSize = () => {\n send(\"size\", { height: Math.ceil(document.documentElement.scrollHeight) } satisfies SizePayload);\n };\n let sizeTimer: ReturnType<typeof setTimeout> | null = null;\n let observer: ResizeObserver | null = null;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(() => {\n if (sizeTimer) return;\n sizeTimer = setTimeout(() => {\n sizeTimer = null;\n reportSize();\n }, 150);\n });\n observer.observe(document.body);\n }\n\n // Artboard gestures performed over the page. The iframe consumes these\n // events, so the console never sees them — forward them once connected:\n // Ctrl/Cmd+wheel becomes a zoom request (page point included so the console\n // can anchor the zoom at the cursor), and a middle-button drag becomes pan\n // steps (the hand tool). Both are prevented locally: the page must neither\n // scroll under a zoom gesture nor start autoscroll under a pan.\n const onWheelGesture = (e: WheelEvent) => {\n if (!consoleOrigin.current || !(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n send(\"wheel\", { deltaY: e.deltaY, x: e.clientX, y: e.clientY } satisfies WheelPayload);\n };\n let panFrom: { x: number; y: number } | null = null;\n const onPanStart = (e: PointerEvent) => {\n if (!consoleOrigin.current || e.button !== 1) return;\n panFrom = { x: e.clientX, y: e.clientY };\n // Capture keeps the drag alive even when the pointer leaves the iframe.\n try {\n (e.target as Element | null)?.setPointerCapture?.(e.pointerId);\n } catch {\n /* capture is best-effort */\n }\n };\n const onPanMove = (e: PointerEvent) => {\n if (!panFrom) return;\n // The middle button was released outside our reach (e.g. over browser chrome).\n if ((e.buttons & 4) === 0) {\n panFrom = null;\n return;\n }\n send(\"pan\", { dx: e.clientX - panFrom.x, dy: e.clientY - panFrom.y } satisfies PanPayload);\n panFrom = { x: e.clientX, y: e.clientY };\n };\n const onPanEnd = (e: PointerEvent) => {\n if (e.button === 1) panFrom = null;\n };\n // Middle-button autoscroll is a `mousedown` default, not a pointerdown one.\n const onMiddleMouseDown = (e: MouseEvent) => {\n if (consoleOrigin.current && e.button === 1) e.preventDefault();\n };\n // Editor shortcuts pressed while the frame has focus (clicking a block\n // focuses the iframe, so the console's own keydown listener never fires).\n // Forward only recognized shortcut combinations, preventing their default\n // here — the frame must not run its own native undo or save dialog.\n const onKeyDown = (e: KeyboardEvent) => {\n // Inside an inline-edit session, Escape cancels and Enter commits\n // (Shift+Enter keeps the native line break); all other keys are native\n // editing — never forwarded (the editable-target check below also holds).\n const session = editSession.current;\n if (session && session.el.contains(e.target as Node)) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n endEdit(false);\n } else if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n endEdit(true);\n }\n return;\n }\n if (!consoleOrigin.current) return;\n const t = e.target as HTMLElement | null;\n if (\n t &&\n (t.isContentEditable ||\n t instanceof HTMLInputElement ||\n t instanceof HTMLTextAreaElement ||\n t instanceof HTMLSelectElement)\n )\n return;\n const key = e.key.toLowerCase();\n const mod = e.ctrlKey || e.metaKey;\n // A live text selection in the page keeps native copy/cut.\n if (mod && (key === \"c\" || key === \"x\") && document.getSelection()?.isCollapsed === false)\n return;\n const forward = mod\n ? [\"z\", \"y\", \"s\", \"d\", \"c\", \"x\", \"v\"].includes(key)\n : key === \"delete\" || key === \"backspace\";\n if (!forward) return;\n e.preventDefault();\n send(\"key\", {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n } satisfies KeyPayload);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n document.addEventListener(\"wheel\", onWheelGesture, { passive: false });\n document.addEventListener(\"pointerdown\", onPanStart);\n document.addEventListener(\"pointermove\", onPanMove);\n document.addEventListener(\"pointerup\", onPanEnd);\n document.addEventListener(\"mousedown\", onMiddleMouseDown);\n\n const onMessage = (e: MessageEvent) => {\n if (!isAllowedEditorOrigin(editorOrigins, e.origin)) return;\n const msg = e.data as { plenary?: boolean; type?: string; payload?: unknown } | null;\n if (!msg || msg.plenary !== true) return;\n\n switch (msg.type) {\n case \"init\":\n consoleOrigin.current = e.origin;\n setActive(true);\n send(\n \"manifest\",\n { components: getManifest(), theme: getTheme() } satisfies ManifestPayload,\n e.origin,\n );\n reportSize();\n break;\n case \"render\":\n consoleOrigin.current = e.origin;\n setActive(true);\n // A draft arriving mid-edit means the console changed the document\n // (e.g. toolbar undo); it is authoritative — cancel the session.\n endEdit(false);\n setDraft(msg.payload as RenderPayload);\n break;\n case \"patch\": {\n endEdit(false);\n const p = msg.payload as PatchPayload;\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, p.nodeId, p.props) } };\n });\n break;\n }\n case \"highlight\": {\n const p = msg.payload as HighlightPayload;\n setSelectedId(p.selectedId);\n setHoverId(p.hoverId);\n break;\n }\n }\n };\n\n window.addEventListener(\"message\", onMessage);\n // Announce readiness to every allowed console origin. Exact origins are\n // addressed directly; a wildcard pattern cannot be a postMessage target, so\n // when any is configured the announce also goes out with target \"*\". That is\n // safe for `hello` alone — it carries only version numbers — and everything\n // after it is sent strictly to the matched origin of a validated `init`.\n const hello = { protocolVersion: PROTOCOL_VERSION, sdkVersion };\n for (const origin of editorOrigins) {\n if (!origin.includes(\"*\")) send(\"hello\", hello, origin);\n }\n if (hasWildcardPattern(editorOrigins)) send(\"hello\", hello, \"*\");\n return () => {\n window.removeEventListener(\"message\", onMessage);\n document.removeEventListener(\"keydown\", onKeyDown);\n document.removeEventListener(\"wheel\", onWheelGesture);\n document.removeEventListener(\"pointerdown\", onPanStart);\n document.removeEventListener(\"pointermove\", onPanMove);\n document.removeEventListener(\"pointerup\", onPanEnd);\n document.removeEventListener(\"mousedown\", onMiddleMouseDown);\n observer?.disconnect();\n if (sizeTimer) clearTimeout(sizeTimer);\n };\n }, [editorOrigins, sdkVersion, region, send, endEdit]);\n\n const beginEdit = (nodeId: string, el: HTMLElement) => {\n endEdit(true); // switching targets commits the previous session\n const onBlur = () => endEdit(true);\n el.setAttribute(\"contenteditable\", getEditableMode());\n el.addEventListener(\"blur\", onBlur);\n editSession.current = { nodeId, el, original: el.textContent ?? \"\", onBlur };\n el.focus();\n };\n\n const onPreviewDoubleClick = (e: ReactMouseEvent) => {\n const id = nodeIdFromEvent(e);\n if (!id || !regionDoc) return;\n // v1: inline editing is the built-in text block's `content` — its DOM is\n // predictably marker > element > text node.\n if (findNodeById(regionDoc.root, id)?.component !== \"text\") return;\n const el = (e.target as HTMLElement).closest(\"[data-plenary-id]\")\n ?.firstElementChild as HTMLElement | null;\n if (el) beginEdit(id, el);\n };\n\n const isPaletteDrag = (e: ReactDragEvent) => e.dataTransfer.types.includes(DND_NEW);\n\n const onDragOver = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n setDropHint(resolveDrop(e.clientX, e.clientY));\n };\n const onDrop = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n const component = e.dataTransfer.getData(DND_NEW);\n const { target } = resolveDrop(e.clientX, e.clientY);\n if (component) send(\"canvasDrop\", { component, target } satisfies CanvasDropPayload);\n setDropHint(null);\n };\n const onDragLeave = (e: ReactDragEvent) => {\n // Clear only when the pointer leaves the preview root entirely.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDropHint(null);\n };\n\n if (!active) return <>{children}</>;\n\n return (\n <div\n data-plenary-preview=\"\"\n onClick={(e) => {\n const id = nodeIdFromEvent(e);\n if (id) send(\"select\", { nodeId: id });\n }}\n onDoubleClick={onPreviewDoubleClick}\n onMouseOver={(e) => send(\"hover\", { nodeId: nodeIdFromEvent(e) })}\n onMouseOut={() => send(\"hover\", { nodeId: null })}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onDragLeave={onDragLeave}\n >\n {regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null}\n {regionDoc\n ? videoControlsBadgeIds(regionDoc).map((id) => <VideoControlsBadge key={id} nodeId={id} />)\n : null}\n {selectedId ? <HighlightOverlay nodeId={selectedId} variant=\"selected\" /> : null}\n {hoverId && hoverId !== selectedId ? (\n <HighlightOverlay nodeId={hoverId} variant=\"hover\" />\n ) : null}\n {dropHint ? <DropIndicator hint={dropHint} /> : null}\n </div>\n );\n}\n\n/** The live insertion indicator drawn over the canvas while dragging a component. */\nfunction DropIndicator({ hint }: { hint: DropHint }) {\n if (!hint.rect) return null;\n const { rect, mode } = hint;\n const base: CSSProperties = { position: \"fixed\", pointerEvents: \"none\", zIndex: 2147483647 };\n if (mode === \"into\") {\n return (\n <div\n style={{\n ...base,\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n outline: \"2px dashed #4f46e5\",\n outlineOffset: \"-2px\",\n background: \"rgba(79,70,229,0.06)\",\n }}\n />\n );\n }\n const y = mode === \"before\" ? rect.top : rect.bottom;\n return (\n <div style={{ ...base, left: rect.left, top: y - 1, width: rect.width, height: 2, background: \"#4f46e5\" }} />\n );\n}\n\nfunction sameBox(a: DOMRect | null, b: DOMRect | null): boolean {\n if (!a || !b) return a === b;\n return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;\n}\n\n/**\n * A fixed outline over a highlighted node, driven from the console sidebar. The\n * `selected` variant marks the block being edited (thick, solid, saturated); the\n * `hover` variant marks whatever the cursor is over (thinner, dashed, lighter) so\n * the two read as clearly different states.\n */\nconst OUTLINE_STYLE: Record<\"selected\" | \"hover\", CSSProperties> = {\n selected: { outline: \"2px solid #4f46e5\" },\n hover: { outline: \"1px dashed #818cf8\" },\n};\n\n// An overlay is positioned in viewport coordinates, so it goes stale as soon\n// as the block moves — a scroll, a resize, an image finishing loading, or an\n// edit that reflows the page above it. Re-measuring every frame tracks all of\n// those without enumerating them; state changes only when the box actually\n// does, so a still page costs one measurement per frame and no re-render.\nfunction useNodeRect(nodeId: string): DOMRect | null {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n let frame = 0;\n const measure = () => {\n const marker = document.querySelector(`[data-plenary-id=\"${CSS.escape(nodeId)}\"]`);\n // Markers use display:contents (no box of their own), so measure the child.\n const target = (marker?.firstElementChild as HTMLElement | null) ?? (marker as HTMLElement | null);\n const next = target ? target.getBoundingClientRect() : null;\n setRect((prev) => (sameBox(prev, next) ? prev : next));\n frame = requestAnimationFrame(measure);\n };\n measure();\n return () => cancelAnimationFrame(frame);\n }, [nodeId]);\n\n return rect;\n}\n\nfunction HighlightOverlay({ nodeId, variant }: { nodeId: string; variant: \"selected\" | \"hover\" }) {\n const rect = useNodeRect(nodeId);\n if (!rect) return null;\n return (\n <div\n style={{\n position: \"fixed\",\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n ...OUTLINE_STYLE[variant],\n pointerEvents: \"none\",\n zIndex: 2147483647,\n }}\n />\n );\n}\n\n/**\n * Ids of video nodes whose published player will show native controls. The\n * editor render hides them (a controls-less video lets clicks bubble to\n * selection), so the canvas draws a cosmetic badge instead — the hint that\n * the real page will have a control bar. Fidelity note: the renderer does not\n * merge registration defaults into props, so only an explicit truthy\n * `controls` counts, exactly like the published render. Media-less nodes show\n * the placeholder, which carries its own glyph — as do nodes whose asset was\n * deleted, where the published page renders no player to hint at either.\n */\nfunction videoControlsBadgeIds(doc: PlenaryDocument): string[] {\n const ids: string[] = [];\n const walk = (nodes: PlenaryNode[]): void => {\n for (const node of nodes) {\n const media = node.props?.media as { missing?: boolean } | undefined;\n if (node.component === \"video\" && node.props?.controls && media && !media.missing) {\n ids.push(node.id);\n }\n for (const children of Object.values(node.slots ?? {})) walk(children);\n }\n };\n walk(doc.root);\n return ids;\n}\n\n/**\n * A miniature control-bar chip drawn over a video block's bottom-left corner.\n * Purely cosmetic (pointer-events none) and drawn outside the annotated tree,\n * so node styles and selection behavior are untouched.\n */\nfunction VideoControlsBadge({ nodeId }: { nodeId: string }) {\n const rect = useNodeRect(nodeId);\n // On a tiny player the chip would cover the content it hints at.\n if (!rect || rect.width < 96 || rect.height < 48) return null;\n\n const glyph = (d: string) => (\n <svg key={d} width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d={d} />\n </svg>\n );\n return (\n <div\n data-plenary-video-controls-badge=\"\"\n style={{\n position: \"fixed\",\n left: rect.left + 8,\n top: rect.bottom - 26,\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"4px 9px\",\n borderRadius: 9999,\n background: \"rgba(0,0,0,0.65)\",\n color: \"#fff\",\n pointerEvents: \"none\",\n zIndex: 2147483646, // just under the highlight outlines\n }}\n >\n {glyph(\"M8 5v14l11-7z\") /* play */}\n {glyph(\"M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z\") /* volume */}\n {glyph(\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\") /* fullscreen */}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/bridge.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type CSSProperties,\n type DragEvent as ReactDragEvent,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from \"react\";\nimport {\n PROTOCOL_VERSION,\n DND_NEW,\n hasWildcardPattern,\n isAllowedEditorOrigin,\n type PlenaryDocument,\n type PlenaryNode,\n type ManifestPayload,\n type RenderPayload,\n type PatchPayload,\n type HighlightPayload,\n type CanvasDragDropPayload,\n type CanvasDragPayload,\n type CanvasDropPayload,\n type CanvasDropTarget,\n type SizePayload,\n type WheelPayload,\n type PanPayload,\n type KeyPayload,\n type EditPayload,\n} from \"@plenary/core\";\nimport { renderDocumentSync } from \"./render.js\";\nimport { getManifest, getTheme } from \"./registry.js\";\n\nexport interface PlenaryBridgeProps {\n /**\n * The `blocks` field on the entry this bridge previews — the key in the draft's\n * `data` whose component tree it renders. One `<PlenaryBridge>` drives one region.\n */\n region: string;\n /**\n * Console origins allowed to drive this preview; others are ignored. Each\n * entry is an exact origin (`https://cms.example.com`, `http://localhost:5173`)\n * or a pattern: `https://*.example.com` allows any subdomain (never the bare\n * apex), `http://localhost:*` allows any port — so a locally-running console\n * can drive a production site. See `isAllowedEditorOrigin` in `@plenary/core`.\n */\n editorOrigins: string[];\n /** Reported to the console in the `hello` handshake. */\n sdkVersion?: string;\n /** Normal page content, rendered until a draft render takes over. */\n children?: ReactNode;\n}\n\ninterface Draft {\n data: Record<string, unknown>;\n locale: string;\n}\n\n/** Return a new document with `propsPatch` merged into the node with `nodeId`. */\nfunction patchDom(\n doc: PlenaryDocument,\n nodeId: string,\n propsPatch: Record<string, unknown>,\n): PlenaryDocument {\n const patch = (nodes: PlenaryNode[]): PlenaryNode[] =>\n nodes.map((node) => {\n let next = node;\n if (node.id === nodeId) next = { ...next, props: { ...next.props, ...propsPatch } };\n if (next.slots) {\n const slots: Record<string, PlenaryNode[]> = {};\n for (const [name, children] of Object.entries(next.slots)) slots[name] = patch(children);\n next = { ...next, slots };\n }\n return next;\n });\n return { ...doc, root: patch(doc.root) };\n}\n\nfunction nodeIdFromEvent(e: ReactMouseEvent): string | null {\n const el = (e.target as HTMLElement | null)?.closest?.(\"[data-plenary-id]\");\n return el?.getAttribute(\"data-plenary-id\") ?? null;\n}\n\nfunction findNodeById(nodes: PlenaryNode[], id: string): PlenaryNode | null {\n for (const node of nodes) {\n if (node.id === id) return node;\n if (node.slots) {\n for (const children of Object.values(node.slots)) {\n const hit = findNodeById(children, id);\n if (hit) return hit;\n }\n }\n }\n return null;\n}\n\n/**\n * The contenteditable mode for inline editing: `plaintext-only` where supported\n * (assigning an unsupported value to the property throws per spec), else `true`\n * — pasted markup is flattened on read either way.\n */\nlet editableMode: string | null = null;\nfunction getEditableMode(): string {\n if (editableMode) return editableMode;\n const probe = document.createElement(\"div\");\n try {\n probe.contentEditable = \"plaintext-only\";\n editableMode = \"plaintext-only\";\n } catch {\n editableMode = \"true\";\n }\n return editableMode;\n}\n\n/** A live inline-editing session on a text block's rendered element. */\ninterface EditSession {\n nodeId: string;\n el: HTMLElement;\n /** The committed content when editing began, to skip no-op edits. */\n original: string;\n onBlur: () => void;\n}\n\n/**\n * Keep the framed page from navigating away from the entry being edited. While\n * `locked`, link clicks and form submits inside the frame are cancelled — but\n * nothing else is: the listeners run in the capture phase and only call\n * `preventDefault()`, never `stopPropagation()`, so the bridge's own click/hover\n * selection (and any other event the SDK relies on) still fire normally.\n *\n * Toggleable by design: pass `false` and the lock lifts. It is tied to the\n * active preview today; when the console later lets the user browse their own\n * site and resolves the entry from the preview URL, that mode would drive this\n * flag (over the protocol) instead of blocking outright.\n */\nfunction useNavigationLock(locked: boolean) {\n useEffect(() => {\n if (!locked || typeof document === \"undefined\") return;\n const onClick = (e: MouseEvent) => {\n if ((e.target as Element | null)?.closest?.(\"a[href]\")) e.preventDefault();\n };\n const onSubmit = (e: Event) => e.preventDefault();\n document.addEventListener(\"click\", onClick, true);\n document.addEventListener(\"submit\", onSubmit, true);\n return () => {\n document.removeEventListener(\"click\", onClick, true);\n document.removeEventListener(\"submit\", onSubmit, true);\n };\n }, [locked]);\n}\n\ninterface DropHint {\n target: CanvasDropTarget;\n /** Viewport rect of the reference element, for drawing the indicator. */\n rect: DOMRect | null;\n mode: \"before\" | \"after\" | \"into\";\n}\n\n/**\n * Resolve a pointer position over the rendered preview to a tree drop target:\n * before/after the block under the cursor, into an empty Section column, or the\n * region root. Uses the `data-plenary-id` / `data-plenary-column` annotations the\n * renderer emits.\n */\nfunction resolveDrop(x: number, y: number): DropHint {\n const root = document.querySelector(\"[data-plenary-preview]\");\n const el = document.elementFromPoint(x, y) as HTMLElement | null;\n if (!root || !el || !root.contains(el)) {\n return { target: { at: \"root\" }, rect: root?.getBoundingClientRect() ?? null, mode: \"after\" };\n }\n const marker = el.closest(\"[data-plenary-id]\") as HTMLElement | null;\n const column = el.closest(\"[data-plenary-column]\") as HTMLElement | null;\n\n // Empty column space: the nearest block marker is the Section (an ancestor of\n // the column), not a child inside it → drop into that column's slot.\n if (column && (!marker || !column.contains(marker))) {\n const sectionId = column.closest(\"[data-plenary-id]\")?.getAttribute(\"data-plenary-id\");\n const slot = column.getAttribute(\"data-plenary-column\");\n if (sectionId && slot != null) {\n return { target: { at: \"slot\", nodeId: sectionId, slot }, rect: column.getBoundingClientRect(), mode: \"into\" };\n }\n }\n\n if (marker) {\n const id = marker.getAttribute(\"data-plenary-id\")!;\n // Markers use display:contents (no box); measure the rendered child.\n const box = (marker.firstElementChild as HTMLElement | null) ?? marker;\n const rect = box.getBoundingClientRect();\n const after = y > rect.top + rect.height / 2;\n return { target: { at: after ? \"after\" : \"before\", nodeId: id }, rect, mode: after ? \"after\" : \"before\" };\n }\n\n return { target: { at: \"root\" }, rect: root.getBoundingClientRect(), mode: \"after\" };\n}\n\n/**\n * The iframe side of live preview. Mounted by the site; inert until a valid\n * handshake from an allowed origin arrives. Once active it renders the draft the\n * console pushes over postMessage instead of the normal page, and reports\n * clicks/hovers back for block selection.\n *\n * Origin discipline is the security model: every inbound message must come from\n * an `editorOrigins` origin and carry `{ plenary: true }`.\n */\nexport function PlenaryBridge({\n region,\n editorOrigins,\n sdkVersion = \"0.0.0\",\n children,\n}: PlenaryBridgeProps) {\n const [draft, setDraft] = useState<Draft | null>(null);\n const [active, setActive] = useState(false);\n // Two outline channels the console drives independently: the selected block and\n // whatever is hovered (a tree row or a preview element). Drawn with distinct styles.\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [hoverId, setHoverId] = useState<string | null>(null);\n const [dropHint, setDropHint] = useState<DropHint | null>(null);\n const consoleOrigin = useRef<string | null>(null);\n // Nodes whose DOM must be rebuilt from the document on the next render (their\n // key changes), because an inline edit mutated DOM React never tracked.\n const [remountEpochs, setRemountEpochs] = useState<ReadonlyMap<string, number>>(new Map());\n const editSession = useRef<EditSession | null>(null);\n\n // While the console is driving the preview, don't let a stray link or form\n // submit navigate the frame off the entry being edited.\n useNavigationLock(active);\n\n // The component tree this bridge renders: the `region` blocks field of the draft.\n const regionDoc = draft ? (draft.data[region] as PlenaryDocument | undefined) : undefined;\n\n const send = useCallback((type: string, payload: unknown, origin?: string) => {\n const target = origin ?? consoleOrigin.current;\n if (typeof window === \"undefined\" || !target) return;\n window.parent.postMessage({ plenary: true, v: PROTOCOL_VERSION, type, payload }, target);\n }, []);\n\n /** End the inline-editing session: tear down, remount the node, send the change up. */\n const endEdit = useCallback(\n (commit: boolean) => {\n const session = editSession.current;\n if (!session) return;\n editSession.current = null;\n const { nodeId, el, original, onBlur } = session;\n el.removeEventListener(\"blur\", onBlur);\n el.removeAttribute(\"contenteditable\");\n const text = (el.innerText ?? el.textContent ?? \"\").toString();\n // Rebuild the node from the document: typing left DOM state React never saw.\n setRemountEpochs((prev) => new Map(prev).set(nodeId, (prev.get(nodeId) ?? 0) + 1));\n if (!commit || text === original) return;\n // Apply locally first so the remounted node renders the new text at once;\n // the console's render echo then arrives already matching.\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, nodeId, { content: text }) } };\n });\n send(\"edit\", { nodeId, props: { content: text } } satisfies EditPayload);\n },\n [region, send],\n );\n\n useEffect(() => {\n if (typeof window === \"undefined\" || window.parent === window) return; // not framed\n\n // Report the page's content height so the console's artboard can size the\n // iframe to the whole page (no inner scrollbar). Sent on init and, trailing-\n // throttled, whenever the body's size changes; send() drops it until the\n // console origin is known.\n const reportSize = () => {\n send(\"size\", { height: Math.ceil(document.documentElement.scrollHeight) } satisfies SizePayload);\n };\n let sizeTimer: ReturnType<typeof setTimeout> | null = null;\n let observer: ResizeObserver | null = null;\n if (typeof ResizeObserver !== \"undefined\") {\n observer = new ResizeObserver(() => {\n if (sizeTimer) return;\n sizeTimer = setTimeout(() => {\n sizeTimer = null;\n reportSize();\n }, 150);\n });\n observer.observe(document.body);\n }\n\n // Artboard gestures performed over the page. The iframe consumes these\n // events, so the console never sees them — forward them once connected:\n // Ctrl/Cmd+wheel becomes a zoom request (page point included so the console\n // can anchor the zoom at the cursor), and a middle-button drag becomes pan\n // steps (the hand tool). Both are prevented locally: the page must neither\n // scroll under a zoom gesture nor start autoscroll under a pan.\n const onWheelGesture = (e: WheelEvent) => {\n if (!consoleOrigin.current || !(e.ctrlKey || e.metaKey)) return;\n e.preventDefault();\n send(\"wheel\", { deltaY: e.deltaY, x: e.clientX, y: e.clientY } satisfies WheelPayload);\n };\n let panFrom: { x: number; y: number } | null = null;\n const onPanStart = (e: PointerEvent) => {\n if (!consoleOrigin.current || e.button !== 1) return;\n panFrom = { x: e.clientX, y: e.clientY };\n // Capture keeps the drag alive even when the pointer leaves the iframe.\n try {\n (e.target as Element | null)?.setPointerCapture?.(e.pointerId);\n } catch {\n /* capture is best-effort */\n }\n };\n const onPanMove = (e: PointerEvent) => {\n if (!panFrom) return;\n // The middle button was released outside our reach (e.g. over browser chrome).\n if ((e.buttons & 4) === 0) {\n panFrom = null;\n return;\n }\n send(\"pan\", { dx: e.clientX - panFrom.x, dy: e.clientY - panFrom.y } satisfies PanPayload);\n panFrom = { x: e.clientX, y: e.clientY };\n };\n const onPanEnd = (e: PointerEvent) => {\n if (e.button === 1) panFrom = null;\n };\n // Middle-button autoscroll is a `mousedown` default, not a pointerdown one.\n const onMiddleMouseDown = (e: MouseEvent) => {\n if (consoleOrigin.current && e.button === 1) e.preventDefault();\n };\n // Editor shortcuts pressed while the frame has focus (clicking a block\n // focuses the iframe, so the console's own keydown listener never fires).\n // Forward only recognized shortcut combinations, preventing their default\n // here — the frame must not run its own native undo or save dialog.\n const onKeyDown = (e: KeyboardEvent) => {\n // Inside an inline-edit session, Escape cancels and Enter commits\n // (Shift+Enter keeps the native line break); all other keys are native\n // editing — never forwarded (the editable-target check below also holds).\n const session = editSession.current;\n if (session && session.el.contains(e.target as Node)) {\n if (e.key === \"Escape\") {\n e.preventDefault();\n endEdit(false);\n } else if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n endEdit(true);\n }\n return;\n }\n if (!consoleOrigin.current) return;\n const t = e.target as HTMLElement | null;\n if (\n t &&\n (t.isContentEditable ||\n t instanceof HTMLInputElement ||\n t instanceof HTMLTextAreaElement ||\n t instanceof HTMLSelectElement)\n )\n return;\n const key = e.key.toLowerCase();\n const mod = e.ctrlKey || e.metaKey;\n // A live text selection in the page keeps native copy/cut.\n if (mod && (key === \"c\" || key === \"x\") && document.getSelection()?.isCollapsed === false)\n return;\n const forward = mod\n ? [\"z\", \"y\", \"s\", \"d\", \"c\", \"x\", \"v\"].includes(key)\n : key === \"delete\" || key === \"backspace\";\n if (!forward) return;\n e.preventDefault();\n send(\"key\", {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n } satisfies KeyPayload);\n };\n document.addEventListener(\"keydown\", onKeyDown);\n document.addEventListener(\"wheel\", onWheelGesture, { passive: false });\n document.addEventListener(\"pointerdown\", onPanStart);\n document.addEventListener(\"pointermove\", onPanMove);\n document.addEventListener(\"pointerup\", onPanEnd);\n document.addEventListener(\"mousedown\", onMiddleMouseDown);\n\n const onMessage = (e: MessageEvent) => {\n if (!isAllowedEditorOrigin(editorOrigins, e.origin)) return;\n const msg = e.data as { plenary?: boolean; type?: string; payload?: unknown } | null;\n if (!msg || msg.plenary !== true) return;\n\n switch (msg.type) {\n case \"init\":\n consoleOrigin.current = e.origin;\n setActive(true);\n send(\n \"manifest\",\n { components: getManifest(), theme: getTheme() } satisfies ManifestPayload,\n e.origin,\n );\n reportSize();\n break;\n case \"render\":\n consoleOrigin.current = e.origin;\n setActive(true);\n // A draft arriving mid-edit means the console changed the document\n // (e.g. toolbar undo); it is authoritative — cancel the session.\n endEdit(false);\n setDraft(msg.payload as RenderPayload);\n break;\n case \"patch\": {\n endEdit(false);\n const p = msg.payload as PatchPayload;\n setDraft((prev) => {\n if (!prev) return prev;\n const doc = prev.data[region] as PlenaryDocument | undefined;\n if (!doc) return prev;\n return { ...prev, data: { ...prev.data, [region]: patchDom(doc, p.nodeId, p.props) } };\n });\n break;\n }\n case \"highlight\": {\n const p = msg.payload as HighlightPayload;\n setSelectedId(p.selectedId);\n setHoverId(p.hoverId);\n break;\n }\n // A palette drag the console is tracking on our behalf. It reports the\n // pointer in our own coordinate space; resolving it against the\n // rendered DOM is something only this side can do.\n case \"canvasDragOver\": {\n const p = msg.payload as CanvasDragPayload;\n setDropHint(resolveDrop(p.x, p.y));\n break;\n }\n case \"canvasDragLeave\":\n setDropHint(null);\n break;\n case \"canvasDragDrop\": {\n const p = msg.payload as CanvasDragDropPayload;\n setDropHint(null);\n const { target } = resolveDrop(p.x, p.y);\n send(\"canvasDrop\", { component: p.component, target } satisfies CanvasDropPayload, e.origin);\n break;\n }\n }\n };\n\n window.addEventListener(\"message\", onMessage);\n // Announce readiness to every allowed console origin. Exact origins are\n // addressed directly; a wildcard pattern cannot be a postMessage target, so\n // when any is configured the announce also goes out with target \"*\". That is\n // safe for `hello` alone — it carries only version numbers — and everything\n // after it is sent strictly to the matched origin of a validated `init`.\n const hello = { protocolVersion: PROTOCOL_VERSION, sdkVersion };\n for (const origin of editorOrigins) {\n if (!origin.includes(\"*\")) send(\"hello\", hello, origin);\n }\n if (hasWildcardPattern(editorOrigins)) send(\"hello\", hello, \"*\");\n return () => {\n window.removeEventListener(\"message\", onMessage);\n document.removeEventListener(\"keydown\", onKeyDown);\n document.removeEventListener(\"wheel\", onWheelGesture);\n document.removeEventListener(\"pointerdown\", onPanStart);\n document.removeEventListener(\"pointermove\", onPanMove);\n document.removeEventListener(\"pointerup\", onPanEnd);\n document.removeEventListener(\"mousedown\", onMiddleMouseDown);\n observer?.disconnect();\n if (sizeTimer) clearTimeout(sizeTimer);\n };\n }, [editorOrigins, sdkVersion, region, send, endEdit]);\n\n const beginEdit = (nodeId: string, el: HTMLElement) => {\n endEdit(true); // switching targets commits the previous session\n const onBlur = () => endEdit(true);\n el.setAttribute(\"contenteditable\", getEditableMode());\n el.addEventListener(\"blur\", onBlur);\n editSession.current = { nodeId, el, original: el.textContent ?? \"\", onBlur };\n el.focus();\n };\n\n const onPreviewDoubleClick = (e: ReactMouseEvent) => {\n const id = nodeIdFromEvent(e);\n if (!id || !regionDoc) return;\n // v1: inline editing is the built-in text block's `content` — its DOM is\n // predictably marker > element > text node.\n if (findNodeById(regionDoc.root, id)?.component !== \"text\") return;\n const el = (e.target as HTMLElement).closest(\"[data-plenary-id]\")\n ?.firstElementChild as HTMLElement | null;\n if (el) beginEdit(id, el);\n };\n\n // Native drag handling, kept for a preview served from the console's own\n // origin — the only case where the browser delivers drag events to the frame.\n // A cross-origin preview (every real deployment) gets nothing here and is\n // driven by the console's `canvasDrag*` messages above instead.\n const isPaletteDrag = (e: ReactDragEvent) => e.dataTransfer.types.includes(DND_NEW);\n\n const onDragOver = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n setDropHint(resolveDrop(e.clientX, e.clientY));\n };\n const onDrop = (e: ReactDragEvent) => {\n if (!isPaletteDrag(e)) return;\n e.preventDefault();\n const component = e.dataTransfer.getData(DND_NEW);\n const { target } = resolveDrop(e.clientX, e.clientY);\n if (component) send(\"canvasDrop\", { component, target } satisfies CanvasDropPayload);\n setDropHint(null);\n };\n const onDragLeave = (e: ReactDragEvent) => {\n // Clear only when the pointer leaves the preview root entirely.\n if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setDropHint(null);\n };\n\n if (!active) return <>{children}</>;\n\n return (\n <div\n data-plenary-preview=\"\"\n onClick={(e) => {\n const id = nodeIdFromEvent(e);\n if (id) send(\"select\", { nodeId: id });\n }}\n onDoubleClick={onPreviewDoubleClick}\n onMouseOver={(e) => send(\"hover\", { nodeId: nodeIdFromEvent(e) })}\n onMouseOut={() => send(\"hover\", { nodeId: null })}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onDragLeave={onDragLeave}\n >\n {regionDoc ? renderDocumentSync(regionDoc, { annotate: true, remountKeys: remountEpochs }) : null}\n {regionDoc\n ? videoControlsBadgeIds(regionDoc).map((id) => <VideoControlsBadge key={id} nodeId={id} />)\n : null}\n {selectedId ? <HighlightOverlay nodeId={selectedId} variant=\"selected\" /> : null}\n {hoverId && hoverId !== selectedId ? (\n <HighlightOverlay nodeId={hoverId} variant=\"hover\" />\n ) : null}\n {dropHint ? <DropIndicator hint={dropHint} /> : null}\n </div>\n );\n}\n\n/** The live insertion indicator drawn over the canvas while dragging a component. */\nfunction DropIndicator({ hint }: { hint: DropHint }) {\n if (!hint.rect) return null;\n const { rect, mode } = hint;\n const base: CSSProperties = { position: \"fixed\", pointerEvents: \"none\", zIndex: 2147483647 };\n if (mode === \"into\") {\n return (\n <div\n style={{\n ...base,\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n outline: \"2px dashed #4f46e5\",\n outlineOffset: \"-2px\",\n background: \"rgba(79,70,229,0.06)\",\n }}\n />\n );\n }\n const y = mode === \"before\" ? rect.top : rect.bottom;\n return (\n <div style={{ ...base, left: rect.left, top: y - 1, width: rect.width, height: 2, background: \"#4f46e5\" }} />\n );\n}\n\nfunction sameBox(a: DOMRect | null, b: DOMRect | null): boolean {\n if (!a || !b) return a === b;\n return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;\n}\n\n/**\n * A fixed outline over a highlighted node, driven from the console sidebar. The\n * `selected` variant marks the block being edited (thick, solid, saturated); the\n * `hover` variant marks whatever the cursor is over (thinner, dashed, lighter) so\n * the two read as clearly different states.\n */\nconst OUTLINE_STYLE: Record<\"selected\" | \"hover\", CSSProperties> = {\n selected: { outline: \"2px solid #4f46e5\" },\n hover: { outline: \"1px dashed #818cf8\" },\n};\n\n// An overlay is positioned in viewport coordinates, so it goes stale as soon\n// as the block moves — a scroll, a resize, an image finishing loading, or an\n// edit that reflows the page above it. Re-measuring every frame tracks all of\n// those without enumerating them; state changes only when the box actually\n// does, so a still page costs one measurement per frame and no re-render.\nfunction useNodeRect(nodeId: string): DOMRect | null {\n const [rect, setRect] = useState<DOMRect | null>(null);\n\n useEffect(() => {\n let frame = 0;\n const measure = () => {\n const marker = document.querySelector(`[data-plenary-id=\"${CSS.escape(nodeId)}\"]`);\n // Markers use display:contents (no box of their own), so measure the child.\n const target = (marker?.firstElementChild as HTMLElement | null) ?? (marker as HTMLElement | null);\n const next = target ? target.getBoundingClientRect() : null;\n setRect((prev) => (sameBox(prev, next) ? prev : next));\n frame = requestAnimationFrame(measure);\n };\n measure();\n return () => cancelAnimationFrame(frame);\n }, [nodeId]);\n\n return rect;\n}\n\nfunction HighlightOverlay({ nodeId, variant }: { nodeId: string; variant: \"selected\" | \"hover\" }) {\n const rect = useNodeRect(nodeId);\n if (!rect) return null;\n return (\n <div\n style={{\n position: \"fixed\",\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n ...OUTLINE_STYLE[variant],\n pointerEvents: \"none\",\n zIndex: 2147483647,\n }}\n />\n );\n}\n\n/**\n * Ids of video nodes whose published player will show native controls. The\n * editor render hides them (a controls-less video lets clicks bubble to\n * selection), so the canvas draws a cosmetic badge instead — the hint that\n * the real page will have a control bar. Fidelity note: the renderer does not\n * merge registration defaults into props, so only an explicit truthy\n * `controls` counts, exactly like the published render. Media-less nodes show\n * the placeholder, which carries its own glyph — as do nodes whose asset was\n * deleted, where the published page renders no player to hint at either.\n */\nfunction videoControlsBadgeIds(doc: PlenaryDocument): string[] {\n const ids: string[] = [];\n const walk = (nodes: PlenaryNode[]): void => {\n for (const node of nodes) {\n const media = node.props?.media as { missing?: boolean } | undefined;\n if (node.component === \"video\" && node.props?.controls && media && !media.missing) {\n ids.push(node.id);\n }\n for (const children of Object.values(node.slots ?? {})) walk(children);\n }\n };\n walk(doc.root);\n return ids;\n}\n\n/**\n * A miniature control-bar chip drawn over a video block's bottom-left corner.\n * Purely cosmetic (pointer-events none) and drawn outside the annotated tree,\n * so node styles and selection behavior are untouched.\n */\nfunction VideoControlsBadge({ nodeId }: { nodeId: string }) {\n const rect = useNodeRect(nodeId);\n // On a tiny player the chip would cover the content it hints at.\n if (!rect || rect.width < 96 || rect.height < 48) return null;\n\n const glyph = (d: string) => (\n <svg key={d} width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d={d} />\n </svg>\n );\n return (\n <div\n data-plenary-video-controls-badge=\"\"\n style={{\n position: \"fixed\",\n left: rect.left + 8,\n top: rect.bottom - 26,\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n padding: \"4px 9px\",\n borderRadius: 9999,\n background: \"rgba(0,0,0,0.65)\",\n color: \"#fff\",\n pointerEvents: \"none\",\n zIndex: 2147483646, // just under the highlight outlines\n }}\n >\n {glyph(\"M8 5v14l11-7z\") /* play */}\n {glyph(\"M3 9v6h4l5 5V4L7 9H3zm13.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z\") /* volume */}\n {glyph(\"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z\") /* fullscreen */}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @plenary/react — the DOM renderer, component registry, and built-in\n * components (Box/Section/Text/Image/Video/Form) for rendering a Page's `dom`\n * tree. Custom components register on top of the built-ins. The preview bridge\n * lands in Phase 5.\n */\n\nexport {\n PlenaryPage,\n renderDocument,\n renderDocumentSync,\n type PlenaryPageProps,\n type SyncRenderOptions,\n} from \"./render.js\";\nexport {\n Registry,\n registry,\n register,\n registerTheme,\n Plenary,\n getManifest,\n getTheme,\n} from \"./registry.js\";\nexport { Box, type BoxProps } from \"./components/Box.js\";\nexport { Section, type SectionProps } from \"./components/Section.js\";\nexport { Text, type TextProps } from \"./components/Text.js\";\nexport { Image, type ImageProps } from \"./components/Image.js\";\nexport { Video, type VideoProps } from \"./components/Video.js\";\nexport { Form, type FormProps } from \"./components/Form.js\";\nexport { Placeholder, type PlaceholderProps } from \"./components/Placeholder.js\";\nexport {\n resolveMedia,\n toResolvedMedia,\n type MediaValue,\n type ResolvedMedia,\n} from \"./media.js\";\nexport { toResolvedForm, type FormValue, type ResolvedForm } from \"./form.js\";\nexport {\n PlenaryProvider,\n usePlenaryClient,\n type PlenaryProviderProps,\n} from \"./context.js\";\nexport type {\n PlenaryComponent,\n RegisteredComponent,\n RenderContext,\n SlotProps,\n} from \"./types.js\";\n"],"mappings":"AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAA0B;AACnC,SAAS,eAAkC;AAC3C,SAAS,YAA4B;AACrC,SAAS,aAA8B;AACvC,SAAS,aAA8B;AACvC,SAAS,YAA4B;AACrC,SAAS,mBAA0C;AACnD;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,sBAAyD;AAClE;AAAA,EACE;AAAA,EACA;AAAA,OAEK;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @plenary/react — the DOM renderer, component registry, and built-in\n * components (Box/Section/Text/Image/Video/Form) for rendering a Page's `dom`\n * tree. Custom components register on top of the built-ins; the preview bridge\n * lives in the `./bridge` export.\n */\n\nexport {\n PlenaryPage,\n renderDocument,\n renderDocumentSync,\n type PlenaryPageProps,\n type SyncRenderOptions,\n} from \"./render.js\";\nexport {\n Registry,\n registry,\n register,\n registerTheme,\n Plenary,\n getManifest,\n getTheme,\n} from \"./registry.js\";\nexport { Box, type BoxProps } from \"./components/Box.js\";\nexport { Section, type SectionProps } from \"./components/Section.js\";\nexport { Text, type TextProps } from \"./components/Text.js\";\nexport { Image, type ImageProps } from \"./components/Image.js\";\nexport { Video, type VideoProps } from \"./components/Video.js\";\nexport { Form, type FormProps } from \"./components/Form.js\";\nexport { Placeholder, type PlaceholderProps } from \"./components/Placeholder.js\";\nexport {\n resolveMedia,\n toResolvedMedia,\n type MediaValue,\n type ResolvedMedia,\n} from \"./media.js\";\nexport { toResolvedForm, type FormValue, type ResolvedForm } from \"./form.js\";\nexport {\n PlenaryProvider,\n usePlenaryClient,\n type PlenaryProviderProps,\n} from \"./context.js\";\nexport type {\n PlenaryComponent,\n RegisteredComponent,\n RenderContext,\n SlotProps,\n} from \"./types.js\";\n"],"mappings":"AAOA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAA0B;AACnC,SAAS,eAAkC;AAC3C,SAAS,YAA4B;AACrC,SAAS,aAA8B;AACvC,SAAS,aAA8B;AACvC,SAAS,YAA4B;AACrC,SAAS,mBAA0C;AACnD;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,sBAAyD;AAClE;AAAA,EACE;AAAA,EACA;AAAA,OAEK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Plenary React renderer, component registry, built-in components, and preview bridge.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -20,8 +20,8 @@
20
20
  "dist"
21
21
  ],
22
22
  "dependencies": {
23
- "@plenary/client": "0.1.0",
24
- "@plenary/core": "0.1.0"
23
+ "@plenary/client": "0.2.0",
24
+ "@plenary/core": "0.2.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^18 || ^19",